"""Обзор прогона в терминале.

То же, что показывает панель, но там, где агент и владелец уже находятся. Панель
нужна не всегда: по ssh, в логе, в чужом харнессе её нет, а понимать прогон надо.

Цвет включается только для настоящего терминала: в канале и в журнале escape-коды —
мусор, который потом мешает грепать.
"""

from __future__ import annotations

import os
import shutil
import sys
from datetime import datetime

from . import gates, phases
from .store import Run


def _(text: str) -> str:
    try:
        from .i18n import translate
        return translate(text)
    except Exception:
        return text


RESET, DIM, BOLD = "\033[0m", "\033[2m", "\033[1m"
GREEN, RED, YELLOW, BLUE, GREY = (
    "\033[32m", "\033[31m", "\033[33m", "\033[36m", "\033[90m",
)

PHASE_MARK = {"done": "✓", "active": "▶", "skipped": "⤼", "pending": "·"}
TASK_MARK = {
    "done": "✓", "failed": "✗", "in-progress": "▶",
    "review": "◑", "repair": "⚒", "pending": "·",
}
TASK_RU = {
    "pending": "ждёт", "in-progress": "идёт", "review": "на ревью",
    "repair": "ремонт", "done": "закрыта", "failed": "провалена",
}
REQ_RU = {
    "open": "открыто", "in-spec": "в спеке", "in-task": "в задаче", "done": "закрыто",
    "placeholder": "нужны данные", "deferred": "отложено", "dropped": "снято",
}


class Ink:
    def __init__(self, enabled: bool):
        self.on = enabled

    def __call__(self, text: str, *codes: str) -> str:
        return "".join(codes) + text + RESET if self.on and codes else text


def _width() -> int:
    # Ширина ограничена: длинные строки на широком терминале читаются хуже коротких.
    return max(60, min(shutil.get_terminal_size((92, 24)).columns, 88))


def _dur(seconds: float | None) -> str:
    if seconds is None or seconds < 0:
        return "—"
    s = int(round(seconds))
    if s < 60:
        return f"{s} с"
    m, s = divmod(s, 60)
    if m < 60:
        return f"{m} мин" + (f" {s} с" if s else "")
    h, m = divmod(m, 60)
    return f"{h} ч" + (f" {m} мин" if m else "")


def _span(a: str | None, b: str | None) -> float | None:
    if not a:
        return None
    start = datetime.fromisoformat(a)
    end = datetime.fromisoformat(b) if b else datetime.now(start.tzinfo)
    return (end - start).total_seconds()


def _bar(done: int, live: int, bad: int, total: int, width: int, ink: Ink) -> str:
    if not total:
        return ink("░" * width, GREY)
    unit = width / total
    nd, nl, nb = (round(done * unit), round(live * unit), round(bad * unit))
    rest = max(0, width - nd - nl - nb)
    return (
        ink("█" * nd, GREEN) + ink("▓" * nl, BLUE)
        + ink("█" * nb, RED) + ink("░" * rest, GREY)
    )


SILENCE_FLOOR_MS = 15 * 60 * 1000


def _silence(run: Run) -> int | None:
    """Сколько прогон стоит без движения, если это уже похоже на остановку.

    Порог — вдвое больше медианы закрытой задачи, но не меньше пятнадцати минут:
    живой прогон на устройстве идёт молча минутами, и объявлять его мёртвым нельзя.
    """
    from datetime import datetime, timezone

    from .store import _ms, paused_ms_between

    st = run.state
    stamps = [_ms(st.get("updated_at"))]
    for t in st.get("tasks", []):
        stamps += [_ms(t.get("started_at")), _ms(t.get("closed_at"))]
    for row in run.evidence():
        stamps.append(_ms(row.get("ts")))
    stamps = [x for x in stamps if x]
    if not stamps:
        return None
    if run.is_paused():
        return None  # на паузе тишина законна, это не остановка
    last = max(stamps)
    now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
    from datetime import datetime as _dt

    quiet = now_ms - last - paused_ms_between(
        st, _dt.fromtimestamp(last / 1000, timezone.utc).isoformat()
    )
    spans = sorted(
        x for x in (
            (_ms(t.get("closed_at")) or 0) - (_ms(t.get("started_at")) or 0)
            for t in st.get("tasks", []) if t.get("status") == "done"
        ) if x > 0
    )
    median = spans[len(spans) // 2] if spans else 0
    limit = max(SILENCE_FLOOR_MS, median * 2)
    return quiet if quiet > limit else None


def render(run: Run) -> str:
    st = run.state
    ink = Ink(sys.stdout.isatty() and os.environ.get("NO_COLOR") is None)
    w = _width()
    out: list[str] = []
    rule = ink("─" * w, GREY)

    title = st["title"]
    from .manifest import short_title

    head = short_title(title)
    elapsed = _span(st["started_at"], st.get("finished_at"))
    closed = bool(st.get("finished_at"))

    out.append(ink(head, BOLD))
    if head != title:
        out.append(ink(title[:w], DIM))
    out.append(
        ink("режим ", GREY) + st["mode"]
        + ink("  ·  масштаб ", GREY) + phases.SCALE_RU.get(st["tier"], st["tier"])
        + ink("  ·  ", GREY)
        + (ink(f"✓ закрыт за {_dur(elapsed)}", GREEN) if closed
           else ink(f"⏳ идёт {_dur(elapsed)}", BLUE))
    )
    out.append("")

    tasks, reqs = st["tasks"], st["requirements"]
    done = [t for t in tasks if t["status"] == "done"]
    live = [t for t in tasks if t["status"] in ("in-progress", "review", "repair")]
    bad = [t for t in tasks if t["status"] == "failed"]
    bw = w - 22

    out.append(ink(_("ЗАДАЧИ"), BOLD) + ink(f"  {len(done)} из {len(tasks)}", GREY))
    out.append("  " + _bar(len(done), len(live), len(bad), len(tasks), bw, ink))

    plan = phases.active_phases(run)
    stages = st["stages"]
    livep = [p for p in plan if stages[p]["status"] != "skipped"]
    donep = [p for p in livep if stages[p]["status"] == "done"]
    out.append(ink(_("ФАЗЫ"), BOLD) + ink(f"    {len(donep)} из {len(livep)}", GREY))
    out.append("  " + _bar(len(donep), 1 if any(stages[p]["status"] == "active" for p in livep) else 0,
                           0, len(livep), bw, ink))
    out.append("")

    out.append(rule)
    for p in plan:
        s = stages[p]
        colour = {"done": GREEN, "active": BLUE, "skipped": GREY}.get(s["status"], GREY)
        span = _span(s.get("started_at"), s.get("finished_at")) if s.get("started_at") else None
        line = (
            "  " + ink(PHASE_MARK.get(s["status"], "·"), colour)
            + f" {_(phases.PHASE_RU[p]):<14}"
            + ink(f"{_dur(span) if span is not None else '·':>12}", GREY)
        ).rstrip()
        if s["status"] == "skipped" and s.get("reason"):
            line += ink(f"   {s['reason']}", GREY)
        out.append(line)
    out.append(rule)
    out.append("")

    out.append(ink(_("ГЕЙТЫ"), BOLD))
    row = []
    for name in phases.applicable_gates(run):
        e = st.get("gates", {}).get(name, {})
        if e.get("status") == "passed":
            row.append(ink(f"✓ {name}", GREEN))
        elif e.get("status") == "failed":
            row.append(ink(f"✗ {name}", RED))
        else:
            row.append(ink(f"· {name}", GREY))
    out.append("  " + "   ".join(row) + ink(_("   (chekan gate — подробно)"), GREY))
    out.append("")

    # Прогон может стоять, а задачи числиться идущими: исполнитель упёрся в квоту,
    # оборвалась сеть, убили процесс. Отличить работу от тишины можно только по
    # движению — по последнему доказательству или смене состояния. Причину знать
    # неоткуда, поэтому говорим ровно то, что видим.
    quiet_for = _silence(run)

    if tasks:
        out.append(ink(_("ЧТО СДЕЛАНО"), BOLD))
        stale_limit_ms = gates.stale_timeout_sec() * 1000
        for t in tasks:
            is_stale = t["status"] == "in-progress" and quiet_for is not None and quiet_for >= stale_limit_ms
            colour = RED if is_stale else {"done": GREEN, "failed": RED, "in-progress": BLUE}.get(t["status"], GREY)
            span = _span(t.get("started_at"), t.get("closed_at")) if t.get("started_at") else None
            proven = run.green_evidence(t["id"]) is not None
            name = t["title"][: w - 34]
            out.append(
                "  " + ink(TASK_MARK.get(t["status"], "·"), colour)
                + ink(f" {t['id']:<4}", BOLD) + f"{name:<{max(10, w - 34)}}"
                + ink(f"{_dur(span) if span is not None else '—':>10}", GREY)
                + ("  " + ink(_("доказана"), GREEN) if t["status"] == "done" and proven
                   else "  " + ink(_("БЕЗ ДОКАЗАТЕЛЬСТВА"), RED) if t["status"] == "done"
                   else "  " + ink(f"затих {_dur(quiet_for)}", RED if is_stale else YELLOW)
                   if t["status"] == "in-progress" and quiet_for is not None
                   else "  " + ink(_(TASK_RU.get(t["status"], t["status"])), GREY))
            )
            who = (
                f"{t['model']} ({t['agent']})"
                if t.get("model") and t.get("agent") and t["model"] != t["agent"]
                else (t.get("model") or t.get("agent"))
            )
            if who:
                out.append(ink(f"       {who}", GREY))
        out.append("")

    if reqs:
        by = {}
        for r in reqs:
            by[r["status"]] = by.get(r["status"], 0) + 1
        out.append(
            ink(_("ТРЕБОВАНИЯ"), BOLD) + "  "
            + ink(", ".join(f"{_(REQ_RU.get(k, k))} {v}" for k, v in sorted(by.items())), GREY)
        )
        out.append("")

    from . import detach

    live_checks = detach.pending(run)
    if live_checks:
        # Отдельным разделом, а не только в хвосте журнала: идущая проверка не
        # должна вытесняться из окна последних пяти строк — её как раз и ждут.
        out.append(ink(_("В ОТРЫВЕ"), BOLD))
        for e in live_checks:
            out.append(
                "  " + ink("◐", BLUE) + ink(f" {(e.get('ts') or '')[11:19]} ", GREY)
                + e.get("cmd", "")[: w - 26]
                + ink(f"  идёт в отрыве{', задача ' + e['task'] if e.get('task') else ''}", BLUE)
            )
        out.append("")

    # Запись о начале отрыва — не результат: её место в разделе выше, а исход
    # придёт отдельной строкой. Иначе одна проверка числилась бы идущей вечно.
    rows = [e for e in run.evidence() if e.get("state") != detach.RUNNING][-5:]
    if rows:
        out.append(ink(_("ПОСЛЕДНИЕ ПРОВЕРКИ"), BOLD))
        for e in reversed(rows):
            ok = e.get("exit") == 0
            out.append(
                "  " + ink("✓" if ok else "✗", GREEN if ok else RED)
                + ink(f" {(e.get('ts') or '')[11:19]} ", GREY)
                + e.get("cmd", "")[: w - 26]
                + ink(f"  {round((e.get('duration_ms') or 0) / 1000)} с", GREY)
            )
        out.append("")

    out.append(rule)
    current, blockers = phases.resolve(run)
    if closed:
        out.append(ink(_("  Прогон закрыт. Отчёт: report.md рядом с состоянием."), GREY))
        out.append(ink(_("  Новая работа — новый прогон: chekan init \"…\""), GREY))
    elif blockers:
        out.append("  " + ink(_("Не пускает дальше:"), YELLOW))
        for b in blockers[:4]:
            out.append(ink(f"    · {b[: w - 8]}", GREY))
        if len(blockers) > 4:
            out.append(ink(f"    (и ещё {len(blockers) - 4})", GREY))
    else:
        out.append("  " + ink(f"Фаза «{_(phases.PHASE_RU[current])}» закрыта. Дальше: ", GREY)
                   + ink("chekan advance", BOLD))
    out.append(rule)
    return "\n".join(out)
