"""Рычаг порчи: проверка, которая проверяет проверку (R03).

Зелёный прогон доказывает только то, что команда вернула ноль. Врёт ли она —
неизвестно, пока её не сломали нарочно. Рычаг порчи вносит поломку и обязан
получить ненулевой код: нулевой значит, что проверка не смотрит туда, куда думает
исполнитель.

Рычаг пишется в тот же журнал доказательств, что и прогон, и несёт ссылку на
проверку, которую покрывает: в `evidence.jsonl` видно зелёный прогон и красный
рычаг одной парой.
"""

from __future__ import annotations

import subprocess
import time
from pathlib import Path

from . import proof
from .store import StateError, git_sha, harness, now

LEVER_KIND = "lever"


def _execute(cmd: str, cwd: Path, timeout: int) -> tuple[int, str, str]:
    """Прогон команды тем же путём, что и доказательство: сперва псевдотерминал.

    Отдельно от `proof.run_command`, потому что строка рычага несёт поле, которого
    у обычного доказательства нет, — ссылку на покрываемую проверку.
    """
    try:
        exit_code, out = proof._capture_via_pty(cmd, cwd, timeout)
        return exit_code, out, ""
    except Exception:
        try:
            done = subprocess.run(
                cmd, shell=True, cwd=cwd, capture_output=True, text=True, timeout=timeout,
            )
            return done.returncode, done.stdout or "", done.stderr or ""
        except subprocess.TimeoutExpired:
            return 124, "", f"команда не уложилась в {timeout} с и была снята"
        except OSError as exc:
            return 127, "", f"не удалось запустить: {exc}"


def run_lever(
    run,
    lever: str,
    check_row: dict,
    task: str | None = None,
    model: str | None = None,
    cwd: Path | None = None,
    timeout: int = 1800,
) -> dict:
    """Гоняет рычаг порчи и пишет его строкой журнала рядом с прогоном."""
    if not lever or not lever.strip():
        raise StateError(
            "пустой рычаг порчи ничего не ломает: оболочка вернёт на него ноль, и "
            "проверка засчитается непроверенной. Назовите команду, которая портит "
            "код и гоняет проверку."
        )
    workdir = cwd or run.root
    started = time.monotonic()
    exit_code, out, err = _execute(lever, workdir, timeout)
    row = {
        "ts": now(),
        "task": task,
        "kind": LEVER_KIND,
        "cmd": lever,
        "exit": exit_code,
        "duration_ms": int((time.monotonic() - started) * 1000),
        "cwd": str(workdir),
        "git_sha": git_sha(run.root),
        "tree": proof.worktree_fingerprint(run.root),
        "agent": harness(),
        "model": model,
        # Ссылка на покрываемую проверку. Без неё пару пришлось бы угадывать по
        # соседству строк, а журнал дописывают и параллельные прогоны.
        "lever_for": {
            "cmd": check_row.get("cmd"),
            "ts": check_row.get("ts"),
            "exit": check_row.get("exit"),
        },
        "out_tail": proof._tail(out),
        "err_tail": proof._tail(err),
    }
    run.append_evidence(row)
    return row


def is_red(row: dict) -> bool:
    """Рычаг сработал: порча внесена, проверка покраснела."""
    return row.get("exit") not in (None, 0)


def refusal_text(row: dict) -> list[str]:
    """Отказ на рычаге с нулевым кодом. Молчать здесь опаснее всего."""
    return [
        f"Рычаг порчи `{row.get('cmd')}` вернул 0.",
        "Проверка не краснеет там, где должна: порчу внесли, а она осталась зелёной —",
        "значит, доказывает не то, что вы думаете.",
        "Почините проверку или напишите рычаг, который её действительно ломает.",
    ]


def checks_without_lever(run) -> list[dict]:
    """Зелёные проверки, которых никто не ломал.

    Покрытой считается только та пара, где рычаг дал красное: рычаг с нулевым
    кодом ничего не доказал и покрытием не считается.
    """
    rows = run.evidence()
    covered = {
        (row.get("task"), (row.get("lever_for") or {}).get("cmd"))
        for row in rows
        if row.get("kind") == LEVER_KIND and is_red(row)
    }
    seen: set[tuple] = set()
    missing: list[dict] = []
    for row in rows:
        if row.get("kind") != "check" or row.get("exit") != 0:
            continue
        key = (row.get("task"), row.get("cmd"))
        if key in seen or key in covered:
            continue
        seen.add(key)
        missing.append({"task": row.get("task"), "cmd": row.get("cmd")})
    return missing


def report_line(run) -> str:
    """Строка отчёта. Закрывать задачи без рычага пока можно — но не молча."""
    missing = checks_without_lever(run)
    if not missing:
        return "Проверок без рычага порчи: 0 — каждую зелёную проверку кто-то ломал."
    listed = ", ".join(
        f"{item['task'] or 'без задачи'} `{item['cmd']}`" for item in missing
    )
    return f"Проверок без рычага порчи: {len(missing)} — {listed}."
