package migrate

import (
	"fmt"
	"sort"

	"github.com/go-gormigrate/gormigrate/v2"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"gorm.io/gorm"
)

type Migrator struct {
	db *gorm.DB
}

func NewMigrator(db *gorm.DB) *Migrator {
	return &Migrator{
		db: db,
	}
}

var ecosystemDBMigrationRecords []*migration

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

// 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] }

func (m *Migrator) AddEcosystemMigrateRecord(version int, migrate func(*gorm.DB) error) bool {
	validateVersion(version, ecosystemDBMigrationRecords)
	ecosystemDBMigrationRecords = append(
		ecosystemDBMigrationRecords, &migration{
			version: version,
			migrate: migrate,
		},
	)
	return true
}

func (m *Migrator) RunMigrate() error {
	return m.doMigrate(ecosystemDBMigrationRecords)
}

func (m *Migrator) doMigrate(migrations []*migration) error {
	sort.Sort(byVersion(migrations))
	gormMigrations := make([]*gormigrate.Migration, 0, len(migrations))
	for _, item := range migrations {
		gormMigrations = append(gormMigrations, &gormigrate.Migration{
			ID:      fmt.Sprintf("%d", item.version),
			Migrate: item.migrate,
		})
	}
	options := &gormigrate.Options{
		TableName:                 "ecosystem_migrations",
		IDColumnName:              "id",
		IDColumnSize:              255,
		UseTransaction:            false,
		ValidateUnknownMigrations: false,
	}
	goMigration := gormigrate.New(m.db, options, gormMigrations)

	err := goMigration.Migrate()
	if err != nil {
		log.Info("Start to rollback")
		rollErr := goMigration.RollbackLast()
		if rollErr != nil {
			return errors.Annotatef(err, "failed to rollback cause by %s", rollErr.Error())
		}

		log.Info("Rollback successfully")
		return errors.Trace(err)
	}

	return nil
}

// GormAutoMigrateAndPrint execute gorm auto migrate and print the DDL SQL
func GormAutoMigrateAndPrint(db *gorm.DB, tables []interface{}) error {
	tx := db.Begin()
	var statements []string
	// Currently gorm doesn't support print auto migration SQL, so we use callback to get the SQL.
	_ = tx.Callback().Raw().Register("record_migration", func(tx *gorm.DB) {
		statements = append(statements, tx.Statement.SQL.String())
	})
	for _, table := range tables {
		err := db.Migrator().AutoMigrate(table)
		if err != nil {
			return err
		}
	}
	tx.Rollback()
	_ = tx.Callback().Raw().Remove("record_migration")
	for _, s := range statements {
		fmt.Println(s)
	}

	return nil
}

// Version should be unique and positive
func validateVersion(version int, records []*migration) {
	if version <= 0 {
		panic("version should be positive")
	}

	for _, item := range records {
		if item.version == version {
			panic(fmt.Sprintf("migration version is already added: %d", version))
		}
	}
}
