"""Read-side aggregator for okstra-run error logs.

task 의 모든 run errors-<type>-<seq>.jsonl 을 모아 phase / worker / errorType
별로 집계하고 timestamp 누적 .md 리포트로 렌더한다. task 산출물을 mutate 하지
않는다.
"""
from __future__ import annotations

import argparse
import datetime as dt
import json
import sys
from pathlib import Path

from .fixed_text import line

from okstra_ctl.task_target import resolve_task_root, project_rel
from okstra_project import read_task_key
from okstra_ctl.error_log_core import (
    glob_error_logs,
    parse_records,
    aggregate,
)


def render_error(
    run_root: Path, *, role_execution_ref: str
) -> str:
    """Render one error line using the stored execution label."""
    from okstra_ctl.execution_identity import (
        execution_identity_for_ref,
        stored_execution_label,
    )
    from okstra_ctl.usage_identity import run_execution_manifest

    manifest = run_execution_manifest(run_root)
    role = execution_identity_for_ref(manifest, role_execution_ref)
    label = stored_execution_label({"executionLabel": role.execution_label})
    return f"{label}: error"


def _escape_cell(value: object) -> str:
    return str(value).replace("|", "\\|").replace("\n", " ")


def _md_table(header: list[str], rows: list[list[str]]) -> str:
    line = "| " + " | ".join(_escape_cell(h) for h in header) + " |"
    sep = "| " + " | ".join("---" for _ in header) + " |"
    if not rows:
        empty = "| " + " | ".join(["_없음_"] + [""] * (len(header) - 1)) + " |"
        return "\n".join([line, sep, empty])
    body = ["| " + " | ".join(_escape_cell(c) for c in cells) + " |" for cells in rows]
    return "\n".join([line, sep, *body])


# The two types a next run can actually act on: a worker that broke its
# contract (adjust the roster or pre-check its result) and a tool that failed
# (raise a budget, or expect the same failure). Everything else is either
# already retried or not actionable at preparation time.
CARRY_FORWARD_ERROR_TYPES = ("contract-violation", "tool-failure")


def prior_run_error_digest(
    task_root: Path, *, error_types: tuple[str, ...] = CARRY_FORWARD_ERROR_TYPES
) -> str:
    """Markdown digest of the traps this task's earlier runs already hit.

    Read-only: unlike ``build_and_write`` this writes no report file, so it is
    safe to call while a run is being prepared. The records have always been on
    disk under ``runs/*/logs/errors-*.jsonl`` and ``okstra error-report`` has
    aggregated them for just as long — what was missing is anyone reading them
    *before* the next run, so the same violation recurred and the response was
    improvised every time.

    Returns ``""`` when nothing actionable is recorded, which is the signal for
    the caller to stage no file at all rather than an empty one.
    """
    records, _ = parse_records(glob_error_logs(task_root))
    carried = [
        record
        for record in records
        if str(record.get("errorType", "")) in error_types
    ]
    if not carried:
        return ""
    counts: dict[tuple[str, str, str], int] = {}
    for record in carried:
        key = (
            str(record.get("errorType", "")),
            str(record.get("phase", "")),
            str(record.get("agent", "")),
        )
        counts[key] = counts.get(key, 0) + 1
    table = _md_table(
        ["Error type", "Phase", "Agent", "Count"],
        [
            [error_type, phase or "—", agent or "—", str(count)]
            for (error_type, phase, agent), count in sorted(
                counts.items(), key=lambda item: (-item[1], item[0])
            )
        ],
    )
    return (
        "# Prior-Run Errors\n\n"
        "`contract-violation` and `tool-failure` records this task's earlier "
        "runs wrote. These are not findings about the work — they are traps "
        "that fired before and can fire again in this run.\n\n"
        f"{table}\n"
    )


def render_markdown(*, task_key, records, agg, parse_skipped, generated_at) -> str:
    by_type = ", ".join(f"{k}: {v}" for k, v in sorted(agg["byErrorType"].items())) or "_없음_"
    by_src = ", ".join(f"{k}: {v}" for k, v in sorted(agg["bySource"].items())) or "_없음_"
    parts = [
        f"# okstra 에러 리포트 — {task_key}",
        "",
        "## 요약",
        "",
        f"- 총 에러: {agg['errorCount']}건",
        f"- 집계된 로그(run) 수: {agg['runCount']}",
        f"- errorType 별: {by_type}",
        f"- source 별: {by_src}",
        f"- 파싱 건너뛴 줄: {parse_skipped}",
        f"- 생성 시각: {generated_at}",
        "",
        "## phase 별 집계",
        "",
        _md_table(["Phase", "Count"],
                  [[r["phase"] or "(없음)", str(r["count"])] for r in agg["byPhase"]]),
        "",
        "## worker(agent) 별 집계",
        "",
        _md_table(["Agent", "Count"],
                  [[r["agent"] or "(없음)", str(r["count"])] for r in agg["byAgent"]]),
        "",
        "## 개별 에러 이벤트",
        "",
        _md_table(
            [
                "ts", "phase", "agent", "executionLabel", "errorType",
                "command", "exitCode", "message",
            ],
            [[
                str(r.get("ts", "")), str(r.get("phase", "")), str(r.get("agent", "")),
                str(r.get("executionLabel") or ""),
                str(r.get("errorType", "")), f"`{r.get('command', '')}`",
                str(r.get("exitCode", "")),
                str(r.get("message", "")),
            ] for r in sorted(records, key=lambda x: str(x.get("ts", "")))],
        ),
        "",
    ]
    return "\n".join(parts)


def _timestamp_segment(now: dt.datetime) -> str:
    return now.strftime("%Y-%m-%d_%H-%M-%S")


def build_and_write(task_root: Path, project_root: Path, now: dt.datetime) -> dict:
    task_key = read_task_key(task_root)
    paths = glob_error_logs(task_root)
    records, skipped = parse_records(paths)
    agg = aggregate(records)
    report_rel = ""
    if paths:
        md = render_markdown(
            task_key=task_key, records=records, agg=agg,
            parse_skipped=skipped, generated_at=now.isoformat(),
        )
        out_dir = task_root / "error-reports"
        out_dir.mkdir(parents=True, exist_ok=True)
        out_path = out_dir / f"error-report-{_timestamp_segment(now)}.md"
        out_path.write_text(md, encoding="utf-8")
        report_rel = project_rel(out_path, project_root)
    return {
        "taskKey": task_key,
        "reportPath": report_rel,
        "parseSkipped": skipped,
        "totals": {
            "errorCount": agg["errorCount"],
            "runCount": agg["runCount"],
            "byErrorType": agg["byErrorType"],
            "bySource": agg["bySource"],
        },
        "byPhase": agg["byPhase"],
        "byAgent": agg["byAgent"],
    }


def render_result_text(result: dict) -> str:
    totals = result.get("totals") if isinstance(result.get("totals"), dict) else {}
    parts = ["# Okstra Error Report Input\n\n", line("Task key", result.get("taskKey")), line("Report path", result.get("reportPath")), line("Total errors", totals.get("errorCount")), line("Run count", totals.get("runCount")), line("Parse skipped", result.get("parseSkipped"))]
    for key, label in (("byErrorType", "Error type"), ("bySource", "Source")):
        values = totals.get(key)
        if isinstance(values, dict):
            for name, count in sorted(values.items()):
                parts.append(line(f"{label} {name}", count))
    for key, label in (("byPhase", "Phase"), ("byAgent", "Agent")):
        values = result.get(key)
        if isinstance(values, dict):
            for name, count in sorted(values.items()):
                parts.append(line(f"{label} {name}", count))
        elif isinstance(values, list):
            identity = "phase" if key == "byPhase" else "agent"
            for row in values:
                if isinstance(row, dict):
                    parts.append(line(f"{label} {row.get(identity)}", row.get("count")))
    return "".join(parts)


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="Aggregate okstra-run error logs into a task-level report."
    )
    parser.add_argument("target", help="task root path or task-key")
    parser.add_argument("--project-root", default="", help="project root for task-key lookup")
    parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
    parser.add_argument("--text", action="store_true", help="emit fixed text fields")
    args = parser.parse_args(argv)

    task_root, project_root = resolve_task_root(args.target, args.project_root, args.cwd)
    result = build_and_write(task_root, project_root, dt.datetime.now(dt.timezone.utc))
    output = render_result_text(result) if args.text else json.dumps(result, ensure_ascii=False, indent=2)
    print(output, end="" if args.text else "\n")
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
