package migrate

import (
	"context"
	"fmt"
	"log"
	"strings"

	"github.com/tidbcloud/controlplane9/internal/config"
)

// Options configures a migration run.
type Options struct {
	// Reset drops all tables and re-applies schema + seed.
	Reset bool
	// Fresh forces re-dump even if cache is fresh.
	Fresh bool
	// SeedFlag is the --seed flag value ("", "true", "config-tables", "full").
	SeedFlag string
	// Services is the list of REAL services to migrate.
	Services []string
	// StagingDSNs maps service name → staging database DSN.
	StagingDSNs map[string]string
	// LocalDSNs maps service name → local database DSN.
	LocalDSNs map[string]string
	// AdminDSN is the local DB admin DSN (no database selected) for DROP/CREATE.
	AdminDSN string
	// SeedConfig from cp9.yaml database.seed section.
	SeedConfig config.SeedConfig
	// StateDir is the cp9 state directory for caching.
	StateDir string
	// Dumper performs the actual mysqldump. If nil, a real dumper is used.
	Dumper *Dumper
}

// Run dumps schema from staging and applies it to the local database for each service.
func Run(ctx context.Context, opts *Options) *Result {
	result := &Result{Reset: opts.Reset}

	for _, svc := range opts.Services {
		stagingDSN, ok := opts.StagingDSNs[svc]
		if !ok {
			log.Printf("migrate: skipping %s — no staging DSN", svc)
			result.Services = append(result.Services, ServiceResult{
				Service: svc,
				Status:  "skipped",
			})
			continue
		}

		localDSN, ok := opts.LocalDSNs[svc]
		if !ok {
			log.Printf("migrate: skipping %s — no local DSN", svc)
			result.Services = append(result.Services, ServiceResult{
				Service: svc,
				Status:  "skipped",
			})
			continue
		}

		svcResult := runService(ctx, svc, stagingDSN, localDSN, opts)
		result.Services = append(result.Services, svcResult)

		if svcResult.Status == "failed" {
			return result // stop on first failure
		}
	}

	return result
}

func runService(ctx context.Context, svc, stagingDSN, localDSN string, opts *Options) ServiceResult {
	// Reset: drop and recreate the database first.
	if opts.Reset {
		if err := ResetDatabase(ctx, opts.AdminDSN, svc); err != nil {
			return ServiceResult{Service: svc, Status: "failed", Error: err.Error()}
		}
	}

	// Resolve the dumper: use provided Dumper or build from staging DSN.
	dumper := opts.Dumper
	if dumper == nil {
		host, port, user, pass, err := ParseDSN(stagingDSN)
		if err != nil {
			return ServiceResult{Service: svc, Status: "failed", Error: fmt.Sprintf("parse staging DSN: %v", err)}
		}
		dumper = &Dumper{
			StagingHost: host,
			StagingPort: port,
			StagingUser: user,
			StagingPass: pass,
		}
	}

	// Extract database name from staging DSN (may differ from service name).
	stagingDB := extractDBName(stagingDSN, svc)

	// Check schema cache.
	schemaPath := CachedDumpPath(opts.StateDir, svc)
	usedCache := false

	var schemaDump []byte
	if !opts.Fresh && !opts.Reset && IsCacheFresh(schemaPath) {
		cached, err := ReadCacheFile(schemaPath)
		if err == nil {
			schemaDump = cached
			usedCache = true
			log.Printf("migrate: using cached schema dump for %s", svc)
		}
	}

	if schemaDump == nil {
		var err error
		schemaDump, err = dumper.DumpSchema(ctx, stagingDB)
		if err != nil {
			return ServiceResult{Service: svc, Status: "failed", Error: err.Error()}
		}

		// Cache the dump.
		if writeErr := WriteCacheFile(schemaPath, schemaDump); writeErr != nil {
			log.Printf("migrate: warning: failed to cache schema dump for %s: %v", svc, writeErr)
		}
	}

	// Apply schema to local DB.
	if err := ApplyDump(ctx, localDSN, schemaDump); err != nil {
		return ServiceResult{Service: svc, Status: "failed", Error: fmt.Sprintf("apply schema: %v", err)}
	}

	// Handle seed data.
	seedMode := ResolveSeedMode(opts.SeedFlag, opts.SeedConfig, svc)
	if seedMode != config.SeedSchemaOnly {
		if err := runSeed(ctx, svc, stagingDB, localDSN, seedMode, dumper, opts); err != nil {
			return ServiceResult{Service: svc, Status: "failed", Error: fmt.Sprintf("seed: %v", err)}
		}
	}

	status := "ok"
	if usedCache {
		status = "cached"
	}
	return ServiceResult{Service: svc, Status: status}
}

func runSeed(ctx context.Context, svc, stagingDB, localDSN string, mode config.SeedMode, dumper *Dumper, opts *Options) error {
	seedPath := CachedSeedPath(opts.StateDir, svc)

	// Check seed cache.
	if !opts.Fresh && IsCacheFresh(seedPath) {
		cached, err := ReadCacheFile(seedPath)
		if err == nil {
			log.Printf("migrate: using cached seed data for %s", svc)
			return ApplyDump(ctx, localDSN, cached)
		}
	}

	var seedDump []byte
	var err error

	switch mode {
	case config.SeedConfigTables:
		tables := SeedTables(opts.SeedConfig, svc)
		if len(tables) == 0 {
			log.Printf("migrate: no seed tables configured for %s, skipping seed", svc)
			return nil
		}
		seedDump, err = dumper.DumpTables(ctx, stagingDB, tables)
	case config.SeedFull:
		seedDump, err = dumper.DumpFullData(ctx, stagingDB)
	default:
		return nil
	}

	if err != nil {
		return err
	}

	// Cache seed dump.
	if writeErr := WriteCacheFile(seedPath, seedDump); writeErr != nil {
		log.Printf("migrate: warning: failed to cache seed dump for %s: %v", svc, writeErr)
	}

	return ApplyDump(ctx, localDSN, seedDump)
}

// extractDBName parses the database name from a DSN string.
// Falls back to the service name if parsing fails.
func extractDBName(dsn, fallback string) string {
	_, _, dbName := splitDSN(dsn)
	if dbName == "" {
		return fallback
	}
	return dbName
}

// splitDSN extracts the user-info, host, and database portions of a MySQL DSN.
func splitDSN(dsn string) (userHost, host, dbName string) {
	// Strip mysql:// prefix if present.
	dsn = strings.TrimPrefix(dsn, "mysql://")

	// Find the last / which separates host from database.
	slashIdx := strings.LastIndex(dsn, "/")
	if slashIdx < 0 {
		return dsn, dsn, ""
	}

	dbPart := dsn[slashIdx+1:]
	// Remove query parameters.
	if qIdx := strings.Index(dbPart, "?"); qIdx >= 0 {
		dbPart = dbPart[:qIdx]
	}
	return dsn[:slashIdx], dsn[:slashIdx], dbPart
}
