package deploy

import (
	"context"
	"fmt"
	"path"
	"reflect"
	"runtime"
	"sort"
	"strings"
	"time"

	"github.com/avast/retry-go/v4"
	"github.com/go-gormigrate/gormigrate/v2"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/pkg/clusterconfig"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	templateversion "github.com/tidbcloud/cluster-service-ng/pkg/template/version"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type migrator struct {
	sharedPoolRepo   repo.SharedPoolRepo
	tidbGroupRepo    repo.TiDBGroupRepo
	tidbClusterRepo  repo.TiDBClusterRepo
	dataplaneClient  dataplanemgr.Client
	dataplaneManager dataplane.Manager
	clusterConfig    clusterconfig.Client
	csngConfig       *config.ServerConfig
	templateVersion  *templateversion.Service
	kubeClient       client.Client
}

func NewMigrator(
	sharedPoolRepo repo.SharedPoolRepo,
	tidbGroupRepo repo.TiDBGroupRepo,
	tidbClusterRepo repo.TiDBClusterRepo,
	dataplaneClient dataplanemgr.Client,
	dataplaneManager dataplane.Manager,
	clusterConfig clusterconfig.Client,
	csngConfig *config.ServerConfig,
	templateVersionSvc *templateversion.Service,
	kubeClient client.Client,

) *migrator {
	return &migrator{
		sharedPoolRepo:   sharedPoolRepo,
		tidbGroupRepo:    tidbGroupRepo,
		tidbClusterRepo:  tidbClusterRepo,
		dataplaneClient:  dataplaneClient,
		dataplaneManager: dataplaneManager,
		clusterConfig:    clusterConfig,
		csngConfig:       csngConfig,
		templateVersion:  templateVersionSvc,
		kubeClient:       kubeClient,
	}
}

type migrationRecord struct {
	version string
	v       reflect.Value
}

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

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) Migrate(ctx context.Context, db *gorm.DB) error {
	sort.Sort(byVersion(deployMigrationRecords))
	var gormMigrations []*gormigrate.Migration
	for _, item := range deployMigrationRecords {
		results := item.v.Call([]reflect.Value{reflect.ValueOf(m), reflect.ValueOf(ctx)})
		if len(results) != 1 {
			return fmt.Errorf("invalid output of migration method %s", item.version)
		}

		if results[0].Type() != reflect.TypeOf((*gormigrate.MigrateFunc)(nil)).Elem() {
			return fmt.Errorf("result of migration method %s must implement migrate function", item.version)
		}

		gormMigrations = append(gormMigrations, &gormigrate.Migration{
			ID:      item.version,
			Migrate: results[0].Interface().(gormigrate.MigrateFunc),
		})
	}

	options := &gormigrate.Options{
		TableName:                 "cluster_next_gen_deploy_migrations",
		IDColumnName:              "id",
		IDColumnSize:              255,
		UseTransaction:            false,
		ValidateUnknownMigrations: false,
	}
	goMigration := gormigrate.New(db, options, gormMigrations)
	return goMigration.Migrate()
}

var deployMigrationRecords []*migrationRecord

func registerMigrateRecord(v any) {
	fnType := reflect.TypeOf(v)

	if fnType.Kind() != reflect.Func {
		panic("registerMigrateRecord is not a function")
	}
	migrateFuncType := reflect.TypeOf((*gormigrate.MigrateFunc)(nil)).Elem()

	if fnType.NumOut() != 1 {
		panic("invalid output of migration method")
	}

	if fnType.Out(0) != migrateFuncType {
		panic("result of migration method must implement migrate function")
	}

	fnVal := reflect.ValueOf(v)
	fnPtr := fnVal.Pointer()
	fn := runtime.FuncForPC(fnPtr)
	if fn == nil {
		panic("RegisterMethod: cannot get method name")
	}

	fullName := fn.Name()
	_, methodName := path.Split(fullName)

	idx := strings.LastIndex(methodName, ".")
	if idx >= 0 {
		methodName = methodName[idx+1:]
	}

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

	deployMigrationRecords = append(deployMigrationRecords, &migrationRecord{
		version: methodName,
		v:       fnVal,
	})
}

func InvokeRunMigrateWithRetry(ctx context.Context, run func() error) error {
	return retry.Do(func() error {
		return run()
	},
		retry.Context(ctx),
		retry.DelayType(retry.FixedDelay),
		retry.Delay(time.Second*3),
		retry.Attempts(0),
		retry.OnRetry(func(n uint, err error) {
			log.Info(ctx, "migration run error", zap.Uint("retry_times", n), zap.Error(err))
		}),
		retry.LastErrorOnly(true),
		retry.RetryIf(func(err error) bool {
			return true
		}),
	)
}
