package cmd

import (
	"context"
	"errors"
	"fmt"
	"os"
	"reflect"

	"gopkg.in/yaml.v3"

	"github.com/tidbcloud/controlplane9/internal/byoc"
	"github.com/tidbcloud/controlplane9/internal/cluster"
	"github.com/tidbcloud/controlplane9/internal/config"
	"github.com/tidbcloud/controlplane9/internal/host"
	remoteec2 "github.com/tidbcloud/controlplane9/internal/host/remote/ec2k3d"
	"github.com/tidbcloud/controlplane9/internal/lifecycle"
	cptilt "github.com/tidbcloud/controlplane9/internal/tilt"
)

var reserveV1Alpha2TiltPort = cptilt.ReserveAPIPort
var reconcileV1Alpha2Runtime = cptilt.ReconcilePersistedRuntimeForHost
var refreshV1Alpha2RuntimeInputs = cptilt.RefreshRemoteControllerInputs

func prepareV1Alpha2RuntimeSession(
	resolved *config.SessionConfig,
	stateDir string,
) (*cptilt.PortReservation, error) {
	if resolved == nil {
		return nil, fmt.Errorf("session config is required")
	}
	stateExists, err := cluster.StateExists(stateDir)
	if err != nil {
		return nil, fmt.Errorf("check session state before Tilt port preflight: %w", err)
	}
	if stateExists {
		resolvedPath := config.ResolvedSessionPath(stateDir)
		_, statErr := os.Stat(resolvedPath)
		switch {
		case statErr == nil:
			persisted, loadErr := config.LoadSession(resolvedPath)
			if loadErr != nil {
				return nil, fmt.Errorf("load persisted session for resume: %w", loadErr)
			}
			allowBYOCReuseMigration, migrationErr := allowBYOCAccountSetupReuseMigration(
				stateDir,
				persisted,
				resolved,
			)
			if migrationErr != nil {
				return nil, migrationErr
			}
			merged, mergeErr := reconcileV1Alpha2ResumeSession(persisted, resolved, allowBYOCReuseMigration)
			if mergeErr != nil {
				return nil, mergeErr
			}
			*resolved = *merged
			if err := config.WriteResolvedSession(stateDir, resolved); err != nil {
				return nil, fmt.Errorf("persist supported resume config refresh: %w", err)
			}
		case errors.Is(statErr, os.ErrNotExist):
			// A state directory may predate the resolved-session artifact. The
			// normal config-load lifecycle step will create it after preflight.
		default:
			return nil, fmt.Errorf("stat persisted session for resume: %w", statErr)
		}
	}

	paths := cptilt.PathsForState(stateDir, nil)
	var selected *host.Session
	var gcpSelected *host.Session
	gcpContextMissing := false
	gcpInputsRefreshDeferred := false
	remoteAddressChanged := false
	if stateExists {
		state, loadErr := cluster.LoadState(stateDir)
		if loadErr != nil {
			return nil, fmt.Errorf("load lifecycle state for runtime resume: %w", loadErr)
		}
		if config.DedicatedGCPEnabled(resolved) {
			if err := cptilt.ValidateRuntimePlanTiltfile(stateDir, cptilt.PathsForState(stateDir, state).Tiltfile); err != nil {
				return nil, fmt.Errorf("validate persisted runtime plan before resume: %w", err)
			}
		}
		var selectedErr error
		selected, selectedErr = host.Load(stateDir)
		if selectedErr != nil && !errors.Is(selectedErr, os.ErrNotExist) {
			return nil, fmt.Errorf("load selected host for runtime resume: %w", selectedErr)
		}
		if selected != nil && selected.IsRemote() {
			remoteAddressChanged, selectedErr = remoteec2.RefreshInstanceAddress(context.Background(), selected)
			if selectedErr != nil {
				return nil, fmt.Errorf("refresh remote host address for runtime resume: %w", selectedErr)
			}
		}
		var reconcile *cptilt.RuntimeReconcileResult
		var gcpReconcile *cptilt.RuntimeReconcileResult
		if remoteAddressChanged {
			if _, stopErr := cptilt.NewProcessStopper().StopPersistedProcess(context.Background(), paths.Status, stateDir); stopErr != nil {
				return nil, fmt.Errorf("stop stale local tunnels after remote host address change: %w", stopErr)
			}
			reconcile = &cptilt.RuntimeReconcileResult{RestartRequired: true}
		} else {
			reconcile, selectedErr = reconcileV1Alpha2Runtime(
				context.Background(),
				selected,
				paths.Status,
				stateDir,
			)
			if selectedErr != nil {
				return nil, fmt.Errorf("reconcile persisted runtime for resume: %w", selectedErr)
			}
		}
		if remoteAddressChanged {
			if contextStep := state.Steps[cluster.StepSessionContextWrite]; contextStep != nil && contextStep.Status == cluster.StatusDone {
				if writeErr := (&lifecycle.SessionContextWriteStep{StateDir: stateDir}).RunWithState(context.Background(), state); writeErr != nil {
					return nil, fmt.Errorf("refresh session context after remote host address change: %w", writeErr)
				}
			}
		}
		if config.DedicatedGCPEnabled(resolved) {
			gcpStateDir := host.GCPRegionalStateDir(stateDir)
			gcpSelected, selectedErr = host.Load(gcpStateDir)
			switch {
			case selectedErr == nil:
				if _, statErr := os.Stat(cptilt.PathsForState(gcpStateDir, nil).ContextIndex); errors.Is(statErr, os.ErrNotExist) {
					gcpContextMissing = true
				} else if statErr != nil {
					return nil, fmt.Errorf("stat persisted GCP runtime context for resume: %w", statErr)
				}
				gcpStatusPath := cptilt.PathsForState(gcpStateDir, nil).Status
				if _, statErr := os.Stat(gcpStatusPath); errors.Is(statErr, os.ErrNotExist) {
					gcpReconcile = &cptilt.RuntimeReconcileResult{RestartRequired: true}
				} else if statErr != nil {
					return nil, fmt.Errorf("stat persisted GCP runtime for resume: %w", statErr)
				} else {
					gcpReconcile, selectedErr = reconcileV1Alpha2Runtime(
						context.Background(),
						gcpSelected,
						gcpStatusPath,
						gcpStateDir,
					)
				}
				if selectedErr != nil {
					return nil, fmt.Errorf("reconcile persisted GCP runtime for resume: %w", selectedErr)
				}
			case errors.Is(selectedErr, os.ErrNotExist):
				gcpSelected = nil
				if step := state.Steps[cluster.StepHostReady]; step != nil && step.Status == cluster.StatusDone {
					return nil, fmt.Errorf("persisted GCP host is missing after host_ready")
				}
			default:
				return nil, fmt.Errorf("load persisted GCP host for runtime resume: %w", selectedErr)
			}
		}
		steps := []cluster.StepName{cluster.StepSessionReady}
		if gcpContextMissing {
			steps = append(steps, cluster.StepSessionContextWrite)
		}
		gcpInputsRefreshDeferred = gcpContextMissing ||
			(reconcile != nil && reconcile.RestartRequired) ||
			(gcpReconcile != nil && gcpReconcile.RestartRequired)
		if gcpInputsRefreshDeferred {
			steps = append(steps, cluster.StepTiltReady)
		}
		for _, step := range steps {
			if stepState := state.Steps[step]; stepState != nil {
				stepState.Status = cluster.StatusPending
				stepState.Error = ""
				stepState.StartedAt = nil
				stepState.EndedAt = nil
			}
		}
		if saveErr := cluster.SaveState(stateDir, state); saveErr != nil {
			return nil, fmt.Errorf("persist lifecycle runtime resume point: %w", saveErr)
		}
	}
	refreshGCPInputs := func() error {
		if gcpSelected == nil || gcpContextMissing {
			return nil
		}
		gcpStateDir := host.GCPRegionalStateDir(stateDir)
		status, err := cptilt.LoadStatus(cptilt.PathsForState(gcpStateDir, nil).Status)
		if errors.Is(err, os.ErrNotExist) {
			return nil
		}
		if err != nil {
			return err
		}
		if status.Running {
			_, err = refreshV1Alpha2RuntimeInputs(context.Background(), gcpSelected, gcpStateDir, status)
		}
		return err
	}
	if status, statusErr := cptilt.LoadStatus(paths.Status); statusErr == nil && status != nil && status.Running {
		if resolved.Tilt.Port > 0 && status.APIPort != resolved.Tilt.Port {
			return nil, fmt.Errorf(
				"cannot change tilt.port from running session port %d to %d; run cp9 destroy for this session before changing a live Tilt endpoint",
				status.APIPort,
				resolved.Tilt.Port,
			)
		}
		resolved.Tilt.Port = status.APIPort
		if config.DedicatedGCPEnabled(resolved) {
			if _, err := cptilt.GCPRuntimeAPIPort(resolved.Tilt.Port); err != nil {
				return nil, err
			}
		}
		if _, err := refreshV1Alpha2RuntimeInputs(context.Background(), selected, stateDir, status); err != nil {
			return nil, fmt.Errorf("refresh remote controller inputs for resume: %w", err)
		}
		if !gcpInputsRefreshDeferred {
			if err := refreshGCPInputs(); err != nil {
				return nil, fmt.Errorf("refresh GCP remote controller inputs for resume: %w", err)
			}
		}
		return cptilt.ReleasedPortReservation(status.APIPort), nil
	}
	if !gcpInputsRefreshDeferred {
		if err := refreshGCPInputs(); err != nil {
			return nil, fmt.Errorf("refresh GCP remote controller inputs for resume: %w", err)
		}
	}

	reservation, err := reserveV1Alpha2TiltPort(resolved.Tilt.Port)
	if err != nil {
		return nil, err
	}
	resolved.Tilt.Port = reservation.Port()
	if config.DedicatedGCPEnabled(resolved) {
		if _, err := cptilt.GCPRuntimeAPIPort(resolved.Tilt.Port); err != nil {
			_ = reservation.Release()
			return nil, err
		}
	}
	if stateExists {
		if err := config.WriteResolvedSession(stateDir, resolved); err != nil {
			_ = reservation.Release()
			return nil, fmt.Errorf("persist reserved Tilt port for resume: %w", err)
		}
	}
	return reservation, nil
}

func reconcileV1Alpha2ResumeSession(
	persisted *config.SessionConfig,
	requested *config.SessionConfig,
	allowBYOCReuseMigration bool,
) (*config.SessionConfig, error) {
	persistedClone, err := cloneV1Alpha2Session(persisted)
	if err != nil {
		return nil, err
	}
	requestedClone, err := cloneV1Alpha2Session(requested)
	if err != nil {
		return nil, err
	}

	port := persistedClone.Tilt.Port
	if requestedClone.Tilt.Port > 0 {
		port = requestedClone.Tilt.Port
	}
	persistedClone.Tilt.Port = port
	requestedClone.Tilt.Port = port
	if allowBYOCReuseMigration &&
		persistedClone.BYOC != nil && requestedClone.BYOC != nil &&
		!persistedClone.BYOC.AccountSetup.ReuseExistingStacks &&
		requestedClone.BYOC.AccountSetup.ReuseExistingStacks {
		persistedClone.BYOC.AccountSetup.ReuseExistingStacks = true
	}

	if !reflect.DeepEqual(persistedClone, requestedClone) {
		return nil, fmt.Errorf(
			"public config changed immutable session fields during resume; only tilt.port may change (use a new session name or destroy the existing session)",
		)
	}
	return persistedClone, nil
}

func allowBYOCAccountSetupReuseMigration(
	stateDir string,
	persisted *config.SessionConfig,
	requested *config.SessionConfig,
) (bool, error) {
	if persisted == nil || requested == nil || persisted.BYOC == nil || requested.BYOC == nil ||
		persisted.BYOC.AccountSetup.ReuseExistingStacks ||
		!requested.BYOC.AccountSetup.ReuseExistingStacks {
		return false, nil
	}
	_, err := os.Stat(byoc.JournalPath(stateDir))
	switch {
	case err == nil:
		return false, nil
	case errors.Is(err, os.ErrNotExist):
		return true, nil
	default:
		return false, fmt.Errorf("stat BYOC activation journal before account-setup reuse migration: %w", err)
	}
}

func cloneV1Alpha2Session(in *config.SessionConfig) (*config.SessionConfig, error) {
	if in == nil {
		return nil, fmt.Errorf("session config is required")
	}
	data, err := yaml.Marshal(in)
	if err != nil {
		return nil, fmt.Errorf("clone session config: %w", err)
	}
	var out config.SessionConfig
	if err := yaml.Unmarshal(data, &out); err != nil {
		return nil, fmt.Errorf("clone session config: %w", err)
	}
	return &out, nil
}
