package tilt

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"time"
)

const kubeAPITunnelSupervisorScript = `#!/bin/sh
set -u

pid_file=$1
shift
if [ "$#" -eq 0 ]; then
  echo "kube tunnel supervisor: child command is required" >&2
  exit 2
fi

base_backoff=${CP9_TUNNEL_SUPERVISOR_BASE_BACKOFF_SECONDS:-1}
max_backoff=${CP9_TUNNEL_SUPERVISOR_MAX_BACKOFF_SECONDS:-30}
stable_seconds=${CP9_TUNNEL_SUPERVISOR_STABLE_SECONDS:-30}
max_failures=8
backoff=$base_backoff
child_pid=0
attempts=0
last_status=
last_failure_at=
terminal_state=
state_file=${pid_file%.pid}.state

write_state() {
  state=$1
  action_required=$2
  temporary="$state_file.tmp.$$"
  {
    printf 'supervisor_pid=%s\n' "$$"
    printf 'child_pid=%s\n' "$child_pid"
    printf 'state=%s\n' "$state"
    printf 'attempts=%s\n' "$attempts"
    printf 'last_exit_status=%s\n' "$last_status"
    printf 'last_failure_at=%s\n' "$last_failure_at"
    printf 'action_required=%s\n' "$action_required"
  } >"$temporary"
  mv "$temporary" "$state_file"
}

cleanup() {
  trap - INT TERM HUP EXIT
  if [ "$child_pid" -gt 0 ] 2>/dev/null && kill -0 "$child_pid" 2>/dev/null; then
    kill "$child_pid" >/dev/null 2>&1 || true
    wait "$child_pid" 2>/dev/null || true
  fi
  if [ "$terminal_state" != "quiescent" ]; then
    write_state stopped false
  fi
  rm -f "$pid_file" "$pid_file.tmp.$$"
}
trap 'cleanup; exit 0' INT TERM HUP
trap cleanup EXIT

write_state starting false
while :; do
  started_at=$(date +%s)
  "$@" &
  child_pid=$!
  printf '%s\n' "$child_pid" >"$pid_file.tmp.$$"
  mv "$pid_file.tmp.$$" "$pid_file"
  write_state running false
  echo "kube tunnel supervisor: child_pid=$child_pid" >&2

  wait "$child_pid"
  status=$?
  ended_at=$(date +%s)
  child_pid=0
  rm -f "$pid_file"

  runtime=$((ended_at - started_at))
  if [ "$runtime" -ge "$stable_seconds" ]; then
    backoff=$base_backoff
    attempts=0
  fi
  attempts=$((attempts + 1))
  last_status=$status
  last_failure_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  if [ "$attempts" -ge "$max_failures" ]; then
    terminal_state=quiescent
    write_state quiescent true
    echo "kube tunnel supervisor: quiescent after ${attempts} failures status=$status" >&2
    exit "$status"
  fi
  write_state backoff false
  echo "kube tunnel supervisor: restarting after ${backoff}s status=$status" >&2
  sleep "$backoff"
  if [ "$backoff" -lt "$max_backoff" ]; then
    backoff=$((backoff * 2))
    if [ "$backoff" -gt "$max_backoff" ]; then
      backoff=$max_backoff
    fi
  fi
done
`

// PrepareKubeAPITunnelSupervisor materializes the session-owned long-lived
// tunnel supervisor and its atomic child identity file outside generated source.
func PrepareKubeAPITunnelSupervisor(stateDir string) (supervisorPath, childPIDPath string, err error) {
	return prepareTunnelSupervisor(stateDir, "kube-api-tunnel")
}

func prepareTunnelSupervisor(stateDir, name string) (supervisorPath, childPIDPath string, err error) {
	supervisorPath, err = tunnelSupervisorPath(stateDir, name)
	if err != nil {
		return "", "", err
	}
	dir := filepath.Dir(supervisorPath)
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return "", "", fmt.Errorf("create kube tunnel runtime dir: %w", err)
	}
	if err := installTunnelSupervisor(supervisorPath); err != nil {
		return "", "", err
	}
	childPIDPath = filepath.Join(dir, name+"-child.pid")
	return supervisorPath, childPIDPath, nil
}

func tunnelSupervisorPath(stateDir, name string) (string, error) {
	path, err := filepath.Abs(filepath.Join(stateDir, "tilt", "runtime", name+"-supervisor.sh"))
	if err != nil {
		return "", fmt.Errorf("resolve exact tunnel supervisor path: %w", err)
	}
	return path, nil
}

func installTunnelSupervisor(supervisorPath string) error {
	dir := filepath.Dir(supervisorPath)
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return fmt.Errorf("create kube tunnel runtime dir: %w", err)
	}
	tempFile, err := os.CreateTemp(dir, filepath.Base(supervisorPath)+".*.tmp")
	if err != nil {
		return fmt.Errorf("create kube tunnel supervisor temp file: %w", err)
	}
	tempPath := tempFile.Name()
	defer os.Remove(tempPath)
	if _, err := tempFile.WriteString(kubeAPITunnelSupervisorScript); err != nil {
		_ = tempFile.Close()
		return fmt.Errorf("write kube tunnel supervisor: %w", err)
	}
	if err := tempFile.Chmod(0o700); err != nil {
		_ = tempFile.Close()
		return fmt.Errorf("chmod kube tunnel supervisor: %w", err)
	}
	if err := tempFile.Close(); err != nil {
		return fmt.Errorf("close kube tunnel supervisor: %w", err)
	}
	if err := os.Rename(tempPath, supervisorPath); err != nil {
		return fmt.Errorf("install kube tunnel supervisor: %w", err)
	}
	return nil
}

// TunnelSupervisorStatus is the live or terminal state of one local tunnel role.
type TunnelSupervisorStatus struct {
	Role           string
	PID            int
	State          string
	Attempts       int
	LastFailure    string
	ActionRequired bool
	childPID       int
}

func prepareTunnelStart(
	ctx context.Context,
	command []string,
) (prepared []string, childPIDPath, statePath string, release func(), err error) {
	prepared = append([]string(nil), command...)
	childPIDPath = supervisedTunnelChildPIDPath(prepared)
	if childPIDPath == "" {
		return prepared, "", "", func() {}, nil
	}
	supervisorPath := prepared[0]
	if err := installTunnelSupervisor(supervisorPath); err != nil {
		return nil, "", "", nil, err
	}
	release, err = acquireTunnelStartLock(supervisorPath)
	if err != nil {
		return nil, "", "", nil, err
	}
	if _, err := stopExactTunnelSupervisors(ctx, supervisorPath); err != nil {
		release()
		return nil, "", "", nil, err
	}
	childPIDPath, err = newTunnelChildPIDPath(supervisorPath)
	if err != nil {
		release()
		return nil, "", "", nil, err
	}
	prepared[1] = childPIDPath
	return prepared, childPIDPath, tunnelSupervisorStatePath(childPIDPath), release, nil
}

func acquireTunnelStartLock(supervisorPath string) (func(), error) {
	// Retain the lock file: unlinking a locked inode would permit a second lock.
	lockPath := supervisorPath + ".start.lock"
	lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open tunnel start lock: %w", err)
	}
	if err := lockTunnelStart(lockFile); err != nil {
		_ = lockFile.Close()
		return nil, fmt.Errorf("lock tunnel start: %w", err)
	}
	return func() {
		_ = unlockTunnelStart(lockFile)
		_ = lockFile.Close()
	}, nil
}

func newTunnelChildPIDPath(supervisorPath string) (string, error) {
	dir := filepath.Dir(supervisorPath)
	name := strings.TrimSuffix(filepath.Base(supervisorPath), "-supervisor.sh")
	temporary, err := os.CreateTemp(dir, name+"-child.*.pid")
	if err != nil {
		return "", fmt.Errorf("reserve unique tunnel child PID path: %w", err)
	}
	path := temporary.Name()
	if err := temporary.Close(); err != nil {
		_ = os.Remove(path)
		return "", fmt.Errorf("close unique tunnel child PID path: %w", err)
	}
	if err := os.Remove(path); err != nil {
		return "", fmt.Errorf("release unique tunnel child PID path: %w", err)
	}
	return path, nil
}

func tunnelSupervisorStatePath(childPIDPath string) string {
	return strings.TrimSuffix(childPIDPath, filepath.Ext(childPIDPath)) + ".state"
}

func waitForTunnelSupervisorIdentity(
	ctx context.Context,
	statePath string,
	pid int,
	waitCh <-chan error,
) error {
	deadline := time.NewTimer(2 * time.Second)
	defer deadline.Stop()
	ticker := time.NewTicker(10 * time.Millisecond)
	defer ticker.Stop()
	for {
		if state, err := readTunnelSupervisorState(statePath, ""); err == nil && state.PID == pid {
			return nil
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case err := <-waitCh:
			if err != nil {
				return fmt.Errorf("tunnel supervisor exited before publishing identity: %w", err)
			}
			return fmt.Errorf("tunnel supervisor exited before publishing identity")
		case <-deadline.C:
			return fmt.Errorf("tunnel supervisor %d did not publish identity at %s", pid, statePath)
		case <-ticker.C:
		}
	}
}

func stopExactTunnelSupervisors(ctx context.Context, supervisorPath string) ([]int, error) {
	pids, err := tunnelSupervisorPIDs(ctx, supervisorPath)
	if err != nil {
		return nil, err
	}
	for _, pid := range pids {
		process, findErr := os.FindProcess(pid)
		if findErr != nil {
			return nil, fmt.Errorf("find exact tunnel supervisor %d: %w", pid, findErr)
		}
		_ = process.Signal(os.Interrupt)
		if !waitForTunnelSupervisorStop(ctx, pid) {
			_ = killTunnelProcessGroup(pid)
			_ = process.Kill()
			if !waitForTunnelSupervisorStop(ctx, pid) {
				return nil, fmt.Errorf("exact tunnel supervisor %d did not stop", pid)
			}
		}
	}
	if err := stopExactTunnelChildren(ctx, supervisorPath); err != nil {
		return nil, err
	}
	if err := removeTunnelRuntimeFiles(supervisorPath); err != nil {
		return nil, err
	}
	return pids, nil
}

func stopExactTunnelChildren(ctx context.Context, supervisorPath string) error {
	dir := filepath.Dir(supervisorPath)
	name := strings.TrimSuffix(filepath.Base(supervisorPath), "-supervisor.sh")
	pidFiles, err := filepath.Glob(filepath.Join(dir, name+"-child*.pid"))
	if err != nil {
		return fmt.Errorf("find exact tunnel child PID files: %w", err)
	}
	for _, pidFile := range pidFiles {
		data, readErr := os.ReadFile(pidFile)
		if readErr != nil {
			if errors.Is(readErr, os.ErrNotExist) {
				continue
			}
			return fmt.Errorf("read exact tunnel child PID %s: %w", pidFile, readErr)
		}
		pid, parseErr := strconv.Atoi(strings.TrimSpace(string(data)))
		if parseErr != nil || pid <= 0 {
			return fmt.Errorf("parse exact tunnel child PID %q", strings.TrimSpace(string(data)))
		}
		command, commandErr := processCommand(ctx, pid)
		if errors.Is(commandErr, os.ErrProcessDone) {
			continue
		}
		if commandErr != nil {
			return fmt.Errorf("lookup exact tunnel child %d: %w", pid, commandErr)
		}
		state, stateErr := readTunnelSupervisorState(tunnelSupervisorStatePath(pidFile), "")
		if stateErr != nil || state.childPID != pid {
			return fmt.Errorf("exact tunnel child PID %d has no matching independent supervisor identity", pid)
		}
		if !strings.Contains(command, "ssh") || !strings.Contains(command, "-L") {
			return fmt.Errorf("exact tunnel child PID %d does not match ssh -L command %q", pid, command)
		}
		process, findErr := os.FindProcess(pid)
		if findErr != nil {
			return fmt.Errorf("find exact tunnel child %d: %w", pid, findErr)
		}
		_ = process.Signal(os.Interrupt)
		if !waitForTunnelSupervisorStop(ctx, pid) {
			_ = process.Kill()
			if !waitForTunnelSupervisorStop(ctx, pid) {
				return fmt.Errorf("exact tunnel child %d did not stop", pid)
			}
		}
	}
	return nil
}

func stopExactTunnelSupervisorsForRole(ctx context.Context, stateDir, name string) ([]int, error) {
	supervisorPath, err := tunnelSupervisorPath(stateDir, name)
	if err != nil {
		return nil, err
	}
	return stopExactTunnelSupervisors(ctx, supervisorPath)
}

func waitForTunnelSupervisorStop(ctx context.Context, pid int) bool {
	deadline := time.Now().Add(defaultStopGracePeriod)
	for time.Now().Before(deadline) {
		if _, err := processCommand(ctx, pid); errors.Is(err, os.ErrProcessDone) {
			return true
		}
		select {
		case <-ctx.Done():
			return false
		case <-time.After(50 * time.Millisecond):
		}
	}
	return false
}

func tunnelSupervisorPIDs(ctx context.Context, supervisorPath string) ([]int, error) {
	command := exec.CommandContext(ctx, "ps", "-ww", "-axo", "pid=,command=")
	output, err := command.Output()
	if err != nil {
		return nil, fmt.Errorf("list tunnel supervisors: %w", err)
	}
	var pids []int
	for line := range strings.SplitSeq(string(output), "\n") {
		line = strings.TrimSpace(line)
		parts := strings.Fields(line)
		if len(parts) < 2 {
			continue
		}
		pid, parseErr := strconv.Atoi(parts[0])
		if parseErr == nil && pid > 0 && pid != os.Getpid() && tunnelSupervisorProcessMatches(ctx, pid, line, supervisorPath) {
			pids = append(pids, pid)
		}
	}
	slices.Sort(pids)
	return pids, nil
}

func tunnelSupervisorProcessMatches(ctx context.Context, pid int, command, supervisorPath string) bool {
	if strings.Contains(command, supervisorPath) {
		return true
	}
	workingDirectory, err := os.Getwd()
	if err != nil {
		return false
	}
	relativePath, err := filepath.Rel(workingDirectory, supervisorPath)
	if err != nil || !strings.Contains(command, relativePath) {
		return false
	}
	processDirectory, err := processWorkingDirectory(ctx, pid)
	if err != nil {
		return false
	}
	processInfo, processErr := os.Stat(processDirectory)
	workingInfo, workingErr := os.Stat(workingDirectory)
	return processErr == nil && workingErr == nil && os.SameFile(processInfo, workingInfo)
}

func processWorkingDirectory(ctx context.Context, pid int) (string, error) {
	if directory, err := os.Readlink(fmt.Sprintf("/proc/%d/cwd", pid)); err == nil {
		return directory, nil
	}
	command := exec.CommandContext( // #nosec G204 -- pid is a validated integer argument.
		ctx,
		"lsof",
		"-a",
		"-p",
		strconv.Itoa(pid),
		"-d",
		"cwd",
		"-Fn",
	)
	output, err := command.Output()
	if err != nil {
		return "", fmt.Errorf("read process %d working directory: %w", pid, err)
	}
	for line := range strings.SplitSeq(string(output), "\n") {
		if directory, ok := strings.CutPrefix(line, "n"); ok {
			return directory, nil
		}
	}
	return "", fmt.Errorf("process %d working directory is missing", pid)
}

func removeTunnelRuntimeFiles(supervisorPath string) error {
	dir := filepath.Dir(supervisorPath)
	name := strings.TrimSuffix(filepath.Base(supervisorPath), "-supervisor.sh")
	for _, pattern := range []string{name + "-child*.pid", name + "-child*.state"} {
		matches, err := filepath.Glob(filepath.Join(dir, pattern))
		if err != nil {
			return fmt.Errorf("find tunnel runtime files: %w", err)
		}
		for _, match := range matches {
			if err := os.Remove(match); err != nil && !errors.Is(err, os.ErrNotExist) {
				return fmt.Errorf("remove tunnel runtime file %s: %w", match, err)
			}
		}
	}
	return nil
}

// ObserveTunnelSupervisors reports exact-session controller and kube-API tunnel supervisors.
func ObserveTunnelSupervisors(ctx context.Context, stateDir string) []TunnelSupervisorStatus {
	var statuses []TunnelSupervisorStatus
	for _, role := range []struct {
		name  string
		label string
	}{{"controller-tunnel", "controller"}, {"kube-api-tunnel", "kube-api"}} {
		supervisorPath, err := tunnelSupervisorPath(stateDir, role.name)
		if err != nil {
			continue
		}
		pids, _ := tunnelSupervisorPIDs(ctx, supervisorPath)
		live := make(map[int]bool, len(pids))
		for _, pid := range pids {
			live[pid] = true
		}
		byPID := map[int]TunnelSupervisorStatus{}
		stateFiles, _ := filepath.Glob(filepath.Join(filepath.Dir(supervisorPath), role.name+"-child*.state"))
		for _, stateFile := range stateFiles {
			state, err := readTunnelSupervisorState(stateFile, role.label)
			if err != nil || state.PID <= 0 {
				continue
			}
			if !live[state.PID] && state.State != "quiescent" {
				state.State = "stopped"
				state.ActionRequired = true
				if state.LastFailure == "" {
					state.LastFailure = "supervisor process is missing"
				}
			}
			byPID[state.PID] = state
		}
		for _, pid := range pids {
			if _, ok := byPID[pid]; !ok {
				byPID[pid] = TunnelSupervisorStatus{Role: role.label, PID: pid, State: "running"}
			}
		}
		if len(pids) > 1 {
			for _, pid := range pids {
				state := byPID[pid]
				state.State = "duplicate"
				state.ActionRequired = true
				state.LastFailure = "multiple exact-session supervisors are running"
				byPID[pid] = state
			}
		}
		for _, state := range byPID {
			statuses = append(statuses, state)
		}
	}
	slices.SortFunc(statuses, func(a, b TunnelSupervisorStatus) int {
		if a.Role == b.Role {
			return a.PID - b.PID
		}
		return strings.Compare(a.Role, b.Role)
	})
	return statuses
}

func readTunnelSupervisorState(path, role string) (TunnelSupervisorStatus, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return TunnelSupervisorStatus{}, err
	}
	values := map[string]string{}
	for line := range strings.SplitSeq(string(data), "\n") {
		key, value, ok := strings.Cut(strings.TrimSpace(line), "=")
		if ok {
			values[key] = value
		}
	}
	pid, err := strconv.Atoi(values["supervisor_pid"])
	if err != nil || pid <= 0 {
		return TunnelSupervisorStatus{}, fmt.Errorf("invalid tunnel supervisor PID %q", values["supervisor_pid"])
	}
	attempts, _ := strconv.Atoi(values["attempts"])
	childPID, _ := strconv.Atoi(values["child_pid"])
	lastFailure := ""
	if status := strings.TrimSpace(values["last_exit_status"]); status != "" {
		lastFailure = "exit status " + status
		if at := strings.TrimSpace(values["last_failure_at"]); at != "" {
			lastFailure += " at " + at
		}
	}
	return TunnelSupervisorStatus{
		Role:           role,
		PID:            pid,
		State:          values["state"],
		Attempts:       attempts,
		LastFailure:    lastFailure,
		ActionRequired: values["action_required"] == "true",
		childPID:       childPID,
	}, nil
}

// StopStartedProcess terminates a started process. Supervised tunnels own a
// dedicated process group, so error cleanup cannot orphan the SSH child even
// when the shell supervisor itself no longer handles signals.
func StopStartedProcess(started *StartedProcess) {
	if started == nil || started.PID <= 0 {
		return
	}
	if started.ChildPIDPath != "" {
		_ = killTunnelProcessGroup(started.PID)
		_ = os.Remove(started.ChildPIDPath)
		_ = os.Remove(tunnelSupervisorStatePath(started.ChildPIDPath))
		return
	}
	process, err := os.FindProcess(started.PID)
	if err == nil {
		_ = process.Kill()
	}
}
