//go:build !windows

package infra

import (
	"strconv"
	"strings"
)

// ResolvePaneIDs crosses the PID of each Claude process with the panes of the
// given isolated tmux socket (tmux -L <socket> list-panes -a) and returns a
// claudePID -> pane_id ("%N") map.
//
// The pane_id is STABLE for the pane's whole life: it identifies a Claude even
// when its window_id (@N) migrates with the focus swap (break-pane + join-pane
// move the pane between windows). That is why it is the correct stable handle
// for an agent, as opposed to the window_id which changes by design.
//
// For each claudePID it climbs the process tree (PPID in /proc/<pid>/stat) until
// it finds a PID that is a pane_pid of the socket; that pane is the one hosting
// the Claude. Normally the pane_pid IS the claudePID because the pane runs
// `exec claude`, but a shell that launched claude as a child is covered by the
// ascent.
//
// Best-effort and crash-free: an empty socket, no PIDs, or a tmux failure
// (socket down, tmux absent) all yield an empty map; a Claude with no resolvable
// pane is simply omitted from the result (callers degrade it to ""). It reads
// /proc, hence the //go:build !windows tag.
func ResolvePaneIDs(socket string, claudePIDs []int) map[int]string {
	if socket == "" || len(claudePIDs) == 0 {
		return map[int]string{}
	}
	out, _, err := runTmux(socket, "list-panes", "-a", "-F", "#{pane_pid} #{pane_id}")
	if err != nil {
		return map[int]string{}
	}
	return resolvePaneIDsFrom(out, procPPID, claudePIDs)
}

// resolvePaneIDsFrom is the testable core of ResolvePaneIDs: it parses the
// `<pane_pid> <pane_id>` lines produced by tmux and, for each claudePID, climbs
// the process tree via ppidOf until it lands on a pane_pid, returning that
// pane's pane_id. ppidOf is injected so the ascent can be tested without real
// processes. Lines that do not parse are skipped; a PID with no pane ancestor is
// omitted.
func resolvePaneIDsFrom(tmuxOut string, ppidOf func(int) int, claudePIDs []int) map[int]string {
	panePaneID := map[int]string{}
	for _, line := range strings.Split(strings.TrimSpace(tmuxOut), "\n") {
		f := strings.Fields(strings.TrimSpace(line))
		if len(f) < 2 {
			continue
		}
		pp, e := strconv.Atoi(f[0])
		if e != nil {
			continue
		}
		panePaneID[pp] = f[1]
	}

	res := make(map[int]string, len(claudePIDs))
	for _, pid := range claudePIDs {
		if paneID, ok := paneAncestor(pid, panePaneID, ppidOf); ok {
			res[pid] = paneID
		}
	}
	return res
}

// paneAncestor climbs the process tree from pid until it finds a PID that is a
// pane_pid (a key of panePaneID), returning its pane_id. The ascent is bounded
// (64 hops, stop at pid<=1) so a malformed /proc or a cycle cannot hang it.
// Returns ("", false) when no ancestor is a pane. ppidOf is injected for tests.
func paneAncestor(pid int, panePaneID map[int]string, ppidOf func(int) int) (string, bool) {
	for i := 0; pid > 1 && i < 64; i++ {
		if paneID, ok := panePaneID[pid]; ok {
			return paneID, true
		}
		pid = ppidOf(pid)
	}
	return "", false
}
