"""Resumen de una linea de las transiciones accionables de la flota.

Toma el dict `by_classification` que produce `drain_fleet_events` y lo condensa
en una sola linea inyectable por un hook `UserPromptSubmit` al orquestador-Claude
de flota. Funcion pura, determinista y defensiva ante datos mal formados.
"""

# Categorias accionables (clave del dict -> etiqueta de salida), en orden de render.
_ACTIONABLE: list[tuple[str, str]] = [
    ("DICE_TERMINADO", "terminados"),
    ("RECLAMA", "reclaman"),
    ("ESTANCADO", "estancados"),
]

_GOAL_MAXLEN = 40
_SID_LEN = 8
_NO_SID = "????????"
_NO_GOAL = "(sin objetivo)"


def _short_sid(session_id) -> str:
    """Primeros 8 caracteres del session_id, o `????????` si falta/vacio."""
    if not isinstance(session_id, str) or session_id == "":
        return _NO_SID
    return session_id[:_SID_LEN]


def _trunc_goal(goal) -> str:
    """Goal truncado a 40 caracteres con elipsis, o `(sin objetivo)` si falta."""
    if not isinstance(goal, str) or goal == "":
        return _NO_GOAL
    if len(goal) > _GOAL_MAXLEN:
        return goal[:_GOAL_MAXLEN] + "…"
    return goal


def _dedup_events(raw) -> list[dict]:
    """Filtra a dicts validos y deduplica por session_id quedandose con la ultima aparicion.

    Mantiene el orden de la ultima aparicion de cada session_id. Eventos sin
    session_id valido (None/vacio/ausente) NO se deduplican entre si: cada uno
    cuenta como entrada propia.
    """
    if not isinstance(raw, list):
        return []

    ordered: list[dict] = []
    index_by_sid: dict[str, int] = {}
    for ev in raw:
        if not isinstance(ev, dict):
            continue
        sid = ev.get("session_id")
        if isinstance(sid, str) and sid != "":
            if sid in index_by_sid:
                # Reemplaza la entrada previa por la mas reciente, conservando
                # la posicion de la ultima aparicion.
                ordered[index_by_sid[sid]] = None  # type: ignore[call-overload]
            index_by_sid[sid] = len(ordered)
            ordered.append(ev)
        else:
            ordered.append(ev)

    return [ev for ev in ordered if ev is not None]


def summarize_fleet_transitions(by_classification: dict, max_items: int = 6) -> str:
    """Resume en una linea las transiciones accionables de la flota para el orquestador.

    Args:
        by_classification: dict agrupado por campo `to` (salida de drain_fleet_events).
            Solo se consideran las claves accionables DICE_TERMINADO, RECLAMA y
            ESTANCADO; el resto (TRABAJANDO, GONE, MAL_LANZADO) se ignora.
        max_items: limite total de eventos mostrados sumando las tres categorias.
            Si se supera, se truncan los sobrantes y se añade ` (+N mas)`. El
            recorte respeta el orden de categorias terminados -> reclaman ->
            estancados (se llenan en ese orden hasta agotar el cupo).

    Returns:
        Una cadena de una sola linea. `FLEET-STATE: sin cambios` si no hay nada
        accionable; en caso contrario el bloque con las categorias no vacias.
    """
    if not isinstance(by_classification, dict) or not by_classification:
        return "FLEET-STATE: sin cambios"

    # Deduplicar y normalizar cada categoria accionable.
    deduped: dict[str, list[dict]] = {}
    total_available = 0
    for key, _label in _ACTIONABLE:
        events = _dedup_events(by_classification.get(key))
        deduped[key] = events
        total_available += len(events)

    if total_available == 0:
        return "FLEET-STATE: sin cambios"

    # Aplicar el limite total recorriendo categorias en orden, llenando el cupo.
    limit = max_items if isinstance(max_items, int) and max_items >= 0 else 0
    shown_total = 0
    rendered_segments: list[str] = []
    for key, label in _ACTIONABLE:
        events = deduped[key]
        if not events:
            continue
        remaining = limit - shown_total
        if remaining <= 0:
            break
        take = events[:remaining]
        shown_total += len(take)
        items = [f"{_short_sid(ev.get('session_id'))}:{_trunc_goal(ev.get('goal'))}" for ev in take]
        rendered_segments.append(f"{label}=[{', '.join(items)}]")

    omitted = total_available - shown_total

    # Caso limite: max_items == 0 (u omite todo) pero habia eventos disponibles.
    if not rendered_segments:
        return f"FLEET-STATE: (+{omitted} mas) (drain con ./fn run drain_fleet_events para consumir)"

    body = " ".join(rendered_segments)
    overflow = f" (+{omitted} mas)" if omitted > 0 else ""
    return f"FLEET-STATE: {body}{overflow} (drain con ./fn run drain_fleet_events para consumir)"
