package migration

import (
	"context"
	"errors"
	"fmt"
	"path/filepath"

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

// Validate checks that a plan and action contain a supported executable
// contract.
func Validate(plan *schema.MigrationLoopPlan, action *schema.MigrationLoopAction) error {
	if plan == nil || plan.Runtime == nil {
		return errors.New("migration loop has no runtime configuration; rerun prepare")
	}
	if plan.Vmselect == nil {
		return errors.New("migration loop has no vmselect configuration; rerun prepare")
	}
	if action == nil {
		return errors.New("migration loop step has no action; rerun prepare")
	}
	require := func(value, field string) error {
		if value == "" {
			return fmt.Errorf("%s action has no %s; rerun prepare", action.Kind, field)
		}
		return nil
	}
	switch action.Kind {
	case schema.ActionManifest:
		if err := require(action.TenantsFile, "tenantsFile"); err != nil {
			return err
		}
		return require(action.Manifest, "manifest")
	case schema.ActionVerify, schema.ActionMigrate:
		return require(action.Manifest, "manifest")
	default:
		return fmt.Errorf("unsupported action kind %q; rerun prepare", action.Kind)
	}
}

// ValidateStep checks both the action payload and the canonical step-to-kind
// mapping.
//
// Step is a progress/status identity; kind is the executable discriminator,
// and allowing them to disagree would make operator selectors misleading.
func ValidateStep(plan *schema.MigrationLoopPlan, step string, action *schema.MigrationLoopAction) error {
	if err := Validate(plan, action); err != nil {
		return err
	}
	expected, ok := schema.KindForLoopStep(step)
	if !ok {
		return fmt.Errorf("unsupported migration loop step %q; rerun prepare", step)
	}
	if action.Kind != expected {
		return fmt.Errorf("step %q does not match action kind %q; rerun prepare", step, action.Kind)
	}
	return nil
}

// RunAction executes one action in process.
//
// vmctl remains an injected external dependency for the migrate kind; all
// vm-migration capabilities run as Go package calls.
func RunAction(
	ctx context.Context,
	plan *schema.MigrationLoopPlan,
	dir string,
	action *schema.MigrationLoopAction,
	vmctl VmctlRunner,
	output ActionOutput,
) error {
	output = output.normalized()
	if err := Validate(plan, action); err != nil {
		return err
	}
	switch action.Kind {
	case schema.ActionManifest:
		opts, err := manifestOptions(plan, dir, action)
		if err != nil {
			return err
		}
		return BuildManifest(ctx, opts, ManifestOutput{Result: output.Log, Progress: output.Progress})
	case schema.ActionVerify:
		opts, err := VerifyActionOptions(plan, dir, action)
		if err != nil {
			return err
		}
		runner, err := vmquery.NewVerifyRunner(opts, vmquery.NewHTTPClient(), vmquery.VerifyOutput{
			Progress: output.Progress,
			Log:      output.Log,
		})
		if err != nil {
			return err
		}
		_, err = runner.Run(ctx)
		return err
	case schema.ActionMigrate:
		if vmctl == nil {
			return errors.New("migrate action has no vmctl runner")
		}
		return RunVmctl(ctx, VmctlOptions{
			Manifest:          Resolve(dir, action.Manifest),
			VmRateLimit:       action.RateLimitBytes,
			VmConcurrency:     action.Concurrency,
			VmctlStepInterval: action.StepInterval,
			TunePath:          Resolve(dir, TuneFile),
		}, vmctl, output)
	default:
		return fmt.Errorf("unsupported action kind %q", action.Kind)
	}
}

// manifestActionFor returns the tenant's typed manifest action, or nil when
// the plan carries none.
func manifestActionFor(tp schema.MigrationLoopTenant) *schema.MigrationLoopAction {
	for _, c := range tp.Cmds {
		if c.Action != nil && c.Action.Kind == schema.ActionManifest {
			return c.Action
		}
	}
	return nil
}

func manifestOptions(plan *schema.MigrationLoopPlan, dir string, action *schema.MigrationLoopAction) (ManifestOptions, error) {
	if err := Validate(plan, action); err != nil {
		return ManifestOptions{}, err
	}
	runtime := plan.Runtime
	return ManifestOptions{
		Region:         runtime.Region,
		Vmcluster:      runtime.Vmcluster,
		TenantsFile:    Resolve(dir, action.TenantsFile),
		SrcVmselectURL: plan.Vmselect.Migration,
		DstVminsertURL: runtime.Vminsert,
		Start:          runtime.Window.Start,
		End:            runtime.Window.End,
		StepInterval:   runtime.Window.StepInterval,
		Output:         Resolve(dir, action.Manifest),
	}, nil
}

// VerifyActionOptions projects a verify action into the domain runner's
// options.
//
// Server jobs use this helper too, so loop and server execution cannot
// drift on URLs, timeout, or tolerance.
func VerifyActionOptions(plan *schema.MigrationLoopPlan, dir string, action *schema.MigrationLoopAction) (vmquery.VerifyOptions, error) {
	if err := Validate(plan, action); err != nil {
		return vmquery.VerifyOptions{}, err
	}
	if action.Kind != schema.ActionVerify {
		return vmquery.VerifyOptions{}, fmt.Errorf("action kind %q is not a verify action", action.Kind)
	}
	return vmquery.VerifyOptions{
		Manifest:             Resolve(dir, action.Manifest),
		OriginVmselectURL:    plan.Vmselect.Origin,
		CandidateVmselectURL: plan.Vmselect.Candidate,
		TimeoutSeconds:       plan.Runtime.Verification.TimeoutSeconds,
		Tolerance:            plan.Runtime.Verification.Tolerance,
	}, nil
}

// Resolve returns path unchanged when absolute, otherwise joins it onto dir.
//
// Shared by Run's action dispatch and the server jobs that need a resolved path
// before delegating to Run.
func Resolve(dir, path string) string {
	if filepath.IsAbs(path) {
		return path
	}
	return filepath.Join(dir, path)
}
