package db

import (
	"context"
	"database/sql"
	"fmt"

	"tangled.org/core/log"
	"tangled.org/core/tid"
)

var Migrations = []Migration{
	{
		Name: "repos_pk_to_repo_did",
		Fn:   reposPkToRepoDid,
	},
	{
		Name: "hosts_add_last_tid",
		Fn:   hostsAddLastTid,
	},
	{
		Name: "hosts_backfill_last_tid_from_last_seq",
		Fn:   hostsBackfillLastTidFromLastSeq,
	},
	{
		Name: "hosts_drop_last_seq",
		Fn:   hostsDropLastSeq,
	},
}

func hostsAddLastTid(ctx context.Context, tx *sql.Tx) error {
	_, err := tx.ExecContext(ctx,
		`alter table hosts add column if not exists last_tid text not null default ''`,
	)
	return err
}

func hostsBackfillLastTidFromLastSeq(ctx context.Context, tx *sql.Tx) error {
	var hasLastSeq bool
	if err := tx.QueryRowContext(ctx,
		`select exists (
			select 1 from information_schema.columns
			where table_name = 'hosts' and column_name = 'last_seq'
		)`,
	).Scan(&hasLastSeq); err != nil {
		return fmt.Errorf("checking last_seq column: %w", err)
	}
	if !hasLastSeq {
		return nil
	}

	rows, err := tx.QueryContext(ctx,
		`select hostname, last_seq from hosts where last_seq > 0 and last_tid = ''`,
	)
	if err != nil {
		return fmt.Errorf("reading hosts: %w", err)
	}
	defer rows.Close()

	type entry struct {
		hostname string
		lastSeq  int64
	}
	var entries []entry
	for rows.Next() {
		var e entry
		if err := rows.Scan(&e.hostname, &e.lastSeq); err != nil {
			return fmt.Errorf("scanning host row: %w", err)
		}
		entries = append(entries, e)
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterating host rows: %w", err)
	}

	for _, e := range entries {
		gen := tid.MonotonicGenerator{}
		newTid := gen.FromNanos(e.lastSeq)
		if _, err := tx.ExecContext(ctx,
			`update hosts set last_tid = $1 where hostname = $2`,
			newTid, e.hostname,
		); err != nil {
			return fmt.Errorf("backfilling %s: %w", e.hostname, err)
		}
	}
	return nil
}

func hostsDropLastSeq(ctx context.Context, tx *sql.Tx) error {
	_, err := tx.ExecContext(ctx,
		`alter table hosts drop column if exists last_seq`,
	)
	return err
}

func reposPkToRepoDid(ctx context.Context, tx *sql.Tx) error {
	if _, err := tx.ExecContext(ctx,
		`alter table repos add column if not exists repo_did text`,
	); err != nil {
		return fmt.Errorf("adding repo_did column: %w", err)
	}
	var bad int
	if err := tx.QueryRowContext(ctx,
		`select count(*) from repos where repo_did is null or repo_did = ''`,
	).Scan(&bad); err != nil {
		return fmt.Errorf("counting rows with null repo_did: %w", err)
	}
	if bad > 0 {
		log.FromContext(ctx).Warn(
			"dropping repos with null repo_did; their on-disk dirs will be orphaned. re-crawl via tap to restore",
			"count", bad,
		)
		if _, err := tx.ExecContext(ctx,
			`delete from repos where repo_did is null or repo_did = ''`,
		); err != nil {
			return fmt.Errorf("deleting null repo_did rows: %w", err)
		}
	}
	return execAll(ctx, tx,
		`alter table repos alter column repo_did set not null`,
		`alter table repos drop constraint if exists repos_pkey`,
		`alter table repos add constraint repos_pkey primary key (repo_did)`,
	)
}

func execAll(ctx context.Context, tx *sql.Tx, stmts ...string) error {
	if len(stmts) == 0 {
		return nil
	}
	if _, err := tx.ExecContext(ctx, stmts[0]); err != nil {
		return err
	}
	return execAll(ctx, tx, stmts[1:]...)
}
