// Package migration owns the application database migration runner. Each data
// module declares its own steps; the root data package only orders and runs
// them.
package migration

import (
	"fmt"
	"strings"

	"gorm.io/gorm"
)

const TableName = "sys_schema_migrations"

type Step struct {
	ID      string
	Migrate func(*gorm.DB) error
}

type record struct {
	ID string `gorm:"primaryKey;size:255"`
}

func (record) TableName() string { return TableName }

func Run(db *gorm.DB, steps []Step) error {
	if db == nil {
		return fmt.Errorf("database is nil")
	}
	db = db.Session(&gorm.Session{NewDB: true})
	if err := repairMigrationTable(db); err != nil {
		return err
	}
	if !db.Session(&gorm.Session{NewDB: true}).Migrator().HasTable(&record{}) {
		if err := db.Session(&gorm.Session{NewDB: true}).Migrator().CreateTable(&record{}); err != nil {
			return fmt.Errorf("create migration table: %w", err)
		}
	}
	var appliedRows []record
	if err := db.Session(&gorm.Session{NewDB: true}).Find(&appliedRows).Error; err != nil {
		return fmt.Errorf("load applied migrations: %w", err)
	}
	applied := make(map[string]struct{}, len(appliedRows))
	known := make(map[string]struct{}, len(steps))
	for _, row := range appliedRows {
		applied[row.ID] = struct{}{}
	}
	for _, step := range steps {
		if step.ID == "" {
			return fmt.Errorf("migration id is empty")
		}
		if _, duplicate := known[step.ID]; duplicate {
			return fmt.Errorf("duplicate migration id %q", step.ID)
		}
		known[step.ID] = struct{}{}
	}
	for id := range applied {
		if _, ok := known[id]; !ok {
			return fmt.Errorf("unknown applied migration %q", id)
		}
	}
	for _, step := range steps {
		if _, ok := applied[step.ID]; ok {
			continue
		}
		if step.Migrate == nil {
			return fmt.Errorf("migration %q has no function", step.ID)
		}
		if err := step.Migrate(db.Session(&gorm.Session{NewDB: true})); err != nil {
			return fmt.Errorf("apply migration %q: %w", step.ID, err)
		}
		if err := db.Session(&gorm.Session{NewDB: true}).Create(&record{ID: step.ID}).Error; err != nil {
			return fmt.Errorf("record migration %q: %w", step.ID, err)
		}
	}
	return nil
}

func repairMigrationTable(db *gorm.DB) error {
	clean := db.Session(&gorm.Session{NewDB: true})
	if !clean.Migrator().HasTable(TableName) {
		return nil
	}
	var dataType string
	switch clean.Dialector.Name() {
	case "mysql":
		err := clean.Raw("SELECT DATA_TYPE FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?", TableName, "id").Scan(&dataType).Error
		if err != nil {
			return fmt.Errorf("inspect migration table: %w", err)
		}
	case "postgres":
		err := clean.Raw("SELECT data_type FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ? AND column_name = ?", TableName, "id").Scan(&dataType).Error
		if err != nil {
			return fmt.Errorf("inspect migration table: %w", err)
		}
	case "sqlite":
		var rows []struct {
			Name string `gorm:"column:name"`
			Type string `gorm:"column:type"`
		}
		if err := clean.Raw("PRAGMA table_info(" + TableName + ")").Scan(&rows).Error; err != nil {
			return fmt.Errorf("inspect migration table: %w", err)
		}
		for _, row := range rows {
			if strings.EqualFold(row.Name, "id") {
				dataType = row.Type
				break
			}
		}
	default:
		return nil
	}
	normalized := strings.ToLower(dataType)
	if strings.Contains(normalized, "char") || strings.Contains(normalized, "text") || strings.Contains(normalized, "clob") {
		return nil
	}
	if err := clean.Migrator().DropTable(TableName); err != nil {
		return fmt.Errorf("repair invalid migration table: %w", err)
	}
	return nil
}
