package server

import (
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"

	"github.com/tidbcloud/observability/vm-migration/internal/plan"
	"github.com/tidbcloud/observability/vm-migration/internal/schema"
)

var (
	// errInvalidName reports a migration name that is not a DNS-subdomain.
	errInvalidName = errors.New("invalid migration name")

	// errMigrationExists reports <migrations-dir>/<name> is already present.
	errMigrationExists = errors.New("migration already exists")

	// errStagingLeftover reports a leftover <name>.tmp staging directory
	// from an interrupted prepare.
	errStagingLeftover = errors.New("previous prepare did not complete")

	// errInvalidPlan wraps compiled-plan content failures: parse errors,
	// validation errors, a missing tenant inventory, and tenant-name
	// collisions.
	errInvalidPlan = errors.New("invalid compiled plan")
)

// prepareResult reports what one successful prepare committed.
type prepareResult struct {
	Dir      string                    // committed migration directory
	Tenants  int                       // tenant count embedded in the compiled plan
	LoopPlan *schema.MigrationLoopPlan // the plan serialized into Dir
}

// prepareToDir expands a compiled plan into <migrations-dir>/<name>.
//
// The commit is atomic: everything is staged under <migrations-dir>/<name>.tmp
// and renamed into place as the final step. Any failure removes the staging
// directory and leaves the final migration directory absent. The compiled-plan
// snapshot written into the directory is byte-identical to compiledPlan.
//
// A leftover <name>.tmp (e.g. SIGKILL mid-prepare) is NOT auto-cleaned
// because two prepares racing on the same name could otherwise delete each
// other's staging dir. Remove it manually before retrying.
func prepareToDir(migrationsDir, name string, compiledPlan []byte) (prepareResult, error) {
	if !isValidMigrationName(name) {
		return prepareResult{}, fmt.Errorf("%w %q: must be DNS-subdomain (1-63 chars, [a-z0-9-])", errInvalidName, name)
	}
	outDir := filepath.Join(migrationsDir, name)
	if _, err := os.Stat(outDir); err == nil {
		return prepareResult{}, fmt.Errorf("%w: %s; remove it before retrying", errMigrationExists, outDir)
	} else if !os.IsNotExist(err) {
		return prepareResult{}, fmt.Errorf("stat migration directory %s: %w", outDir, err)
	}
	tmpDir := outDir + ".tmp"
	if _, err := os.Stat(tmpDir); err == nil {
		return prepareResult{}, fmt.Errorf("%w; remove %s manually before retrying", errStagingLeftover, tmpDir)
	} else if !os.IsNotExist(err) {
		return prepareResult{}, fmt.Errorf("stat %s: %w", tmpDir, err)
	}

	cfg, _, err := plan.LoadBytes(compiledPlan)
	if err != nil {
		return prepareResult{}, fmt.Errorf("%w: %w", errInvalidPlan, err)
	}
	if len(cfg.Tenants) == 0 {
		return prepareResult{}, fmt.Errorf("%w: no tenants embedded", errInvalidPlan)
	}
	loopPlan, err := plan.BuildMigrationLoop(cfg)
	if err != nil {
		return prepareResult{}, fmt.Errorf("%w: %w", errInvalidPlan, err)
	}

	if err := os.MkdirAll(migrationsDir, 0o755); err != nil {
		return prepareResult{}, fmt.Errorf("mkdir %s: %w", migrationsDir, err)
	}
	// Mkdir, not MkdirAll, for the staging dir: an exclusive create
	// atomically rejects a second concurrent prepare of the same name,
	// which would otherwise share the staging directory and interleave
	// writes.
	if err := os.Mkdir(tmpDir, 0o755); err != nil {
		if os.IsExist(err) {
			// Lost the create race with a concurrent prepare of the same
			// name: the loser sees the winner's staging dir.
			return prepareResult{}, fmt.Errorf("%w; remove %s manually before retrying", errStagingLeftover, tmpDir)
		}
		return prepareResult{}, fmt.Errorf("mkdir %s: %w", tmpDir, err)
	}
	committed := false
	defer func() {
		if !committed {
			cleanupStaging(tmpDir)
		}
	}()

	if err := os.WriteFile(filepath.Join(tmpDir, "compiled-plan.json"), compiledPlan, 0o600); err != nil {
		return prepareResult{}, fmt.Errorf("write compiled-plan.json snapshot: %w", err)
	}
	if err := plan.WritePerTenantFiles(tmpDir, loopPlan); err != nil {
		return prepareResult{}, err
	}
	if _, err := plan.WriteLoopDocument(tmpDir, loopPlan); err != nil {
		return prepareResult{}, err
	}
	if err := os.Rename(tmpDir, outDir); err != nil {
		return prepareResult{}, fmt.Errorf("rename %s → %s: %w", tmpDir, outDir, err)
	}
	committed = true
	return prepareResult{Dir: outDir, Tenants: len(cfg.Tenants), LoopPlan: loopPlan}, nil
}

// cleanupStaging removes a failed staging directory on error.
func cleanupStaging(tmpDir string) {
	if err := os.RemoveAll(tmpDir); err != nil {
		slog.Warn("prepare cleanup failed", "path", tmpDir, "err", err)
	}
}

// Prepare stages a migration directory and registers it in the live
// Registry.
func (r *Registry) Prepare(name string, compiledPlan []byte) (*Migration, error) {
	res, err := prepareToDir(r.migrationsDir, name, compiledPlan)
	if err != nil {
		return nil, err
	}
	m := &Migration{
		name:    name,
		dir:     res.Dir,
		plan:    res.LoopPlan,
		rootCtx: r.root,
		logs:    newLogHub(),
	}
	r.register(m)
	return m, nil
}

// register adds m to the Registry under its name, replacing any previous
// entry.
func (r *Registry) register(m *Migration) {
	m.loopOptions = r.loopOptions
	m.retryInterval = r.retryInterval
	m.provisioner = r.provisioner
	r.mu.Lock()
	if _, exists := r.migrations[m.name]; !exists {
		r.order = append(r.order, m.name)
	}
	r.migrations[m.name] = m
	r.mu.Unlock()
}
