package models

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"gorm.io/gorm"
)

// SenlinAgentSchemaMigration 记录已完成的数据修复及审计摘要，确保升级可重入且可追溯。
type SenlinAgentSchemaMigration struct {
	Version   int       `gorm:"primaryKey;autoIncrement:false"`
	Name      string    `gorm:"not null"`
	Details   string    `gorm:"type:text;not null"`
	AppliedAt time.Time `gorm:"not null"`
}

func (SenlinAgentSchemaMigration) TableName() string {
	return "senlin_agent_schema_migrations"
}

type versionedMigration struct {
	version int
	name    string
	run     func(*gorm.DB) (map[string]int, error)
}

func runVersionedMigrations(database *gorm.DB) error {
	migrations := []versionedMigration{
		{version: 1, name: "normalize_project_identifiers", run: normalizeLegacyProjectIdentifiers},
		{version: 2, name: "deduplicate_project_tags", run: deduplicateLegacyProjectTags},
	}
	for _, migration := range migrations {
		// Connection callbacks can provide an initialized Gorm session. Start each
		// migration from a clean statement so Model/Where clauses used to check the
		// version table are not inherited by the data-repair transaction.
		migrationDatabase := database.Session(&gorm.Session{NewDB: true})
		var applied int64
		if err := migrationDatabase.Model(&SenlinAgentSchemaMigration{}).Where("version = ?", migration.version).Count(&applied).Error; err != nil {
			return err
		}
		if applied > 0 {
			continue
		}
		if err := migrationDatabase.Session(&gorm.Session{NewDB: true}).Transaction(func(tx *gorm.DB) error {
			details, err := migration.run(tx)
			if err != nil {
				return err
			}
			encoded, err := json.Marshal(details)
			if err != nil {
				return err
			}
			return tx.Create(&SenlinAgentSchemaMigration{
				Version: migration.version, Name: migration.name, Details: string(encoded), AppliedAt: time.Now().UTC(),
			}).Error
		}); err != nil {
			return fmt.Errorf("schema migration %d (%s): %w", migration.version, migration.name, err)
		}
	}
	return nil
}

func normalizeLegacyProjectIdentifiers(tx *gorm.DB) (map[string]int, error) {
	var projects []SenlinAgentProject
	if err := tx.Model(&SenlinAgentProject{}).Select("id", "owner_id", "identifier").Order("owner_id asc, id asc").Find(&projects).Error; err != nil {
		return nil, err
	}

	keepers := make(map[uint]map[string]uint)
	reserved := make(map[uint]map[string]struct{})
	for _, project := range projects {
		identifier := strings.TrimSpace(project.Identifier)
		if identifier == "" {
			continue
		}
		if keepers[project.OwnerID] == nil {
			keepers[project.OwnerID] = make(map[string]uint)
			reserved[project.OwnerID] = make(map[string]struct{})
		}
		if _, exists := keepers[project.OwnerID][identifier]; !exists {
			keepers[project.OwnerID][identifier] = project.ID
		}
		reserved[project.OwnerID][identifier] = struct{}{}
	}

	updated := 0
	for _, project := range projects {
		if reserved[project.OwnerID] == nil {
			reserved[project.OwnerID] = make(map[string]struct{})
		}
		identifier := strings.TrimSpace(project.Identifier)
		candidate := identifier
		if identifier == "" {
			candidate = uniqueIdentifier(fmt.Sprintf("project-%d", project.ID), reserved[project.OwnerID])
		} else if keepers[project.OwnerID][identifier] != project.ID {
			candidate = uniqueIdentifier(fmt.Sprintf("%s-%d", identifier, project.ID), reserved[project.OwnerID])
		}
		reserved[project.OwnerID][candidate] = struct{}{}
		if candidate == project.Identifier {
			continue
		}
		if err := tx.Model(&SenlinAgentProject{}).Where("id = ?", project.ID).Update("identifier", candidate).Error; err != nil {
			return nil, err
		}
		updated++
	}
	if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_senlin_agent_projects_owner_identifier ON senlin_agent_projects (owner_id, identifier)`).Error; err != nil {
		return nil, err
	}
	return map[string]int{"audited": len(projects), "updated": updated}, nil
}

func uniqueIdentifier(base string, reserved map[string]struct{}) string {
	candidate := base
	for suffix := 2; ; suffix++ {
		if _, exists := reserved[candidate]; !exists {
			return candidate
		}
		candidate = fmt.Sprintf("%s-%d", base, suffix)
	}
}

func deduplicateLegacyProjectTags(tx *gorm.DB) (map[string]int, error) {
	var tags []SenlinAgentTag
	if err := tx.Model(&SenlinAgentTag{}).Select("id", "identity", "project_id", "name").Order("project_id asc, name asc, id asc").Find(&tags).Error; err != nil {
		return nil, err
	}
	keepers := make(map[string]SenlinAgentTag)
	deduplicated := 0
	for _, tag := range tags {
		key := fmt.Sprintf("%d\x00%s", tag.ProjectID, tag.Name)
		keeper, exists := keepers[key]
		if !exists {
			keepers[key] = tag
			continue
		}
		if err := tx.Model(&SenlinAgentTask{}).Where("tag_id = ?", tag.ID).Updates(map[string]any{
			"tag_id": keeper.ID, "tag_identity": keeper.Identity,
		}).Error; err != nil {
			return nil, err
		}
		if err := tx.Where("id = ?", tag.ID).Delete(&SenlinAgentTag{}).Error; err != nil {
			return nil, err
		}
		deduplicated++
	}
	if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_senlin_agent_tags_project_name ON senlin_agent_tags (project_id, name)`).Error; err != nil {
		return nil, err
	}
	return map[string]int{"audited": len(tags), "deduplicated": deduplicated}, nil
}
