package main

import (
	"bufio"
	"flag"
	"fmt"
	"log"
	"log/slog"
	"os"
	"regexp"
	"sort"
	"strings"

	"git.mchus.pro/mchus/quoteforge/internal/appstate"
	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/models"
	"github.com/google/uuid"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

type configRow struct {
	ID            uint
	UUID          string
	OwnerUsername string
	Name          string
	ProjectUUID   *string
}

type migrationAction struct {
	ConfigID          uint
	ConfigUUID        string
	ConfigName        string
	OwnerUsername     string
	TargetProjectName string
	CurrentProject    string
	NeedCreateProject bool
	NeedReactivate    bool
}

func main() {
	defaultLocalDBPath, err := appstate.ResolveDBPath("")
	if err != nil {
		log.Fatalf("failed to resolve default local SQLite path: %v", err)
	}
	localDBPath := flag.String("localdb", defaultLocalDBPath, "path to local SQLite database (default: user state dir or QFS_DB_PATH)")
	apply := flag.Bool("apply", false, "apply migration (default is preview only)")
	yes := flag.Bool("yes", false, "skip interactive confirmation (works only with -apply)")
	flag.Parse()

	local, err := localdb.New(*localDBPath)
	if err != nil {
		log.Fatalf("failed to initialize local database: %v", err)
	}
	if !local.HasSettings() {
		log.Fatalf("SQLite connection settings are not configured. Run qfs setup first.")
	}
	dsn, err := local.GetDSN()
	if err != nil {
		log.Fatalf("failed to build DSN from SQLite settings: %v", err)
	}
	dbUser := strings.TrimSpace(local.GetDBUser())

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		log.Fatalf("failed to connect database: %v", err)
	}

	if err := ensureProjectsTable(db); err != nil {
		log.Fatalf("precheck failed: %v", err)
	}

	actions, existingProjects, err := buildPlan(db, dbUser)
	if err != nil {
		log.Fatalf("failed to build migration plan: %v", err)
	}

	printPlan(actions)
	if len(actions) == 0 {
		slog.Info("Nothing to migrate.")
		return
	}

	if !*apply {
		slog.Info("Preview complete. Re-run with -apply to execute.")
		return
	}

	if !*yes {
		ok, confirmErr := askForConfirmation()
		if confirmErr != nil {
			log.Fatalf("confirmation failed: %v", confirmErr)
		}
		if !ok {
			slog.Info("Aborted.")
			return
		}
	}

	if err := executePlan(db, actions, existingProjects); err != nil {
		log.Fatalf("migration failed: %v", err)
	}

	slog.Info("Migration completed successfully.")
}

func ensureProjectsTable(db *gorm.DB) error {
	var count int64
	if err := db.Raw("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'qt_projects'").Scan(&count).Error; err != nil {
		return fmt.Errorf("checking qt_projects table: %w", err)
	}
	if count == 0 {
		return fmt.Errorf("table qt_projects does not exist; run migration 009_add_projects.sql first")
	}
	return nil
}

func buildPlan(db *gorm.DB, fallbackOwner string) ([]migrationAction, map[string]*models.Project, error) {
	var configs []configRow
	if err := db.Table("qt_configurations").
		Select("id, uuid, owner_username, name, project_uuid").
		Find(&configs).Error; err != nil {
		return nil, nil, fmt.Errorf("load configurations: %w", err)
	}

	codeRegex := regexp.MustCompile(`^(OPS-[0-9]{4})`)
	owners := make(map[string]struct{})
	projectNames := make(map[string]struct{})
	type candidate struct {
		config configRow
		code   string
		owner  string
	}
	candidates := make([]candidate, 0)

	for _, cfg := range configs {
		match := codeRegex.FindStringSubmatch(strings.TrimSpace(cfg.Name))
		if len(match) < 2 {
			continue
		}
		owner := strings.TrimSpace(cfg.OwnerUsername)
		if owner == "" {
			owner = strings.TrimSpace(fallbackOwner)
		}
		if owner == "" {
			continue
		}
		code := match[1]
		owners[owner] = struct{}{}
		projectNames[code] = struct{}{}
		candidates = append(candidates, candidate{config: cfg, code: code, owner: owner})
	}

	ownerList := setKeys(owners)
	nameList := setKeys(projectNames)
	existingProjects := make(map[string]*models.Project)
	if len(ownerList) > 0 && len(nameList) > 0 {
		var projects []models.Project
		if err := db.Where("owner_username IN ? AND name IN ?", ownerList, nameList).Find(&projects).Error; err != nil {
			return nil, nil, fmt.Errorf("load existing projects: %w", err)
		}
		for i := range projects {
			p := projects[i]
			existingProjects[projectKey(p.OwnerUsername, derefString(p.Name))] = &p
		}
	}

	actions := make([]migrationAction, 0)
	for _, c := range candidates {
		key := projectKey(c.owner, c.code)
		existing := existingProjects[key]

		currentProject := ""
		if c.config.ProjectUUID != nil {
			currentProject = *c.config.ProjectUUID
		}

		if existing != nil && currentProject == existing.UUID {
			continue
		}

		action := migrationAction{
			ConfigID:          c.config.ID,
			ConfigUUID:        c.config.UUID,
			ConfigName:        c.config.Name,
			OwnerUsername:     c.owner,
			TargetProjectName: c.code,
			CurrentProject:    currentProject,
		}
		if existing == nil {
			action.NeedCreateProject = true
		} else if !existing.IsActive {
			action.NeedReactivate = true
		}
		actions = append(actions, action)
	}

	return actions, existingProjects, nil
}

func printPlan(actions []migrationAction) {
	createCount := 0
	reactivateCount := 0
	for _, a := range actions {
		if a.NeedCreateProject {
			createCount++
		}
		if a.NeedReactivate {
			reactivateCount++
		}
	}

	slog.Info("Plan summary", "actions", len(actions), "create", createCount, "reactivate", reactivateCount)
	slog.Info("Details:")

	for _, a := range actions {
		extra := ""
		if a.NeedCreateProject {
			extra = " [create project]"
		} else if a.NeedReactivate {
			extra = " [reactivate project]"
		}
		current := a.CurrentProject
		if current == "" {
			current = "NULL"
		}
		fmt.Printf("- %s | owner=%s | \"%s\" | project: %s -> %s%s\n",
			a.ConfigUUID, a.OwnerUsername, a.ConfigName, current, a.TargetProjectName, extra)
	}
}

func askForConfirmation() (bool, error) {
	fmt.Print("\nApply these changes? type 'yes' to continue: ")
	reader := bufio.NewReader(os.Stdin)
	line, err := reader.ReadString('\n')
	if err != nil {
		return false, err
	}
	return strings.EqualFold(strings.TrimSpace(line), "yes"), nil
}

func executePlan(db *gorm.DB, actions []migrationAction, existingProjects map[string]*models.Project) error {
	return db.Transaction(func(tx *gorm.DB) error {
		projectCache := make(map[string]*models.Project, len(existingProjects))
		for k, v := range existingProjects {
			cp := *v
			projectCache[k] = &cp
		}

		for _, action := range actions {
			key := projectKey(action.OwnerUsername, action.TargetProjectName)
		project := projectCache[key]
			if project == nil {
				project = &models.Project{
					UUID:          uuid.NewString(),
					OwnerUsername: action.OwnerUsername,
					Code:          action.TargetProjectName,
					Name:          ptrString(action.TargetProjectName),
					IsActive:      true,
					IsSystem:      false,
				}
				if err := tx.Create(project).Error; err != nil {
					return fmt.Errorf("create project %s for owner %s: %w", action.TargetProjectName, action.OwnerUsername, err)
				}
				projectCache[key] = project
			} else if !project.IsActive {
				if err := tx.Model(&models.Project{}).Where("uuid = ?", project.UUID).Update("is_active", true).Error; err != nil {
					return fmt.Errorf("reactivate project %s (%s): %w", derefString(project.Name), project.UUID, err)
				}
				project.IsActive = true
			}

			if err := tx.Table("qt_configurations").Where("id = ?", action.ConfigID).Update("project_uuid", project.UUID).Error; err != nil {
				return fmt.Errorf("move configuration %s to project %s: %w", action.ConfigUUID, project.UUID, err)
			}
		}

		return nil
	})
}

func setKeys(set map[string]struct{}) []string {
	keys := make([]string, 0, len(set))
	for k := range set {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}

func projectKey(owner, name string) string {
	return owner + "||" + name
}

func derefString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func ptrString(value string) *string {
	return &value
}
