package migration

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

// Options controls migration repair behavior.
type Options struct {
	// AllowChecksumRepair lets local/dev databases accept edited historical migrations.
	// Production-like environments must keep this false so checksum drift fails closed.
	AllowChecksumRepair bool
}

func Apply(ctx context.Context, db *sql.DB, dir string) error {
	return ApplyWithOptions(ctx, db, dir, Options{})
}

func ApplyWithOptions(ctx context.Context, db *sql.DB, dir string, options Options) error {
	if strings.TrimSpace(dir) == "" {
		return errors.New("migration dir is required")
	}
	if _, err := os.Stat(dir); err != nil {
		return err
	}
	if err := ensureSchemaTable(ctx, db); err != nil {
		return err
	}

	files, err := migrationFiles(dir)
	if err != nil {
		return err
	}
	for _, file := range files {
		version := filepath.Base(file)
		applied, err := isApplied(ctx, db, version, file, options)
		if err != nil {
			return err
		}
		if applied {
			continue
		}
		if err := applyFile(ctx, db, version, file); err != nil {
			return err
		}
	}
	return nil
}

func ensureSchemaTable(ctx context.Context, db *sql.DB) error {
	if _, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS schema_migrations (
  version VARCHAR(128) PRIMARY KEY,
  checksum CHAR(64) NOT NULL DEFAULT '',
  dirty BOOLEAN NOT NULL DEFAULT FALSE,
  applied_at_ms BIGINT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`); err != nil {
		return err
	}
	if err := ensureColumn(ctx, db, "checksum", "ALTER TABLE schema_migrations ADD COLUMN checksum CHAR(64) NOT NULL DEFAULT '' AFTER version"); err != nil {
		return err
	}
	if err := ensureColumn(ctx, db, "dirty", "ALTER TABLE schema_migrations ADD COLUMN dirty BOOLEAN NOT NULL DEFAULT FALSE AFTER checksum"); err != nil {
		return err
	}
	return ensureColumn(ctx, db, "applied_at_ms", "ALTER TABLE schema_migrations ADD COLUMN applied_at_ms BIGINT NOT NULL DEFAULT 0 AFTER dirty")
}

func ensureColumn(ctx context.Context, db *sql.DB, column string, alter string) error {
	var count int
	err := db.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'schema_migrations' AND column_name = ?`, column).Scan(&count)
	if err != nil {
		return err
	}
	if count > 0 {
		return nil
	}
	_, err = db.ExecContext(ctx, alter)
	return err
}

func migrationFiles(dir string) ([]string, error) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	files := make([]string, 0, len(entries))
	for _, entry := range entries {
		if entry.IsDir() || filepath.Ext(entry.Name()) != ".sql" {
			continue
		}
		files = append(files, filepath.Join(dir, entry.Name()))
	}
	sort.Strings(files)
	return files, nil
}

func isApplied(ctx context.Context, db *sql.DB, version string, file string, options Options) (bool, error) {
	var row migrationRow
	err := db.QueryRowContext(ctx, "SELECT version, checksum, dirty FROM schema_migrations WHERE version = ?", version).Scan(&row.Version, &row.Checksum, &row.Dirty)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	if row.Dirty {
		return false, fmt.Errorf("migration %s is dirty; repair it before continuing", version)
	}
	body, err := os.ReadFile(file)
	if err != nil {
		return false, err
	}
	sum := checksum(body)
	if row.Checksum != "" && row.Checksum != sum {
		if options.AllowChecksumRepair {
			// 本地开发阶段允许直接编辑历史迁移；修复只更新记录，不重放历史 SQL，避免重复 ALTER 破坏现有库。
			if _, err := db.ExecContext(ctx, "UPDATE schema_migrations SET checksum = ? WHERE version = ?", sum, version); err != nil {
				return false, err
			}
			return true, nil
		}
		return false, fmt.Errorf("migration %s checksum changed", version)
	}
	if row.Checksum == "" {
		if _, err := db.ExecContext(ctx, "UPDATE schema_migrations SET checksum = ? WHERE version = ?", sum, version); err != nil {
			return false, err
		}
	}
	return true, nil
}

func applyFile(ctx context.Context, db *sql.DB, version string, file string) error {
	body, err := os.ReadFile(file)
	if err != nil {
		return err
	}
	sum := checksum(body)
	// 迁移文件会用 MySQL 会话变量在多条语句之间传递 DDL 决策。database/sql 不保证连续 Exec
	// 复用同一底层连接，因此整个文件必须绑定 *sql.Conn，否则 @ddl/@checkpoint 可能在 PREPARE 前丢失。
	conn, err := db.Conn(ctx)
	if err != nil {
		return err
	}
	defer conn.Close()

	var existing migrationRow
	err = conn.QueryRowContext(ctx, "SELECT version, checksum, dirty FROM schema_migrations WHERE version = ?", version).Scan(&existing.Version, &existing.Checksum, &existing.Dirty)
	if err == nil {
		if existing.Dirty {
			return fmt.Errorf("migration %s is dirty; repair it before continuing", version)
		}
		if existing.Checksum != "" && existing.Checksum != sum {
			return fmt.Errorf("migration %s checksum changed", version)
		}
		return nil
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return err
	}

	if _, err := conn.ExecContext(ctx, "INSERT INTO schema_migrations(version, checksum, dirty) VALUES (?, ?, TRUE)", version, sum); err != nil {
		return err
	}

	for _, statement := range splitSQL(string(body)) {
		if _, err := conn.ExecContext(ctx, statement); err != nil {
			return fmt.Errorf("apply migration %s: %w", version, err)
		}
	}
	if _, err := conn.ExecContext(ctx, "UPDATE schema_migrations SET dirty = FALSE, checksum = ?, applied_at_ms = ? WHERE version = ?", sum, time.Now().UTC().UnixMilli(), version); err != nil {
		return err
	}
	return nil
}

func splitSQL(raw string) []string {
	statements := make([]string, 0)
	var b strings.Builder
	var quote rune
	var inLineComment bool
	var inBlockComment bool
	escaped := false
	for i, r := range raw {
		next := rune(0)
		if i+1 < len(raw) {
			next = rune(raw[i+1])
		}
		if inLineComment {
			b.WriteRune(r)
			if r == '\n' {
				inLineComment = false
			}
			continue
		}
		if inBlockComment {
			b.WriteRune(r)
			if r == '*' && next == '/' {
				inBlockComment = false
			}
			continue
		}
		if quote == 0 && r == '-' && next == '-' {
			inLineComment = true
			b.WriteRune(r)
			continue
		}
		if quote == 0 && r == '/' && next == '*' {
			inBlockComment = true
			b.WriteRune(r)
			continue
		}
		if quote != 0 {
			b.WriteRune(r)
			if escaped {
				escaped = false
				continue
			}
			if r == '\\' {
				escaped = true
				continue
			}
			if r == quote {
				quote = 0
			}
			continue
		}
		switch r {
		case '\'', '"', '`':
			quote = r
			b.WriteRune(r)
		case ';':
			appendStatement(&statements, b.String())
			b.Reset()
		default:
			b.WriteRune(r)
		}
	}
	appendStatement(&statements, b.String())
	return statements
}

type migrationRow struct {
	Version  string
	Checksum string
	Dirty    bool
}

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

func appendStatement(statements *[]string, raw string) {
	statement := strings.TrimSpace(raw)
	if statement == "" {
		return
	}
	*statements = append(*statements, statement)
}
