"""Durable outcome summaries for the Nipux terminal UI."""

from __future__ import annotations

import textwrap
from typing import Any

from nipux_cli.tui_event_format import (
    brief_reflection_text,
    chat_agent_message_text,
    event_clock,
    event_hour,
    event_title_body,
    event_tool_args,
    experiment_metric_text,
    generic_display_text,
    shell_write_target,
    short_path,
    tool_live_summary,
)
from nipux_cli.tui_style import _bold, _event_badge, _fit_ansi, _muted, _one_line, _page_indicator, _strip_ansi


CHAT_RIGHT_PAGES = [("updates", "Updates"), ("status", "Jobs")]

DURABLE_OUTCOME_LABELS = {
    "SAVE",
    "FIND",
    "SOURCE",
    "TEST",
    "TASK",
    "ROAD",
    "VALID",
    "LEARN",
    "FILE",
}

SUMMARY_COUNT_LABELS = DURABLE_OUTCOME_LABELS | {"DONE", "FAIL"}
PRIMARY_OUTCOME_LABELS = DURABLE_OUTCOME_LABELS | {"FAIL"}
OUTCOME_LABEL_ORDER = [
    "SAVE",
    "FIND",
    "TEST",
    "FILE",
    "TASK",
    "ROAD",
    "VALID",
    "SOURCE",
    "LEARN",
    "PLAN",
    "UPDATE",
    "FAIL",
    "DONE",
]

OUTCOME_SUMMARY_NAMES = {
    "SAVE": "outputs",
    "FIND": "findings",
    "SOURCE": "sources",
    "TEST": "measurements",
    "TASK": "tasks",
    "ROAD": "roadmap",
    "VALID": "validations",
    "LEARN": "lessons",
    "PLAN": "plans",
    "UPDATE": "updates",
    "FAIL": "blocks",
    "FILE": "files",
    "DONE": "research",
}

SUMMARY_EVENT_TYPES = (
    "agent_message",
    "artifact",
    "error",
    "experiment",
    "finding",
    "lesson",
    "milestone_validation",
    "reflection",
    "roadmap",
    "source",
    "task",
)

SUMMARY_TOOL_EVENT_TYPES = ("tool_result",)


def model_update_event_parts(event: dict[str, Any], *, width: int, compact: bool = True) -> tuple[str, str, str] | None:
    kind = str(event.get("event_type") or "")
    title = generic_display_text(event.get("title") or "")
    body = generic_display_text(event.get("body") or "")
    metadata = event.get("metadata") if isinstance(event.get("metadata"), dict) else {}
    status = str(metadata.get("status") or "")
    clock = event_clock(event)
    chars = max(24, width - 16)
    if kind == "error":
        return "FAIL", _outcome_text(event_title_body(title, body, fallback="error"), chars=chars, compact=compact), clock
    if kind == "artifact":
        detail = title or body or str(metadata.get("summary") or "") or "saved output"
        return "SAVE", _outcome_text(detail, chars=chars, compact=compact), clock
    if kind == "finding":
        return "FIND", _outcome_text(event_title_body(title, body, fallback="finding"), chars=chars, compact=compact), clock
    if kind == "source":
        return "SOURCE", _outcome_text(event_title_body(title, body, fallback="source"), chars=chars, compact=compact), clock
    if kind == "experiment":
        metric = experiment_metric_text(metadata)
        detail = event_title_body(title, body, fallback="measurement")
        if metric and metric not in detail:
            detail = f"{detail} - {metric}"
        return "TEST", _outcome_text(detail, chars=chars, compact=compact), clock
    if kind == "task":
        task_status = str(metadata.get("status") or "")
        detail = event_title_body(title, body, fallback="task")
        prefix = f"{task_status} " if task_status else ""
        return "TASK", _outcome_text(prefix + detail, chars=chars, compact=compact), clock
    if kind == "roadmap":
        return "ROAD", _outcome_text(event_title_body(title, body, fallback="roadmap"), chars=chars, compact=compact), clock
    if kind == "milestone_validation":
        validation = str(metadata.get("validation_status") or metadata.get("status") or "")
        detail = event_title_body(title, body, fallback="milestone")
        return "VALID", _outcome_text(f"{validation} {detail}".strip(), chars=chars, compact=compact), clock
    if kind == "lesson":
        return "LEARN", _outcome_text(event_title_body(title, body, fallback="lesson"), chars=chars, compact=compact), clock
    if kind == "reflection":
        return "PLAN", _outcome_text(brief_reflection_text(body or title), chars=chars, compact=compact), clock
    if kind == "agent_message" and title.lower() in {"error", "blocked"}:
        detail = body or chat_agent_message_text(title, body) or event_title_body(title, body, fallback="error")
        return "FAIL", _outcome_text(detail, chars=chars, compact=compact), clock
    if kind == "agent_message" and title.lower() in {"progress", "update", "report", "plan", "planning"}:
        durable_progress = _durable_progress_event_parts(metadata, body=body, chars=chars, compact=compact, clock=clock)
        if durable_progress:
            return durable_progress
        detail = chat_agent_message_text(title, body) or event_title_body(title, body, fallback="update")
        return "UPDATE", _outcome_text(detail, chars=chars, compact=compact), clock
    if kind == "tool_result" and status == "completed":
        tool = title
        if tool in {"web_search", "web_extract"}:
            return "DONE", _outcome_text(tool_live_summary(tool, metadata, body), chars=chars, compact=compact), clock
        if tool == "shell_exec":
            command = str(event_tool_args(metadata).get("command") or "")
            target = shell_write_target(command)
            if target:
                return "FILE", _outcome_text(f"updated {short_path(target, max_width=chars - 8)} via shell", chars=chars, compact=compact), clock
        if tool == "write_file":
            output = metadata.get("output") if isinstance(metadata.get("output"), dict) else {}
            path = str(output.get("path") or event_tool_args(metadata).get("path") or "")
            return "FILE", _outcome_text(f"updated {short_path(path, max_width=chars - 8)}", chars=chars, compact=compact), clock
    return None


def is_summary_event_candidate(event: dict[str, Any]) -> bool:
    kind = str(event.get("event_type") or "")
    if kind in SUMMARY_EVENT_TYPES:
        return True
    if kind != "tool_result":
        return False
    metadata = event.get("metadata") if isinstance(event.get("metadata"), dict) else {}
    if str(metadata.get("status") or "") != "completed":
        return False
    title = str(event.get("title") or "")
    if title == "write_file":
        return True
    if title == "shell_exec":
        command = str(event_tool_args(metadata).get("command") or "")
        return bool(shell_write_target(command))
    return False


def latest_durable_outcome_line(events: list[dict[str, Any]], *, width: int) -> str:
    fallback: tuple[str, str, str] | None = None
    for event in reversed(events):
        parsed = model_update_event_parts(event, width=width)
        if not parsed:
            continue
        label, text, _clock = parsed
        if label == "DONE":
            fallback = fallback or parsed
            continue
        if label not in PRIMARY_OUTCOME_LABELS:
            continue
        prefix = f"{_muted('Outcome')} {_event_badge(label)} "
        return _fit_ansi(prefix + _one_line(text, max(12, width - len(_strip_ansi(prefix)))), width)
    if fallback:
        label, text, _clock = fallback
        prefix = f"{_muted('Outcome')} {_event_badge(label)} "
        return _fit_ansi(prefix + _one_line(text, max(12, width - len(_strip_ansi(prefix)))), width)
    return ""


def latest_hour_outcome_summary_line(events: list[dict[str, Any]], *, width: int) -> str:
    """Return a single compact count summary for the newest visible activity hour."""

    buckets: dict[str, dict[str, int]] = {}
    order: list[str] = []
    for event in events:
        parsed = model_update_event_parts(event, width=max(width, 180), compact=False)
        if not parsed:
            continue
        label, _text, _clock = parsed
        if label not in SUMMARY_COUNT_LABELS:
            continue
        hour = event_hour(event)
        if hour not in buckets:
            buckets[hour] = {}
            order.append(hour)
        buckets[hour][label] = int(buckets[hour].get(label) or 0) + 1
    if not order:
        return ""
    summary = hourly_outcome_summary(buckets[order[-1]])
    if not summary:
        return ""
    prefix = f"{_muted('Latest hour')} "
    return _fit_ansi(prefix + _bold(_one_line(summary, max(12, width - len(_strip_ansi(prefix))))), width)


def visible_outcome_summary_line(events: list[dict[str, Any]], *, width: int) -> str:
    """Return a stable summary of the durable outcomes available to the pane."""

    counts = outcome_counts(events, include_research=False, include_failures=True)
    summary = hourly_outcome_summary(counts)
    if not summary:
        return ""
    prefix = f"{_muted('Visible')} "
    return _fit_ansi(prefix + _bold(_one_line(summary, max(12, width - len(_strip_ansi(prefix))))), width)


def job_outcome_summary(events: list[dict[str, Any]], *, width: int) -> str:
    """Return a short per-job durable outcome mix for compact job cards."""

    counts = outcome_counts(events, include_research=False, include_failures=False)
    summary = hourly_outcome_summary(counts)
    if not summary:
        return ""
    return _one_line(summary, width)


def outcome_counts(
    events: list[dict[str, Any]],
    *,
    include_research: bool,
    include_failures: bool,
) -> dict[str, int]:
    counts: dict[str, int] = {}
    for event in events:
        parsed = model_update_event_parts(event, width=220, compact=False)
        if not parsed:
            continue
        label, _text, _clock = parsed
        if label == "DONE" and not include_research:
            continue
        if label == "FAIL" and not include_failures:
            continue
        if label not in SUMMARY_COUNT_LABELS:
            continue
        counts[label] = int(counts.get(label) or 0) + 1
    return counts


def recent_model_update_lines(
    events: list[dict[str, Any]],
    *,
    width: int,
    limit: int,
    include_research: bool = False,
    wrap: bool = True,
) -> list[str]:
    """Render recent durable worker outcomes for the compact status pane."""
    if limit <= 0:
        return []
    lines: list[str] = []
    items: list[dict[str, Any]] = []
    index_by_key: dict[tuple[str, str], int] = {}
    for event in reversed(events):
        parsed = model_update_event_parts(event, width=max(width, 180), compact=False)
        if not parsed:
            continue
        label, text, clock = parsed
        if label == "DONE" and not include_research:
            continue
        if label not in PRIMARY_OUTCOME_LABELS and not (include_research and label == "DONE"):
            continue
        key = (label, text)
        if key in index_by_key:
            items[index_by_key[key]]["count"] = int(items[index_by_key[key]].get("count") or 1) + 1
            continue
        index_by_key[key] = len(items)
        items.append({"label": label, "text": text, "clock": clock, "count": 1})
        if len(items) >= max(limit * 2, limit + 8):
            break
    for item in items:
        label = str(item["label"])
        text = str(item["text"])
        clock = str(item["clock"])
        count = int(item.get("count") or 1)
        prefix = f"{_muted(clock)} {_event_badge(label)} " if clock else f"{_event_badge(label)} "
        prefix_width = len(_strip_ansi(prefix))
        available = max(12, width - prefix_width - 2)
        if count > 1:
            text = f"{text} x{count}"
        if not wrap:
            lines.append(_fit_ansi(prefix + _one_line(text, available), width))
            if len(lines) >= limit:
                return lines
            continue
        wrapped = textwrap.wrap(text, width=available) or [""]
        lines.append(_fit_ansi(prefix + wrapped[0], width))
        if len(lines) >= limit:
            return lines
        continuation_prefix = " " * prefix_width
        for part in wrapped[1:]:
            lines.append(_fit_ansi(continuation_prefix + part, width))
            if len(lines) >= limit:
                return lines
        if len(lines) >= limit:
            return lines
    return lines


def chat_updates_pane_lines(
    *,
    job: dict[str, Any],
    events: list[dict[str, Any]],
    width: int,
    rows: int,
) -> list[str]:
    lines = [
        f"{_muted('Page')}   {_page_indicator('updates', CHAT_RIGHT_PAGES)}",
        f"{_muted('Focus')}  {_bold(_one_line(job.get('title') or 'untitled', width - 8))}",
    ]
    counts = outcome_counts(events, include_research=False, include_failures=True)
    summary = hourly_outcome_summary(counts)
    if summary:
        lines.extend([*_wrapped_label_line("Visible", summary, width=width), ""])
    update_lines = recent_model_update_lines(events, width=width, limit=max(4, rows - len(lines)), wrap=False)
    if update_lines:
        lines.extend(update_lines)
    else:
        lines.extend(["", _muted("No model updates yet.")])
    return [_fit_ansi(line, width) for line in lines[:rows]]


def _wrapped_label_line(label: str, text: str, *, width: int) -> list[str]:
    prefix = f"{_muted(label)} "
    prefix_width = len(_strip_ansi(prefix))
    available = max(12, width - prefix_width)
    wrapped = textwrap.wrap(text, width=available) or [""]
    lines = [_fit_ansi(prefix + _bold(wrapped[0]), width)]
    continuation = " " * prefix_width
    for part in wrapped[1:]:
        lines.append(_fit_ansi(continuation + _bold(part), width))
    return lines


def hourly_update_lines(events: list[dict[str, Any]], *, width: int, limit: int) -> list[str]:
    if limit <= 0:
        return []
    buckets: dict[str, dict[str, Any]] = {}
    order: list[str] = []
    for event in events:
        parsed = model_update_event_parts(event, width=max(width, 220), compact=False)
        if not parsed:
            continue
        label, text, clock = parsed
        if label not in SUMMARY_COUNT_LABELS:
            continue
        hour = event_hour(event)
        if hour not in buckets:
            buckets[hour] = {"counts": {}, "items": [], "clock": clock}
            order.append(hour)
        bucket = buckets[hour]
        counts = bucket["counts"]
        counts[label] = int(counts.get(label) or 0) + 1
        item = (label, text)
        if item not in bucket["items"]:
            bucket["items"].append(item)
    rendered: list[str] = []
    # Each visible hour needs a header and at least a couple durable outcomes.
    # Showing too many buckets makes the pane churn and can trim off the hour
    # label, which is harder to scan during long-running jobs.
    max_visible_hours = max(1, min(len(order), max(1, limit // 4)))
    recent_hours = order[-max_visible_hours:]
    available_items = max(1, limit - len(recent_hours))
    per_bucket = max(1, min(6, available_items // max(1, len(recent_hours))))
    for hour in recent_hours:
        bucket = buckets[hour]
        counts = bucket["counts"]
        summary = hourly_outcome_summary(counts)
        rendered.append(_fit_ansi(f"{_muted(hour)} {_bold(summary or 'activity')}", width))
        primary_items = [item for item in bucket["items"] if item[0] in PRIMARY_OUTCOME_LABELS]
        visible_items = primary_items or bucket["items"]
        for label, text in visible_items[-per_bucket:]:
            prefix = f"  {_event_badge(label)} "
            available = max(16, width - len(_strip_ansi(prefix)))
            parts = textwrap.wrap(text, width=available) or [""]
            rendered.append(_fit_ansi(prefix + parts[0], width))
            for part in parts[1:]:
                rendered.append(_fit_ansi(" " * len(_strip_ansi(prefix)) + part, width))
                if len(rendered) >= limit:
                    return rendered[:limit]
        if len(rendered) >= limit:
            return rendered[:limit]
    return rendered[:limit]


def hourly_outcome_summary(counts: dict[str, Any]) -> str:
    pieces: list[str] = []
    ordered = [label for label in OUTCOME_LABEL_ORDER if label in counts]
    ordered.extend(sorted(label for label in counts if label not in set(OUTCOME_LABEL_ORDER)))
    for label in ordered:
        count = int(counts.get(label) or 0)
        if count <= 0:
            continue
        name = OUTCOME_SUMMARY_NAMES.get(label, label.lower())
        pieces.append(f"{count} {name}")
    return " ".join(pieces)


def _durable_progress_event_parts(
    metadata: dict[str, Any],
    *,
    body: str,
    chars: int,
    compact: bool,
    clock: str,
) -> tuple[str, str, str] | None:
    deltas = _count_map(metadata.get("deltas"))
    updates = _count_map(metadata.get("updates"))
    resolutions = _count_map(metadata.get("resolutions"))
    totals = {
        key: int(deltas.get(key) or 0) + int(updates.get(key) or 0) + int(resolutions.get(key) or 0)
        for key in set(deltas) | set(updates) | set(resolutions)
    }
    if not any(value > 0 for value in totals.values()):
        return None
    key = _dominant_progress_key(totals, resolutions=resolutions)
    if not key:
        return None
    label = _progress_label_for_key(key, resolution=bool(resolutions.get(key)))
    pieces: list[str] = []
    for record_key in ("findings", "experiments", "sources", "tasks", "milestones", "lessons"):
        if deltas.get(record_key):
            pieces.append(_progress_count_phrase(int(deltas[record_key]), record_key, prefix="+"))
        if updates.get(record_key):
            pieces.append(_progress_count_phrase(int(updates[record_key]), record_key, prefix="~", suffix="updated"))
        if resolutions.get(record_key):
            pieces.append(_progress_count_phrase(int(resolutions[record_key]), record_key, suffix="resolved"))
    detail = ", ".join(pieces)
    if body:
        detail = f"{detail} - {generic_display_text(body)}"
    return label, _outcome_text(detail, chars=chars, compact=compact), clock


def _count_map(value: Any) -> dict[str, int]:
    if not isinstance(value, dict):
        return {}
    result: dict[str, int] = {}
    for key, raw_count in value.items():
        try:
            count = int(raw_count)
        except (TypeError, ValueError):
            continue
        if count > 0:
            result[str(key)] = count
    return result


def _dominant_progress_key(totals: dict[str, int], *, resolutions: dict[str, int]) -> str:
    for key in ("experiments", "milestones", "findings", "sources", "tasks", "lessons"):
        if resolutions.get(key):
            return key
    for key in ("experiments", "findings", "milestones", "sources", "tasks", "lessons"):
        if totals.get(key):
            return key
    return ""


def _progress_label_for_key(key: str, *, resolution: bool) -> str:
    if key == "findings":
        return "FIND"
    if key == "sources":
        return "SOURCE"
    if key == "experiments":
        return "TEST"
    if key == "tasks":
        return "TASK"
    if key == "milestones":
        return "VALID" if resolution else "ROAD"
    if key == "lessons":
        return "LEARN"
    return "UPDATE"


def _progress_count_phrase(value: int, key: str, *, prefix: str = "", suffix: str = "") -> str:
    label = OUTCOME_SUMMARY_NAMES.get(_progress_label_for_key(key, resolution=False), key)
    if value == 1 and label.endswith("s"):
        label = label[:-1]
    parts = [f"{prefix}{value} {label}"]
    if suffix:
        parts.append(suffix)
    return " ".join(parts)


def _outcome_text(text: str, *, chars: int, compact: bool) -> str:
    clean = generic_display_text(text)
    if compact:
        return _one_line(clean, chars)
    return _one_line(clean, 900)
