package models

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	mysqlDriver "github.com/go-sql-driver/mysql"
	"gorm.io/gorm"
)

type SQLSchemaMigration struct {
	ID        uint      `gorm:"primaryKey;autoIncrement"`
	Filename  string    `gorm:"size:255;uniqueIndex;not null"`
	AppliedAt time.Time `gorm:"autoCreateTime"`
}

func (SQLSchemaMigration) TableName() string {
	return "qt_schema_migrations"
}

// NeedsSQLMigrations reports whether at least one SQL migration from migrationsDir
// is not yet recorded in qt_schema_migrations.
func NeedsSQLMigrations(db *gorm.DB, migrationsDir string) (bool, error) {
	files, err := listSQLMigrationFiles(migrationsDir)
	if err != nil {
		return false, err
	}
	if len(files) == 0 {
		return false, nil
	}

	// If tracking table does not exist yet, migrations are required.
	if !db.Migrator().HasTable(&SQLSchemaMigration{}) {
		return true, nil
	}

	var count int64
	if err := db.Model(&SQLSchemaMigration{}).Where("filename IN ?", files).Count(&count).Error; err != nil {
		return false, fmt.Errorf("check applied migrations: %w", err)
	}

	return count < int64(len(files)), nil
}

// RunSQLMigrations applies SQL files from migrationsDir once and records them in qt_schema_migrations.
// Local SQLite-only scripts are skipped automatically.
func RunSQLMigrations(db *gorm.DB, migrationsDir string) error {
	if err := ensureSQLMigrationsTable(db); err != nil {
		return fmt.Errorf("migrate qt_schema_migrations table: %w", err)
	}

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

	for _, filename := range files {
		var count int64
		if err := db.Model(&SQLSchemaMigration{}).Where("filename = ?", filename).Count(&count).Error; err != nil {
			return fmt.Errorf("check migration %s: %w", filename, err)
		}
		if count > 0 {
			continue
		}

		path := filepath.Join(migrationsDir, filename)
		content, err := os.ReadFile(path)
		if err != nil {
			return fmt.Errorf("read migration %s: %w", filename, err)
		}

		statements := splitSQLStatements(string(content))
		if len(statements) == 0 {
			if err := db.Create(&SQLSchemaMigration{Filename: filename}).Error; err != nil {
				return fmt.Errorf("record empty migration %s: %w", filename, err)
			}
			continue
		}

		if err := executeMigrationStatements(db, filename, statements); err != nil {
			return err
		}
		if err := db.Create(&SQLSchemaMigration{Filename: filename}).Error; err != nil {
			return fmt.Errorf("record migration %s: %w", filename, err)
		}
	}

	return nil
}

// IsMigrationPermissionError returns true if err indicates insufficient privileges
// to create/alter/read migration metadata or target schema objects.
func IsMigrationPermissionError(err error) bool {
	if err == nil {
		return false
	}

	var mysqlErr *mysqlDriver.MySQLError
	if errors.As(err, &mysqlErr) {
		switch mysqlErr.Number {
		case 1044, 1045, 1142, 1143, 1227:
			return true
		}
	}

	lower := strings.ToLower(err.Error())
	patterns := []string{
		"command denied to user",
		"access denied for user",
		"permission denied",
		"insufficient privilege",
		"sqlstate 42000",
	}
	for _, pattern := range patterns {
		if strings.Contains(lower, pattern) {
			return true
		}
	}
	return false
}

func ensureSQLMigrationsTable(db *gorm.DB) error {
	stmt := `
CREATE TABLE IF NOT EXISTS qt_schema_migrations (
	id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
	filename VARCHAR(255) NOT NULL UNIQUE,
	applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);`
	return db.Exec(stmt).Error
}

func executeMigrationStatements(db *gorm.DB, filename string, statements []string) error {
	for _, stmt := range statements {
		if err := db.Exec(stmt).Error; err != nil {
			if isIgnorableMigrationError(err.Error()) {
				continue
			}
			return fmt.Errorf("exec migration %s statement %q: %w", filename, stmt, err)
		}
	}
	return nil
}

func isSQLiteOnlyMigration(filename string) bool {
	lower := strings.ToLower(filename)
	return strings.Contains(lower, "local_")
}

func isIgnorableMigrationError(message string) bool {
	lower := strings.ToLower(message)
	ignorable := []string{
		"duplicate column name",
		"duplicate key name",
		"already exists",
		"can't create table",
		"duplicate foreign key constraint name",
		"errno 121",
	}
	for _, pattern := range ignorable {
		if strings.Contains(lower, pattern) {
			return true
		}
	}
	return false
}

func splitSQLStatements(script string) []string {
	scanner := bufio.NewScanner(strings.NewReader(script))
	scanner.Buffer(make([]byte, 1024), 1024*1024)

	lines := make([]string, 0, 128)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		if strings.HasPrefix(line, "--") {
			continue
		}
		lines = append(lines, scanner.Text())
	}

	combined := strings.Join(lines, "\n")
	raw := strings.Split(combined, ";")
	stmts := make([]string, 0, len(raw))
	for _, stmt := range raw {
		trimmed := strings.TrimSpace(stmt)
		if trimmed == "" {
			continue
		}
		stmts = append(stmts, trimmed)
	}
	return stmts
}

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

	files := make([]string, 0, len(entries))
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		name := entry.Name()
		if !strings.HasSuffix(strings.ToLower(name), ".sql") {
			continue
		}
		if isSQLiteOnlyMigration(name) {
			continue
		}
		files = append(files, name)
	}
	sort.Strings(files)
	return files, nil
}
