package migrate

import (
	"fmt"
	"sort"

	"github.com/go-gormigrate/gormigrate/v2"

	"gorm.io/gorm"
)

type MigratorOption func(*Migrator)

func WithTable(table string) MigratorOption {
	return func(m *Migrator) {
		m.table = table
	}
}

func WithMigratorTable(table string) MigratorOption {
	return WithTable(table)
}

func WithInitDBFunc(f func(*gorm.DB) error) MigratorOption {
	return func(m *Migrator) {
		m.initDB = f
	}
}

type migration struct {
	migrate func(*gorm.DB) error
	version string
}

type Migrator struct {
	table   string
	records []*migration
	initDB  func(*gorm.DB) error
}

func NewMigrator(opts ...MigratorOption) *Migrator {
	m := &Migrator{
		table:   "migrations",
		records: make([]*migration, 0),
		initDB:  func(*gorm.DB) error { return nil },
	}

	for _, opt := range opts {
		opt(m)
	}

	return m
}

func (m *Migrator) Run(db *gorm.DB) error {
	// we don't depend on foreign key constraint, so just disable it.
	db.DisableForeignKeyConstraintWhenMigrating = true

	m.sortRecords()
	return m.migrate(db, m.records)
}

// AddMigrateRecord add a migration functions to migration queue.
func (m *Migrator) Add(version string, migrate func(*gorm.DB) error) bool {
	m.checkVersion(version)
	m.records = append(m.records, &migration{
		version: version,
		migrate: migrate,
	})
	return true
}

func (m *Migrator) checkVersion(version string) {
	for _, item := range m.records {
		if item.version == version {
			panic(fmt.Sprintf("migration version is already added: %s", version))
		}
	}
}

func (m *Migrator) sortRecords() {
	sort.Sort(byVersion(m.records))
}

// TODO: backup and restore database
func (m *Migrator) migrate(db *gorm.DB, migrations []*migration) error {
	var gormMigrations []*gormigrate.Migration
	for _, item := range migrations {
		gormMigrations = append(gormMigrations, &gormigrate.Migration{
			ID:      item.version,
			Migrate: item.migrate,
		})
	}
	options := &gormigrate.Options{
		TableName:                 m.table,
		IDColumnName:              "id",
		IDColumnSize:              255,
		UseTransaction:            false,
		ValidateUnknownMigrations: false,
	}
	goMigration := gormigrate.New(db, options, gormMigrations)
	goMigration.InitSchema(m.initDB)
	return goMigration.Migrate()
}

// byVersion implements sort.Interface based on the version field.
type byVersion []*migration

func (a byVersion) Len() int           { return len(a) }
func (a byVersion) Less(i, j int) bool { return a[i].version < a[j].version }
func (a byVersion) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
