package db

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"
)

const migrationsTableDDL = `
CREATE TABLE IF NOT EXISTS schema_migrations (
	name TEXT PRIMARY KEY,
	checksum_sha256 TEXT NOT NULL,
	applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
`

func Migrate(ctx context.Context, database *sql.DB, migrationsDir string) error {
	if migrationsDir == "" {
		return fmt.Errorf("migrations dir is required")
	}

	if _, err := database.ExecContext(ctx, migrationsTableDDL); err != nil {
		return fmt.Errorf("ensure schema_migrations table: %w", err)
	}

	applied, err := appliedMigrations(ctx, database)
	if err != nil {
		return err
	}

	files, err := listMigrationFiles(migrationsDir)
	if err != nil {
		return err
	}

	for _, filename := range files {
		fullPath := filepath.Join(migrationsDir, filename)

		contents, err := os.ReadFile(fullPath)
		if err != nil {
			return fmt.Errorf("read migration %s: %w", filename, err)
		}

		checksum := checksum(contents)
		if appliedChecksum, ok := applied[filename]; ok {
			if appliedChecksum != checksum {
				return fmt.Errorf("migration %s checksum mismatch: applied=%s current=%s", filename, appliedChecksum, checksum)
			}

			continue
		}

		if err := applyMigration(ctx, database, filename, checksum, string(contents)); err != nil {
			return err
		}
	}

	return nil
}

func appliedMigrations(ctx context.Context, database *sql.DB) (map[string]string, error) {
	rows, err := database.QueryContext(ctx, `SELECT name, checksum_sha256 FROM schema_migrations`)
	if err != nil {
		return nil, fmt.Errorf("load applied migrations: %w", err)
	}
	defer rows.Close()

	applied := make(map[string]string)
	for rows.Next() {
		var name string
		var checksum string

		if err := rows.Scan(&name, &checksum); err != nil {
			return nil, fmt.Errorf("scan applied migration: %w", err)
		}

		applied[name] = checksum
	}

	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate applied migrations: %w", err)
	}

	return applied, nil
}

func listMigrationFiles(migrationsDir string) ([]string, error) {
	entries, err := os.ReadDir(migrationsDir)
	if err != nil {
		return nil, fmt.Errorf("read migrations dir: %w", err)
	}

	files := make([]string, 0, len(entries))
	for _, entry := range entries {
		if entry.IsDir() || filepath.Ext(entry.Name()) != ".sql" {
			continue
		}

		files = append(files, entry.Name())
	}

	slices.Sort(files)

	return files, nil
}

func applyMigration(ctx context.Context, database *sql.DB, filename, checksum, sqlText string) error {
	tx, err := database.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("begin migration %s: %w", filename, err)
	}

	if strings.TrimSpace(sqlText) != "" {
		if _, err := tx.ExecContext(ctx, sqlText); err != nil {
			tx.Rollback()
			return fmt.Errorf("execute migration %s: %w", filename, err)
		}
	}

	if _, err := tx.ExecContext(
		ctx,
		`INSERT INTO schema_migrations (name, checksum_sha256) VALUES (?, ?)`,
		filename,
		checksum,
	); err != nil {
		tx.Rollback()
		return fmt.Errorf("record migration %s: %w", filename, err)
	}

	if err := tx.Commit(); err != nil {
		return fmt.Errorf("commit migration %s: %w", filename, err)
	}

	return nil
}

func checksum(contents []byte) string {
	sum := sha256.Sum256(contents)
	return hex.EncodeToString(sum[:])
}
