#!/usr/bin/env bash
# fleet_send_text — empuja texto a UN agente de la flota tmux de forma fiable.
#
# El problema que resuelve: el orquestador "nudgea" a los ejecutores con
# `tmux send-keys`. El patron antiguo apuntaba al `window_id` (`@N`) leido del
# JSON de la flota. Pero el focus-swap de FleetView (`break-pane` + `join-pane`)
# RECREA windows, asi que el `@N` de un agente MIGRA (p.ej. `@32` -> `@34`) cada
# vez que se entra/sale de su window. Enviar al `@N` viejo (cacheado por el bloque
# FLEET-STATE o leido un instante antes) manda el texto al window equivocado o a
# otro agente -> "a veces no llega al agente correcto". Ademas, mandar el texto y
# el `Enter` en la MISMA invocacion hace que el TUI de Claude Code a veces no
# interprete el Enter como submit.
#
# Esta funcion arregla las dos cosas:
#   1. Resuelve el `pane_id` ESTABLE (`%N`) FRESCO justo antes de enviar. El
#      `pane_id` se preserva durante toda la vida del pane aunque el pane se mueva
#      de window con break/join — NO migra como el `window_id`. La resolucion va
#      sessionId -> PID (sessions/<PID>.json) -> el pane cuyo proceso (o un
#      ancestro suyo en /proc) es `pane_pid`, leyendo `tmux list-panes -a` en el
#      momento del envio.
#   2. Manda el texto literal (`send-keys -l`), espera un poco, y el `Enter` en
#      una invocacion SEPARADA. Verifica con `capture-pane` que el texto aparecio
#      en el pane antes de pulsar Enter; si no, reintenta.
#
# Guards: NO envia a tu propio pane (la sesion que invoca la funcion). Error claro
# si el sessionId/PID no resuelve a un pane vivo.
#
# Funcion IMPURA: inyecta teclas en un pane tmux ajeno. Por defecto EJECUTA (es el
# caso de uso del bot: nudgear a un ejecutor). Usa --dry-run para ver el plan sin
# enviar nada.
#
# Overrides de entorno (testabilidad, no para uso normal):
#   FN_FLEET_SESSIONS_DIR  directorio de los sessions JSON. Default ~/.claude/sessions
#   FN_FLEET_SELF_PID      fuerza el PID propio (salta la deteccion por /proc)
set -euo pipefail
IFS=$' \t\n'

# Resuelve el pane_id (%N) ESTABLE de un PID dado, leyendo el mapa fresco de panes
# del socket. Sube por la cadena de ancestros del PID en /proc hasta encontrar un
# `pane_pid` del mapa: cubre tanto el caso `exec claude` (pane_pid == claude pid,
# match directo) como el de un claude lanzado bajo un shell (pane_pid == shell
# ancestro). Imprime el pane_id y devuelve 0 si lo encuentra; 1 si no.
#   $1 = PID objetivo
#   $2 = texto del mapa "pane_pid pane_id" (una linea por pane)
_fleet_resolve_pane_for_pid() {
    local p="${1:-}" panes_map="${2:-}" guard=0 pane_id
    while [[ -n "$p" && "$p" != "0" && "$p" != "1" ]]; do
        pane_id="$(awk -v pp="$p" '$1==pp {print $2; exit}' <<<"$panes_map")"
        if [[ -n "$pane_id" ]]; then
            printf '%s\n' "$pane_id"
            return 0
        fi
        p="$(awk '{print $4}' "/proc/$p/stat" 2>/dev/null || true)"
        guard=$((guard + 1))
        [[ "$guard" -gt 64 ]] && break
    done
    return 1
}

fleet_send_text() {
    local target="" txt="" socket="" do_enter=1 dry=0 retries=2
    local got_target=0 got_text=0

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --socket)   shift; socket="${1:-}" ;;
            --no-enter) do_enter=0 ;;
            --retries)  shift; retries="${1:-2}" ;;
            --dry-run)  dry=1 ;;
            -h|--help)
                cat <<'USAGE'
Uso: fleet_send_text <sessionId|PID> "<texto>" [--socket <s>] [--no-enter] [--retries N] [--dry-run]

Empuja <texto> a UN agente de la flota tmux resolviendo su pane_id (%N) ESTABLE
FRESCO justo antes de enviar (no cachea el window_id @N, que migra con el
focus-swap). Manda el texto literal y el Enter en invocaciones separadas, y
verifica con capture-pane que el texto aparecio antes de pulsar Enter;
reintenta si no.

Argumentos:
  <sessionId|PID>  Primer posicional: sessionId del agente (exacto o prefijo) o
                   su PID (todo digitos). Por sessionId se busca en
                   sessions/*.json el que case; su archivo (<pid>.json) da el PID.
  "<texto>"        Segundo posicional: el texto a inyectar en el input del agente.

Opciones:
  --socket <s>   Socket tmux del perfil FleetView. Default: $FLEET_SOCKET, o "fleet".
  --no-enter     Deja el texto en el input sin pulsar Enter (no hace submit).
  --retries N    Reintentos si el texto no aparece tras el send (default 2).
  --dry-run      Imprime el plan (PID, sessionId, pane, socket) y NO envia nada.
  -h, --help     Esta ayuda.

Salida: linea de resultado con `pane=%N` usado e `intento=N`. Exit 0 ok/dry-run;
2 uso incorrecto o target no resuelto; 3 guard (target es la sesion actual);
4 no se encontro pane vivo para el target; 5 enviado pero no verificado tras los
reintentos.

Ejemplos:
  fleet_send_text 32945650-a4e1-472b-90c9-5b38ef60a463 "Cierra tu DoD o reporta el bloqueo." --socket "$FLEET_SOCKET"
  fleet_send_text 32945650 "Falta el error path con evidencia."     # por prefijo de sessionId
  fleet_send_text 48213 "texto" --no-enter --dry-run                # por PID, solo ver el plan
USAGE
                return 0 ;;
            --*)
                echo "fleet_send_text: opcion desconocida '$1' (usa -h)" >&2
                return 2 ;;
            *)
                if [[ "$got_target" -eq 0 ]]; then
                    target="$1"; got_target=1
                elif [[ "$got_text" -eq 0 ]]; then
                    txt="$1"; got_text=1
                else
                    echo "fleet_send_text: argumento extra '$1' (target y texto ya fijados)" >&2
                    return 2
                fi ;;
        esac
        shift
    done

    [[ "$got_target" -eq 0 ]] && {
        echo "fleet_send_text: falta el target (sessionId o PID). Usa -h." >&2
        return 2
    }
    [[ "$got_text" -eq 0 ]] && {
        echo "fleet_send_text: falta el texto a enviar. Usa -h." >&2
        return 2
    }
    [[ "$retries" =~ ^[0-9]+$ ]] || {
        echo "fleet_send_text: --retries debe ser un entero (recibido '$retries')" >&2
        return 2
    }

    local sessions_dir="${FN_FLEET_SESSIONS_DIR:-$HOME/.claude/sessions}"
    [[ -z "$socket" ]] && socket="${FLEET_SOCKET:-fleet}"

    command -v tmux >/dev/null 2>&1 || {
        echo "fleet_send_text: tmux no esta instalado" >&2
        return 1
    }

    # -----------------------------------------------------------------------
    # Resolver (PID, sessionId) a partir del target. Mismo patron que
    # kill_fleet_agent: por PID directo, o por sessionId (exacto/prefijo)
    # buscando en sessions/*.json.
    # -----------------------------------------------------------------------
    local pid="" sid=""
    if [[ "$target" =~ ^[0-9]+$ ]]; then
        pid="$target"
        local sfile="$sessions_dir/$pid.json"
        if [[ -f "$sfile" ]] && command -v jq >/dev/null 2>&1; then
            sid="$(jq -r '.sessionId // ""' "$sfile" 2>/dev/null || true)"
        fi
    else
        command -v jq >/dev/null 2>&1 || {
            echo "fleet_send_text: jq no esta instalado (necesario para resolver el sessionId)" >&2
            return 1
        }
        local f base candidate_sid
        for f in "$sessions_dir"/*.json; do
            [[ -f "$f" ]] || continue
            candidate_sid="$(jq -r '.sessionId // ""' "$f" 2>/dev/null || true)"
            [[ -z "$candidate_sid" ]] && continue
            if [[ "$candidate_sid" == "$target" || "$candidate_sid" == "$target"* ]]; then
                base="$(basename "$f" .json)"
                pid="$base"
                sid="$candidate_sid"
                break
            fi
        done
    fi

    [[ -z "$pid" ]] && {
        echo "fleet_send_text: no se pudo resolver el target '$target' a un PID (sessions en $sessions_dir)" >&2
        return 2
    }

    # -----------------------------------------------------------------------
    # Guard — anti-self: no enviar a la sesion que invoca la funcion.
    # -----------------------------------------------------------------------
    local self_pid="${FN_FLEET_SELF_PID:-}"
    if [[ -z "$self_pid" ]]; then
        local walk="$$" guard=0 comm
        while [[ -n "$walk" && "$walk" != "0" && "$walk" != "1" ]]; do
            comm="$(cat "/proc/$walk/comm" 2>/dev/null || true)"
            if [[ "$comm" == "claude" ]]; then
                self_pid="$walk"
                break
            fi
            walk="$(awk '{print $4}' "/proc/$walk/stat" 2>/dev/null || true)"
            guard=$((guard + 1))
            [[ "$guard" -gt 64 ]] && break
        done
    fi
    if [[ -n "$self_pid" && "$pid" == "$self_pid" ]]; then
        echo "fleet_send_text: REHUSADO — el target (PID $pid) es la sesion actual. No me autoenvio." >&2
        return 3
    fi

    # -----------------------------------------------------------------------
    # Resolver el pane_id (%N) ESTABLE FRESCO. Mapa pane_pid->pane_id del socket
    # leido AHORA; subir por ancestros del PID hasta casar un pane_pid.
    # -----------------------------------------------------------------------
    local panes_map pane=""
    panes_map="$(tmux -L "$socket" list-panes -a -F '#{pane_pid} #{pane_id}' 2>/dev/null || true)"
    if [[ -n "$panes_map" ]]; then
        pane="$(_fleet_resolve_pane_for_pid "$pid" "$panes_map" || true)"
    fi

    [[ -z "$pane" ]] && {
        echo "fleet_send_text: no se encontro un pane vivo para el target '$target' (PID $pid) en el socket '$socket'." >&2
        return 4
    }

    # -----------------------------------------------------------------------
    # Plan.
    # -----------------------------------------------------------------------
    local enter_desc; [[ "$do_enter" -eq 1 ]] && enter_desc="texto + Enter separado" || enter_desc="solo texto (--no-enter)"
    echo "fleet_send_text — target: $target  PID: $pid  sessionId: ${sid:-?}  socket: $socket  pane: $pane  envio: $enter_desc  retries: $retries"

    if [[ "$dry" -eq 1 ]]; then
        echo "DRY-RUN: no se ha enviado nada."
        echo "pane=$pane intento=0 status=dry-run"
        return 0
    fi

    # -----------------------------------------------------------------------
    # Enviar + verificar. El texto se manda literal (-l); el Enter va en una
    # invocacion separada tras un sleep. Verificamos ANTES del Enter (el texto
    # esta en el input; tras Enter el TUI vacia el input y no se podria verificar).
    # Si el texto no aparece, limpiamos el input (C-u) y reintentamos.
    # -----------------------------------------------------------------------
    local anchor="${txt:0:24}"          # fragmento ancla (evita falsos negativos por wrapping)
    local i cap ok=0 used_try=0
    for (( i=1; i<=retries+1; i++ )); do
        tmux -L "$socket" send-keys -t "$pane" -l -- "$txt" 2>/dev/null || true
        sleep 0.3
        cap="$(tmux -L "$socket" capture-pane -p -t "$pane" 2>/dev/null || true)"
        if grep -qF -- "$anchor" <<<"$cap"; then
            ok=1; used_try="$i"
            break
        fi
        # No aparecio: limpiar el input antes de reintentar.
        tmux -L "$socket" send-keys -t "$pane" C-u 2>/dev/null || true
        sleep 0.2
    done

    if [[ "$ok" -ne 1 ]]; then
        echo "fleet_send_text: texto enviado pero NO verificado en el pane $pane tras $((retries+1)) intentos." >&2
        echo "pane=$pane intento=$((retries+1)) status=unverified" >&2
        return 5
    fi

    # Texto presente en el input. Ahora el Enter (separado) para hacer submit.
    if [[ "$do_enter" -eq 1 ]]; then
        tmux -L "$socket" send-keys -t "$pane" Enter 2>/dev/null || true
    fi

    echo "fleet_send_text: OK — texto inyectado en el pane $pane (intento $used_try)$([[ "$do_enter" -eq 1 ]] && echo " + Enter")."
    echo "pane=$pane intento=$used_try status=ok"
    return 0
}

# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    fleet_send_text "$@"
fi
