//go:build !windows

package infra

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

// TmuxMapClaudePanes devuelve un mapa claudePID -> window_id de todos los panes
// del socket cuyo proceso de pane (o algun descendiente directo) sea un proceso
// `claude`. Permite a la TUI saber que Claude de su lista ya vive en la sesion
// fleet (y por tanto es conmutable) y en que window.
//
// Como cada pane que corre Claude lo hace con `exec claude ...`, el #{pane_pid}
// del pane normalmente ES el PID de claude (comm == "claude"). Por robustez, si
// el propio pane_pid no es claude (p.ej. un shell que lanzo claude como hijo),
// se recorren sus descendientes directos buscando el primer comm == "claude".
// Si no se encuentra claude bajo un pane, ese pane se omite.
//
// Opera SIEMPRE sobre el socket aislado pasado como parametro (tmux -L <socket>)
// y lee /proc (no portable a Windows; de ahi el build tag //go:build !windows).
func TmuxMapClaudePanes(socket string) (map[int]string, error) {
	if socket == "" {
		return nil, fmt.Errorf("tmux_map_claude_panes: socket vacio")
	}

	out, stderr, err := runTmux(socket, "list-panes", "-a", "-F", "#{pane_pid} #{window_id}")
	if err != nil {
		return nil, fmt.Errorf("tmux_map_claude_panes: list-panes -a: %w (%s)", err, stderr)
	}

	result := make(map[int]string)
	for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}
		panePID, convErr := strconv.Atoi(fields[0])
		if convErr != nil {
			continue
		}
		windowID := fields[1]

		claudePID, ok := findClaudePID(panePID)
		if !ok {
			continue // no hay claude bajo este pane
		}
		result[claudePID] = windowID
	}
	return result, nil
}

// findClaudePID devuelve el PID de un proceso `claude` que sea el propio pid o
// un hijo directo suyo. Devuelve (pid, true) si lo encuentra; (0, false) si no.
func findClaudePID(pid int) (int, bool) {
	if procComm(pid) == "claude" {
		return pid, true
	}
	for _, child := range procChildren(pid) {
		if procComm(child) == "claude" {
			return child, true
		}
	}
	return 0, false
}

// procComm lee el nombre del comando (comm) de /proc/<pid>/comm. Devuelve ""
// si el proceso no existe o no se puede leer.
func procComm(pid int) string {
	data, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

// procChildren devuelve los PIDs de los hijos DIRECTOS de <pid>. Intenta primero
// /proc/<pid>/task/<pid>/children (rapido, requiere CONFIG_PROC_CHILDREN); si no
// esta disponible, cae a escanear /proc/*/stat por PPID (campo 4).
func procChildren(pid int) []int {
	if kids := procChildrenFromTask(pid); kids != nil {
		return kids
	}
	return procChildrenFromScan(pid)
}

// procChildrenFromTask agrega /proc/<pid>/task/<tid>/children sobre TODOS los
// hilos (tasks) del proceso. Cada `children` lista solo los hijos parenteados
// a ESE task, asi que un proceso multihilo (un shell que hizo fork desde un
// hilo no principal, o el propio test runner de Go) puede tener hijos repartidos
// entre varios tasks. Devuelve nil si el directorio task/ no existe o ningun
// task expone `children` (kernel sin CONFIG_PROC_CHILDREN), para que el caller
// use el fallback de scan por PPID.
func procChildrenFromTask(pid int) []int {
	taskDir := fmt.Sprintf("/proc/%d/task", pid)
	tasks, err := os.ReadDir(taskDir)
	if err != nil {
		return nil
	}
	var kids []int
	supported := false
	for _, task := range tasks {
		tid := task.Name()
		data, err := os.ReadFile(filepath.Join(taskDir, tid, "children"))
		if err != nil {
			continue // este task no expone children; probar el resto
		}
		supported = true
		for _, tok := range strings.Fields(string(data)) {
			if k, err := strconv.Atoi(tok); err == nil {
				kids = append(kids, k)
			}
		}
	}
	if !supported {
		return nil // kernel sin CONFIG_PROC_CHILDREN -> fallback a scan
	}
	// Distinguir "sin hijos" (slice vacio no-nil) de "sin soporte" (nil arriba).
	if kids == nil {
		return []int{}
	}
	return kids
}

// procChildrenFromScan escanea /proc/*/stat buscando procesos cuyo PPID (campo
// 4 de stat, indice 1 tras el comm entre parentesis) sea <pid>.
func procChildrenFromScan(parent int) []int {
	entries, err := os.ReadDir("/proc")
	if err != nil {
		return nil
	}
	var kids []int
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		childPID, err := strconv.Atoi(e.Name())
		if err != nil {
			continue // no es un directorio de PID
		}
		if procPPID(childPID) == parent {
			kids = append(kids, childPID)
		}
	}
	return kids
}

// procPPID extrae el PPID (campo 4 de /proc/<pid>/stat). El comm (campo 2) va
// entre parentesis y puede contener espacios y ')', asi que se parsea tomando
// lo que hay tras el ULTIMO ')'. Tras el comm, los campos son: state(0) ppid(1)
// pgrp(2)... -> el PPID es el indice 1 de ese resto.
func procPPID(pid int) int {
	data, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "stat"))
	if err != nil {
		return -1
	}
	s := string(data)
	close := strings.LastIndex(s, ")")
	if close < 0 {
		return -1
	}
	rest := strings.Fields(s[close+1:])
	const ppidIdx = 1 // state=rest[0], ppid=rest[1]
	if len(rest) <= ppidIdx {
		return -1
	}
	ppid, err := strconv.Atoi(rest[ppidIdx])
	if err != nil {
		return -1
	}
	return ppid
}
