package cmd

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

	"github.com/innerr/ticat/pkg/core/model"

	"github.com/tidbcloud/controlplane9/internal/artifactsync"
	"github.com/tidbcloud/controlplane9/internal/cluster"
	"github.com/tidbcloud/controlplane9/internal/config"
	cp9errors "github.com/tidbcloud/controlplane9/internal/errors"
	"github.com/tidbcloud/controlplane9/internal/lifecycle"
	cptilt "github.com/tidbcloud/controlplane9/internal/tilt"
)

type artifactResult struct {
	Environment string   `json:"environment"`
	Artifacts   []string `json:"artifacts"`
	Digest      string   `json:"digest"`
	Runtime     string   `json:"runtime,omitempty"`
	Snapshot    string   `json:"snapshot"`
	Refreshed   bool     `json:"refreshed,omitempty"`
}

var (
	loadArtifactRefreshRuntime          = loadV1Alpha2TiltRuntimeForResource
	acquireArtifactRefreshOperationLock = acquireV1Alpha2SessionOperationLock
	ensureArtifactRefreshSession        = ensureTiltSessionRunning
	resolveArtifactRefreshSession       = config.ResolveSession
	selectedArtifactRefreshSourceBuilds = cptilt.SelectedSourceBuildServices
	resolveArtifactRefresh              = artifactsync.Resolve
	loadArtifactRefreshSnapshot         = artifactsync.LoadSnapshot
	refreshArtifactRemoteContext        = cptilt.RefreshRemoteArtifactContext
	restoreArtifactRemoteContext        = cptilt.RestoreRemoteArtifactContext
	clearArtifactRemoteTransaction      = cptilt.ClearRemoteArtifactRefreshTransaction
	triggerArtifactRefreshTarget        = triggerV1Alpha2TiltResource
	runArtifactRefreshContext           = runArtifactRefreshContextStep
	publishArtifactRefreshRollback      = artifactsync.PublishSnapshotKeepingRefreshTransaction
)

func runArtifactRefreshContextStep(ctx context.Context, stateDir string, state *cluster.State) error {
	return (&lifecycle.SessionContextWriteStep{StateDir: stateDir}).RunWithState(ctx, state)
}

func (r *artifactResult) FormatText() string {
	action := "resolved"
	if r.Refreshed {
		action = "refreshed"
	}
	target := r.Environment
	if r.Runtime != "" {
		target += "/" + r.Runtime
	}
	return fmt.Sprintf("%s %s artifacts %s (%s); snapshot %s", action, target, strings.Join(r.Artifacts, ", "), r.Digest, r.Snapshot)
}

func artifactsResolveCmd() model.NormalCmd {
	return func(args model.ArgVals, cc *model.Cli, env *model.Env, _ []model.ParsedCmd) error {
		syncVerbose(env)
		environment := strings.TrimSpace(args.GetRaw("environment"))
		output, err := filepath.Abs(args.GetRaw("output"))
		if err != nil {
			return cp9errors.Newf(cp9errors.CategoryConfig, "resolve artifact output path: %v", err)
		}
		tokenHelper, err := filepath.Abs(args.GetRaw("token-helper"))
		if err != nil {
			return cp9errors.Newf(cp9errors.CategoryConfig, "resolve GitHub token helper path: %v", err)
		}
		snapshot, err := artifactsync.Resolve(context.Background(), &artifactsync.Options{
			Environment:       environment,
			OutputDir:         output,
			GitHubAppClientID: args.GetRaw("github-app-client-id"),
			TokenHelper:       tokenHelper,
		})
		if err != nil {
			return cp9errors.Newf(cp9errors.CategoryResource, "resolve artifacts: %v", err)
		}
		return model.Output(cc, env, artifactResultFromSnapshot(snapshot, output, false, nil))
	}
}

func artifactsRefreshCmd() model.NormalCmd {
	return func(args model.ArgVals, cc *model.Cli, env *model.Env, _ []model.ParsedCmd) error {
		syncVerbose(env)
		selected, err := parseArtifactSelection(args.GetRaw("artifacts"))
		if err != nil {
			return err
		}
		cfgPath := args.GetRaw("config")
		localPath, localProvided := explicitLocalPath(args)
		result, err := runV1Alpha2ArtifactRefresh(
			context.Background(), cfgPath, localPath, localProvided, selected,
			args.GetRaw("image"), args.GetRaw("runtime"),
		)
		if err != nil {
			return wrapDay2CommandError("refresh artifacts", err)
		}
		return model.Output(cc, env, result)
	}
}

func runV1Alpha2ArtifactRefresh(
	ctx context.Context,
	cfgPath, localPath string,
	localProvided bool,
	selected map[string]bool,
	image, requestedRuntime string,
) (result *artifactResult, resultErr error) {
	names := selectedNames(selected)
	runtime, err := loadArtifactRefreshRuntime(
		cfgPath, localPath, localProvided, names[0], strings.TrimSpace(requestedRuntime),
	)
	if err != nil {
		return nil, err
	}
	if runtime.Session == nil {
		return nil, errors.New("artifact refresh session is missing")
	}
	releaseOperationLock, err := acquireArtifactRefreshOperationLock(cluster.DefaultStateDir, runtime.Session.Name)
	if err != nil {
		return nil, fmt.Errorf("acquire artifact refresh operation lock: %w", err)
	}
	defer releaseOperationLock()
	if err := ensureArtifactRefreshSession(ctx, runtime); err != nil {
		return nil, err
	}
	if step, ok := runtime.State.Steps[cluster.StepSessionReady]; !ok || step.Status != cluster.StatusDone {
		return nil, fmt.Errorf("session %s is not Ready", runtime.Session.Name)
	}
	registry, _, err := artifactsync.DefaultRegistry()
	if err != nil {
		return nil, err
	}
	known := map[string]bool{}
	for i := range registry.Artifacts {
		artifact := &registry.Artifacts[i]
		known[artifact.Name] = true
	}
	before, err := readTiltReadiness(ctx, runtime.Selected, runtime.Status)
	if err != nil {
		return nil, fmt.Errorf("read Tilt state before artifact refresh: %w", err)
	}
	if before == nil {
		return nil, fmt.Errorf("read Tilt state before artifact refresh: empty response")
	}
	for name := range selected {
		if !known[name] {
			return nil, cp9errors.Newf(cp9errors.CategoryConfig, "artifact %q is not registered", name)
		}
		if _, ok := before.ResourceStates[name]; !ok {
			return nil, fmt.Errorf("tilt resource %s is not present in the Ready session", name)
		}
	}
	output := artifactsync.OutputDir(runtime.StateDir)
	current, err := loadArtifactRefreshSnapshot(output)
	if err != nil {
		return nil, fmt.Errorf("load current artifact snapshot: %w", err)
	}
	resolved, err := resolveArtifactRefreshSession(runtime.Session, cfgPath)
	if err != nil {
		return nil, err
	}
	sourceBuilds, err := selectedArtifactRefreshSourceBuilds(resolved.Tilt.File)
	if err != nil {
		return nil, err
	}
	for _, name := range sourceBuilds {
		if selected[name] {
			return nil, cp9errors.Newf(
				cp9errors.CategoryConfig,
				"artifact %q is running from source; remove it from source_build_services before refreshing its image",
				name,
			)
		}
	}
	options := artifactOptions(resolved, current.Environment, output)
	options.Current = current
	options.Selected = selected
	options.ImageOverride = image
	options.TrackTransaction = true
	snapshot, err := resolveArtifactRefresh(ctx, &options)
	if err != nil {
		return nil, err
	}
	changed := cloneArtifactSelection(options.ChangedArtifacts)
	recovered := cloneArtifactSelection(options.RecoveredArtifacts)
	clearTransactions := func(clearCtx context.Context) error {
		if runtime.Status.Controller == cptilt.ControllerRemote {
			if err := clearArtifactRemoteTransaction(clearCtx, runtime.Selected, runtime.Status); err != nil {
				return err
			}
		}
		if err := artifactsync.ClearRefreshTransaction(output); err != nil {
			return fmt.Errorf("clear local artifact refresh transaction: %w", err)
		}
		return nil
	}
	if snapshot.Digest == current.Digest {
		if runtime.Status.Controller == cptilt.ControllerRemote {
			runtimeTargets := mergeArtifactSelections(changed, recovered)
			remoteChanged, remoteRecovered, refreshErr := refreshArtifactRemoteContext(
				ctx, runtime.Selected, runtime.StateDir, runtime.RuntimeStateDir,
				runtime.Status, selected, runtimeTargets, snapshot,
			)
			changed = artifactRefreshTargets(snapshot, changed, remoteChanged)
			recovered = mergeArtifactSelections(recovered, remoteRecovered)
			if refreshErr != nil {
				return nil, refreshErr
			}
		}
		if len(recovered) > 0 {
			if err := triggerArtifactRecoveryTargets(ctx, runtime, recovered); err != nil {
				return nil, err
			}
		}
		changed = mergeArtifactSelections(changed, recovered)
		if err := waitForArtifactRefreshTargets(ctx, runtime, before, changed); err != nil {
			return nil, err
		}
		if err := clearTransactions(ctx); err != nil {
			return nil, err
		}
		return artifactResultFromRuntime(snapshot, output, selected, runtime), nil
	}
	converged := false
	defer func() {
		if resultErr == nil || converged || options.RecoveredTransaction {
			return
		}
		rollbackCtx := context.WithoutCancel(ctx)
		rollbackBaseline, baselineErr := readTiltReadiness(rollbackCtx, runtime.Selected, runtime.Status)
		if baselineErr != nil {
			resultErr = errors.Join(resultErr, fmt.Errorf("read rollback artifact runtime baseline: %w", baselineErr))
			return
		}
		if rollbackBaseline == nil {
			resultErr = errors.Join(resultErr, errors.New("read rollback artifact runtime baseline: empty response"))
			return
		}
		rollbackLocalChanged, err := publishArtifactRefreshRollback(output, current, selected)
		if err != nil {
			resultErr = errors.Join(resultErr, fmt.Errorf("restore prior artifact snapshot: %w", err))
			return
		}
		if err := runArtifactRefreshContext(rollbackCtx, runtime.StateDir, runtime.State); err != nil {
			resultErr = errors.Join(resultErr, fmt.Errorf("restore prior artifact context: %w", err))
			return
		}
		rollbackRestoreErr := error(nil)
		rollbackRemoteChanged, _, err := restoreArtifactRemoteContext(
			rollbackCtx,
			runtime.Selected,
			runtime.StateDir,
			runtime.RuntimeStateDir,
			runtime.Status,
			selected,
			map[string]bool{},
			current,
		)
		if err != nil {
			rollbackRestoreErr = fmt.Errorf("restore prior remote artifact context: %w", err)
			resultErr = errors.Join(resultErr, rollbackRestoreErr)
		}
		if rollbackRestoreErr != nil {
			return
		}
		rollbackWaitErr := error(nil)
		rollbackTargets := rollbackLocalChanged
		if runtime.Status.Controller == cptilt.ControllerRemote {
			rollbackTargets = artifactRefreshTargets(current, map[string]bool{}, rollbackRemoteChanged)
		}
		if len(rollbackTargets) > 0 {
			if err := waitForArtifactRefreshTargets(rollbackCtx, runtime, rollbackBaseline, rollbackTargets); err != nil {
				rollbackWaitErr = fmt.Errorf("wait for artifact runtime rollback: %w", err)
			}
		}
		if rollbackWaitErr != nil {
			resultErr = errors.Join(resultErr, rollbackWaitErr)
			return
		}
		if err := clearTransactions(rollbackCtx); err != nil {
			resultErr = errors.Join(resultErr, fmt.Errorf("clear artifact rollback transaction: %w", err))
		}
	}()
	if err := runArtifactRefreshContext(ctx, runtime.StateDir, runtime.State); err != nil {
		return nil, err
	}
	runtimeTargets := mergeArtifactSelections(changed, recovered)
	remoteChanged, remoteRecovered, refreshErr := refreshArtifactRemoteContext(
		ctx, runtime.Selected, runtime.StateDir, runtime.RuntimeStateDir,
		runtime.Status, selected, runtimeTargets, snapshot,
	)
	recovered = mergeArtifactSelections(recovered, remoteRecovered)
	if runtime.Status.Controller == cptilt.ControllerRemote {
		changed = artifactRefreshTargets(snapshot, changed, remoteChanged)
	}
	if refreshErr != nil {
		return nil, refreshErr
	}
	if len(recovered) > 0 {
		if err := triggerArtifactRecoveryTargets(ctx, runtime, recovered); err != nil {
			return nil, err
		}
	}
	changed = mergeArtifactSelections(changed, recovered)
	if err := waitForArtifactRefreshTargets(ctx, runtime, before, changed); err != nil {
		return nil, err
	}
	converged = true
	if err := clearTransactions(ctx); err != nil {
		return nil, err
	}
	return artifactResultFromRuntime(snapshot, output, selected, runtime), nil
}

func artifactResultFromRuntime(
	snapshot *artifactsync.Snapshot,
	output string,
	selected map[string]bool,
	runtime *v1alpha2TiltRuntime,
) *artifactResult {
	result := artifactResultFromSnapshot(snapshot, output, true, selected)
	if runtime.MultiRuntime {
		result.Runtime = runtime.RuntimeID
	}
	return result
}

func artifactOptions(session *config.SessionConfig, environment, output string) artifactsync.Options {
	tiltDir := filepath.Dir(session.Tilt.File)
	return artifactsync.Options{
		Environment:       environment,
		OutputDir:         output,
		GitHubAppClientID: session.Services.GitHubAppClientID,
		TokenHelper:       filepath.Clean(filepath.Join(tiltDir, "..", "build", "remote", "github-app-user-token.sh")),
	}
}

func artifactResultFromSnapshot(snapshot *artifactsync.Snapshot, output string, refreshed bool, selected map[string]bool) *artifactResult {
	names := make([]string, 0, len(snapshot.Artifacts))
	for i := range snapshot.Artifacts {
		if len(selected) == 0 || selected[snapshot.Artifacts[i].Name] {
			names = append(names, snapshot.Artifacts[i].Name)
		}
	}
	slices.Sort(names)
	return &artifactResult{
		Environment: snapshot.Environment,
		Artifacts:   names,
		Digest:      snapshot.Digest,
		Snapshot:    filepath.Join(output, artifactsync.SnapshotFile),
		Refreshed:   refreshed,
	}
}

func parseArtifactSelection(value string) (map[string]bool, error) {
	selected := map[string]bool{}
	for name := range strings.SplitSeq(value, ",") {
		name = strings.TrimSpace(name)
		if name == "" {
			continue
		}
		if selected[name] {
			return nil, cp9errors.Newf(cp9errors.CategoryConfig, "artifacts contains duplicate entry %q", name)
		}
		selected[name] = true
	}
	if len(selected) == 0 {
		return nil, cp9errors.New(cp9errors.CategoryConfig, "artifacts argument requires one or more comma-separated registry entry names")
	}
	return selected, nil
}

func selectedNames(selected map[string]bool) []string {
	names := make([]string, 0, len(selected))
	for name := range selected {
		names = append(names, name)
	}
	slices.Sort(names)
	return names
}

func waitForArtifactRefreshTargets(
	ctx context.Context,
	runtime *v1alpha2TiltRuntime,
	baseline *cptilt.ResourceReadiness,
	targets map[string]bool,
) error {
	if len(targets) == 0 {
		return nil
	}
	if baseline == nil {
		return errors.New("artifact refresh wait baseline is empty")
	}
	for _, name := range selectedNames(targets) {
		state, ok := baseline.ResourceStates[name]
		if !ok {
			return fmt.Errorf("artifact refresh wait baseline lacks %s", name)
		}
		if err := waitForV1Alpha2TiltResource(ctx, runtime, name, &state); err != nil {
			return err
		}
	}
	return nil
}

func cloneArtifactSelection(selected map[string]bool) map[string]bool {
	cloned := make(map[string]bool, len(selected))
	for name, enabled := range selected {
		if enabled {
			cloned[name] = true
		}
	}
	return cloned
}

func mergeArtifactSelections(left, right map[string]bool) map[string]bool {
	merged := cloneArtifactSelection(left)
	for name, enabled := range right {
		if enabled {
			merged[name] = true
		}
	}
	return merged
}

func triggerArtifactRecoveryTargets(
	ctx context.Context,
	runtime *v1alpha2TiltRuntime,
	recovered map[string]bool,
) error {
	for _, name := range selectedNames(recovered) {
		if err := triggerArtifactRefreshTarget(ctx, runtime, name); err != nil {
			return fmt.Errorf("trigger Tilt resource %s for recovery: %w", name, err)
		}
	}
	return nil
}

func artifactRefreshTargets(snapshot *artifactsync.Snapshot, localChanged, remoteChanged map[string]bool) map[string]bool {
	kinds := make(map[string]string, len(snapshot.Artifacts))
	for i := range snapshot.Artifacts {
		kinds[snapshot.Artifacts[i].Name] = snapshot.Artifacts[i].Kind
	}
	targets := make(map[string]bool, len(remoteChanged))
	for name := range remoteChanged {
		targets[name] = true
	}
	for name := range localChanged {
		if kinds[name] == "crd" {
			targets[name] = true
		}
	}
	return targets
}
