package cmd

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"

	cpaws "github.com/tidbcloud/controlplane9/internal/aws"
	"github.com/tidbcloud/controlplane9/internal/cluster"
	"github.com/tidbcloud/controlplane9/internal/config"
	"github.com/tidbcloud/controlplane9/internal/host"
)

const sessionConflictCode = "SESSION_CONFLICT"

type sessionCollisionError struct {
	session string
}

func (e *sessionCollisionError) Error() string {
	if e == nil || e.session == "" {
		return sessionConflictCode
	}
	return fmt.Sprintf("%s: session %s already exists", sessionConflictCode, e.session)
}

func inspectV1Alpha2LocalSessionState(
	ctx context.Context,
	sessionName string,
) (bool, string, []string, error) {
	currentStatePath, err := filepath.Abs(cluster.SessionStateDir(sessionName))
	if err != nil {
		return false, "", nil, fmt.Errorf("resolve current session state path: %w", err)
	}

	currentStateInfo, statErr := os.Stat(currentStatePath)
	currentStateExists := statErr == nil
	currentStateAuthoritative := false
	if currentStateExists {
		stateExists, stateErr := cluster.StateExists(currentStatePath)
		if stateErr != nil {
			return false, currentStatePath, nil, stateErr
		}
		if stateExists {
			state, loadErr := cluster.LoadState(currentStatePath)
			if loadErr == nil && state.ClusterName == sessionName {
				currentStateAuthoritative = true
			}
		}
	} else if !os.IsNotExist(statErr) {
		return false, currentStatePath, nil, fmt.Errorf("stat current session state: %w", statErr)
	}

	paths := make([]string, 0)
	if currentStateExists && !currentStateAuthoritative {
		paths = append(paths, currentStatePath)
	}
	worktreeRoots, err := gitWorktreeRoots(ctx)
	if err != nil {
		return false, currentStatePath, nil, err
	}
	for _, root := range worktreeRoots {
		candidate, err := filepath.Abs(filepath.Join(root, cluster.DefaultStateDir, sessionName))
		if err != nil {
			return false, currentStatePath, nil, fmt.Errorf("resolve worktree session state path: %w", err)
		}
		candidateInfo, err := os.Stat(candidate)
		if err == nil {
			if currentStateInfo != nil && os.SameFile(currentStateInfo, candidateInfo) {
				continue
			}
			paths = append(paths, candidate)
		} else if !os.IsNotExist(err) {
			return false, currentStatePath, nil, fmt.Errorf("stat worktree session state %q: %w", candidate, err)
		}
	}
	slices.Sort(paths)
	paths = slices.Compact(paths)
	if currentStateAuthoritative && len(paths) == 0 {
		return true, currentStatePath, nil, nil
	}
	if currentStateAuthoritative {
		paths = append(paths, currentStatePath)
		slices.Sort(paths)
	}
	return false, currentStatePath, paths, nil
}

func gitWorktreeRoots(ctx context.Context) ([]string, error) {
	topLevel, err := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel").Output()
	if err != nil {
		return nil, fmt.Errorf("resolve current git worktree root: %w", err)
	}
	root := strings.TrimSpace(string(topLevel))
	if root == "" {
		return nil, fmt.Errorf("resolve current git worktree root: empty result")
	}
	out, err := exec.CommandContext(ctx, "git", "-C", root, "worktree", "list", "--porcelain").Output()
	if err != nil {
		return nil, fmt.Errorf("list git worktrees from %q: %w", root, err)
	}
	roots := make([]string, 0)
	for line := range strings.SplitSeq(string(out), "\n") {
		if worktreeRoot, ok := strings.CutPrefix(line, "worktree "); ok {
			worktreeRoot = strings.TrimSpace(worktreeRoot)
			if worktreeRoot != "" {
				roots = append(roots, worktreeRoot)
			}
		}
	}
	if len(roots) == 0 {
		return nil, fmt.Errorf("list git worktrees from %q: no worktrees returned", root)
	}
	return roots, nil
}

func runV1Alpha2SessionCollisionPreflight(
	ctx context.Context,
	session *config.SessionConfig,
	owner string,
	scanCloud bool,
) (*cpaws.SessionCollisionInventory, error) {
	if session == nil {
		return nil, fmt.Errorf("session config is required")
	}
	resume, currentStatePath, localPaths, err := inspectV1Alpha2LocalSessionState(ctx, session.Name)
	if err != nil {
		return nil, err
	}
	inventory := &cpaws.SessionCollisionInventory{
		Status:           "clear",
		Session:          session.Name,
		CurrentStatePath: currentStatePath,
	}
	if resume {
		inventory.Status = "resume"
		return inventory, nil
	}
	inventory.LocalStatePaths = localPaths

	if scanCloud {
		reaper, verifierRequest, err := makeV1Alpha2SessionCollisionReaper(ctx, session, owner)
		if err != nil {
			return inventory, fmt.Errorf("initialize session collision scanner: %w", err)
		}
		result, err := reaper.Reap(ctx, cpaws.Scope{
			SessionID:  session.Name,
			Regions:    []string{session.Target.Region},
			AllOwners:  true,
			AdminSweep: true,
		}, true, nil)
		if err != nil {
			return inventory, fmt.Errorf("scan session collision resources: %w", err)
		}
		inventory.CloudResources, _, err = filterPostNukeStaleSurvivorsWithWarnings(
			ctx,
			result.Survivors,
			verifierRequest,
			"session_collision_preflight",
		)
		if err != nil {
			return inventory, fmt.Errorf("verify session collision resources: %w", err)
		}
		inventory.CloudResources = slices.DeleteFunc(inventory.CloudResources, func(resource cpaws.Resource) bool {
			return resource.Type == cpaws.ResourceEC2KeyPair
		})
	}

	if len(inventory.LocalStatePaths) == 0 && len(inventory.CloudResources) == 0 {
		return inventory, nil
	}
	inventory.Status = "conflict"
	inventory.Code = sessionConflictCode
	inventory.RecoveryActions = sessionCollisionRecoveryActions(inventory, owner)
	return inventory, &sessionCollisionError{session: inventory.Session}
}

func sessionCollisionRecoveryActions(
	inventory *cpaws.SessionCollisionInventory,
	owner string,
) []string {
	if inventory == nil {
		return nil
	}
	foreignOwners := make([]string, 0)
	for i := range inventory.CloudResources {
		resourceOwner := strings.TrimSpace(inventory.CloudResources[i].Owner)
		if !cpaws.OwnerTagMatches(owner, resourceOwner) {
			if resourceOwner == "" {
				resourceOwner = "<unknown>"
			}
			foreignOwners = append(foreignOwners, resourceOwner)
		}
	}
	slices.Sort(foreignOwners)
	foreignOwners = slices.Compact(foreignOwners)
	if len(foreignOwners) > 0 {
		return []string{
			fmt.Sprintf(
				"choose a different session name or ask owner(s) %s to run exact cleanup; do not destroy or adopt these resources with the current config",
				strings.Join(foreignOwners, ","),
			),
		}
	}

	actions := make([]string, 0, len(inventory.LocalStatePaths))
	for _, statePath := range inventory.LocalStatePaths {
		if statePath == inventory.CurrentStatePath {
			actions = append(actions, "bin/cp9 destroy config=<same-config>")
			continue
		}
		worktreeRoot := filepath.Dir(filepath.Dir(statePath))
		configPath := config.ResolvedSessionPath(statePath)
		if _, err := os.Stat(configPath); os.IsNotExist(err) {
			configPath = "<same-session-config>"
		}
		actions = append(actions, fmt.Sprintf(
			"cd %q && bin/cp9 destroy config=%q",
			worktreeRoot,
			configPath,
		))
	}
	if len(actions) == 0 && len(inventory.CloudResources) > 0 {
		actions = append(actions, "bin/cp9 destroy config=<same-config>")
	}
	slices.Sort(actions)
	return slices.Compact(actions)
}

func runV1Alpha2UpSessionCollisionPreflight(
	ctx context.Context,
	session *config.SessionConfig,
) (*cpaws.SessionCollisionInventory, error) {
	resume, _, _, err := inspectV1Alpha2LocalSessionState(ctx, session.Name)
	if err != nil {
		return nil, err
	}
	if !config.UsesCloudSubstrate(session) {
		return cmdRunV1Alpha2SessionCollisionPreflight(ctx, session, "", false)
	}
	stateDir := host.ResolveSessionConfig(session).StateDir
	if resume && session.Host.Mode != config.HostModeRemoteEC2K3d {
		return cmdRunV1Alpha2SessionCollisionPreflight(ctx, session, "", false)
	}
	if resume && session.Host.Mode == config.HostModeRemoteEC2K3d {
		instanceID, err := persistedRemoteInstanceID(stateDir)
		if err != nil {
			return nil, err
		}
		if strings.TrimSpace(instanceID) != "" {
			if _, err := validateV1Alpha2UserManagedEC2Key(ctx, session, nil, stateDir, ""); err != nil {
				return nil, err
			}
			// Preserve the old resume fast-path: a persisted host does not
			// require refreshing the source AWS profile or rescanning cloud state.
			return cmdRunV1Alpha2SessionCollisionPreflight(ctx, session, "", false)
		}
	}

	profile := session.Target.AWS.IAM.SourceProfile
	if strings.TrimSpace(profile) == "" {
		profile = session.Target.AWS.IAM.Profile
	}
	awsCfg, err := cmdLoadAWSConfig(ctx, profile, session.Target.Region)
	if err != nil {
		return nil, err
	}
	result, effectiveAWSConfig, err := cmdRunV1Alpha2SetupIAMPreflight(
		ctx,
		&awsCfg,
		profile,
		session.Target.Region,
		session.Target.AWS.IAM.RoleARN,
		session.Name,
		session.CI.OwnerAllowlist,
	)
	if err != nil {
		return nil, err
	}
	if session.Host.Mode == config.HostModeRemoteEC2K3d {
		effective := cpaws.EffectiveAWSConfig(&effectiveAWSConfig, result)
		if _, err := validateV1Alpha2UserManagedEC2Key(
			ctx,
			session,
			&effective,
			stateDir,
			result.Account,
		); err != nil {
			return nil, err
		}
	}
	if resume {
		return cmdRunV1Alpha2SessionCollisionPreflight(ctx, session, "", false)
	}
	return cmdRunV1Alpha2SessionCollisionPreflight(ctx, session, result.Owner, true)
}
