package proc

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"
)

// ProcessInfo captures native process metadata used to identify process
// lifetimes and manage process trees.
type ProcessInfo struct {
	PID       int
	PPID      int
	PGID      int
	Command   string
	CWD       string
	StartedAt time.Time

	// StartID is an optional platform-native process-start identity.
	StartID string
}

// ProcessIdentityMatches reports whether a and b identify the same process
// lifetime. Unknown start times fail closed so PID reuse cannot be mistaken for
// the original process.
func ProcessIdentityMatches(a, b ProcessInfo) bool {
	if a.PID <= 0 ||
		a.PID != b.PID ||
		a.StartedAt.IsZero() ||
		b.StartedAt.IsZero() {
		return false
	}
	if a.StartID != "" && b.StartID != "" {
		return a.StartID == b.StartID
	}
	// Exact timestamps preserve compatibility with identities persisted before
	// a platform supplied StartID.
	return a.StartedAt.Equal(b.StartedAt)
}

// ListProcesses runs ps to enumerate running processes with a stable creation
// timestamp. Callers that use the result for destructive ownership decisions
// must fail closed when StartedAt is zero.
func ListProcesses() []ProcessInfo {
	out, err := exec.Command("ps", "-axo", "pid=,ppid=,pgid=,lstart=,command=").Output()
	if err != nil {
		out, err = exec.Command("ps", "-eo", "pid=,ppid=,pgid=,lstart=,args=").Output()
	}
	if err != nil {
		return nil
	}
	return parseProcessList(out)
}

func parseProcessList(out []byte) []ProcessInfo {
	var processes []ProcessInfo
	for _, raw := range bytes.Split(out, []byte("\n")) {
		line := strings.TrimSpace(string(raw))
		if line == "" {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) < 9 {
			continue
		}
		pid, err1 := strconv.Atoi(fields[0])
		ppid, err2 := strconv.Atoi(fields[1])
		pgid, err3 := strconv.Atoi(fields[2])
		if err1 != nil || err2 != nil || err3 != nil {
			continue
		}
		startedAt, err := time.ParseInLocation(
			"Mon Jan _2 15:04:05 2006",
			strings.Join(fields[3:8], " "),
			time.Local,
		)
		if err != nil {
			continue
		}
		processes = append(processes, ProcessInfo{
			PID:       pid,
			PPID:      ppid,
			PGID:      pgid,
			Command:   strings.Join(fields[8:], " "),
			StartedAt: startedAt,
		})
	}
	return processes
}

// CollectDescendantPIDs walks the process tree under rootPID and returns all
// descendant PIDs (not including rootPID itself).
func CollectDescendantPIDs(rootPID int, processes []ProcessInfo) []int {
	return collectDescendantPIDs(rootPID, processes)
}

func collectDescendantPIDs(rootPID int, processes []ProcessInfo) []int {
	children := make(map[int][]int)
	for _, proc := range processes {
		children[proc.PPID] = append(children[proc.PPID], proc.PID)
	}
	var result []int
	var visit func(int)
	visit = func(pid int) {
		for _, child := range children[pid] {
			visit(child)
			result = append(result, child)
		}
	}
	visit(rootPID)
	return result
}

// CommandLine returns the command line for pid via /proc or ps.
func CommandLine(pid int) string {
	if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)); err == nil {
		return strings.TrimSpace(string(bytes.ReplaceAll(data, []byte{0}, []byte{' '})))
	}
	out, err := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "command=").Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

// ProcessCWD returns the current working directory of pid, or "" if unknown.
func ProcessCWD(pid int) string {
	if cwd, err := os.Readlink(fmt.Sprintf("/proc/%d/cwd", pid)); err == nil {
		return cwd
	}
	if _, err := exec.LookPath("lsof"); err == nil {
		out, err := exec.Command("lsof", "-a", "-p", strconv.Itoa(pid), "-d", "cwd", "-Fn").Output()
		if err == nil {
			for _, line := range strings.Split(string(out), "\n") {
				if strings.HasPrefix(line, "n") {
					return strings.TrimPrefix(line, "n")
				}
			}
		}
	}
	return ""
}

// ProcessByPID returns the current process identity for pid. The result is
// intentionally sourced from the same snapshot shape as ListProcesses so
// callers can compare persisted start identity before destructive cleanup.
func ProcessByPID(pid int) (ProcessInfo, bool) {
	return processByPIDWithFallback(
		pid,
		processByPIDNative,
		nativeProcessIdentityRequired(),
		ListProcesses,
	)
}

func nativeProcessIdentityRequired() bool {
	switch runtime.GOOS {
	case "darwin", "linux", "windows":
		return true
	default:
		return false
	}
}

func processByPIDWithFallback(
	pid int,
	native func(int) (ProcessInfo, bool),
	nativeRequired bool,
	listProcesses func() []ProcessInfo,
) (ProcessInfo, bool) {
	if pid <= 0 {
		return ProcessInfo{}, false
	}
	if process, ok := native(pid); ok {
		process.CWD = ProcessCWD(pid)
		return process, true
	}
	if nativeRequired {
		return ProcessInfo{}, false
	}
	for _, process := range listProcesses() {
		if process.PID != pid {
			continue
		}
		process.CWD = ProcessCWD(pid)
		return process, true
	}
	return ProcessInfo{}, false
}

// PIDIsZombie reports whether pid is in zombie state.
func PIDIsZombie(pid int) bool {
	if pid <= 0 {
		return false
	}
	out, err := exec.Command("ps", "-o", "state=", "-p", strconv.Itoa(pid)).Output()
	if err != nil {
		return false
	}
	state := strings.TrimSpace(string(out))
	return strings.HasPrefix(state, "Z")
}

// RepoDevProcessTargets returns dev processes (agentteam-server/vite/etc.)
// whose command line indicates they belong to repoRoot.
func RepoDevProcessTargets(repoRoot string, processes []ProcessInfo) []ProcessInfo {
	var targets []ProcessInfo
	seen := map[int]bool{}
	for _, proc := range processes {
		if !repoDevProcessMatches(repoRoot, proc) {
			continue
		}
		if seen[proc.PID] {
			continue
		}
		seen[proc.PID] = true
		targets = append(targets, proc)
	}
	return targets
}

func repoDevProcessMatches(repoRoot string, proc ProcessInfo) bool {
	cmd := proc.Command
	binDir := filepath.Join(repoRoot, ".data", "dev")
	webDir := filepath.Join(repoRoot, "web")

	if strings.Contains(cmd, filepath.Join(binDir, "agentteam-server")) {
		return true
	}

	matchesWebDev := strings.Contains(cmd, "npm run dev") ||
		strings.Contains(cmd, "bun run dev") ||
		strings.Contains(cmd, filepath.Join(webDir, "node_modules", ".bin", "vite")) ||
		strings.Contains(cmd, filepath.Join(webDir, "node_modules", "vite", "bin", "vite.js"))
	if !matchesWebDev {
		return false
	}
	cwd := ProcessCWD(proc.PID)
	if cwd == "" {
		return strings.Contains(cmd, repoRoot)
	}
	return cwd == repoRoot || cwd == webDir || strings.HasPrefix(cwd, filepath.Join(repoRoot, ".data"))
}
