package migrations

import (
	"fmt"
	"os"

	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"

	"xorm.io/xorm"
	"xorm.io/xorm/names"
)

const minHatDBVersion = 0

type Migration interface {
	Description() string
	Migrate(*xorm.Engine) error
}

type migration struct {
	description string
	migrate     func(*xorm.Engine) error
}

func NewMigration(desc string, fn func(*xorm.Engine) error) Migration {
	return &migration{desc, fn}
}

func (m *migration) Description() string {
	return m.description
}

func (m *migration) Migrate(x *xorm.Engine) error {
	return m.migrate(x)
}

type HatVersion struct {
	ID         int64 `xorm:"pk autoincr"`
	HatVersion int64
}

var migrations = []Migration{}

func GetCurrentHatDBVersion(x *xorm.Engine) (int64, error) {
	if err := x.Sync(new(HatVersion)); err != nil {
		return -1, fmt.Errorf("sync: %v", err)
	}

	currentVersion := &HatVersion{ID: 1}
	has, err := x.Get(currentVersion)
	if err != nil {
		return -1, fmt.Errorf("get: %v", err)
	}
	if !has {
		return -1, nil
	}

	return currentVersion.HatVersion, nil
}

func ExpectedHatVersion() int64 {
	return int64(minHatDBVersion + len(migrations))
}

func EnsureUpToDate(x *xorm.Engine) error {
	currentDB, err := GetCurrentHatDBVersion(x)
	if err != nil {
		return err
	}

	if currentDB < 0 {
		return fmt.Errorf("Database has not been initialized")
	}

	if minHatDBVersion > currentDB {
		return fmt.Errorf("DB version %d (<= %d) is too old for auto-migration. ", currentDB, minHatDBVersion)
	}

	expected := ExpectedHatVersion()

	if currentDB != expected {
		return fmt.Errorf(`Current database version %d is not equal to the expected version %d.`, currentDB, expected)
	}

	return nil
}

func Migrate(x *xorm.Engine) error {
	x.SetMapper(names.GonicMapper{})
	if err := x.Sync(new(HatVersion)); err != nil {
		return fmt.Errorf("sync: %v", err)
	}

	currentVersion := &HatVersion{ID: 1}
	has, err := x.Get(currentVersion)
	if err != nil {
		return fmt.Errorf("get: %v", err)
	} else if !has {
		currentVersion.ID = 0
		currentVersion.HatVersion = int64(minHatDBVersion)

		if _, err = x.InsertOne(currentVersion); err != nil {
			return fmt.Errorf("insert: %v", err)
		}
	}

	v := currentVersion.HatVersion
	if minHatDBVersion > v {
		log.Fatal(`Gitea no longer supports auto-migration from your previously installed version.
		Please try upgrading to a lower version first (suggested v1.6.4), then upgrade to this version.`)
		return nil
	}
	if int(v-minHatDBVersion) > len(migrations) {
		msg := fmt.Sprintf("Your database (migration version: %d) is for a newer Gitea, you can not use the newer database for this old Gitea release (%d).", v, minHatDBVersion+len(migrations))
		msg += "\nGitea will exit to keep your database safe and unchanged. Please use the correct Gitea release, do not change the migration version manually (incorrect manual operation may lose data)."
		if !setting.IsProd {
			msg += fmt.Sprintf("\nIf you are in development and really know what you're doing, you can force changing the migration version by executing: UPDATE version SET version=%d WHERE id=1;", minHatDBVersion+len(migrations))
		}
		_, _ = fmt.Fprintln(os.Stderr, msg)
		log.Fatal(msg)
		return nil
	}

	for i, m := range migrations[v-minHatDBVersion:] {
		log.Info("Migration[%d]: %s", v+int64(i), m.Description())
		x.SetMapper(names.GonicMapper{})
		if err = m.Migrate(x); err != nil {
			return fmt.Errorf("migration[%d]: %s failed: %v", v+int64(i), m.Description(), err)
		}
		currentVersion.HatVersion = v + int64(i) + 1
		if _, err = x.ID(1).Update(currentVersion); err != nil {
			return err
		}
	}

	return nil
}
