#!/usr/bin/env python3
"""echo_triage.py — one-tap inbox triage, built on the reflect pipeline.

Triage used to be pure prose: the agent hand-routed each inbox line with individual
PATCH/PUT calls and hand-wrote the processing log — which is why the inbox rots.
But the machinery it needs already exists in echo_reflect (validate -> classify
against the entity index -> preview -> apply via capture). This module reuses it
and adds the two triage-specific pieces:

  * `list_inbox`  — parse `inbox/captures/inbox.md` into structured capture lines
                    (date, text, age in days) so the model builds proposals from
                    data, not by re-reading prose;
  * `apply`       — same contract as reflect (dry-run unless confirm), plus an
                    audit line in `inbox/processing-log/YYYY-MM-DD.md` for every
                    routed item. Originals are NEVER deleted (operating contract:
                    the processing log is the audit trail, deletion is explicit).

Proposals are reflect's PROPOSAL_SCHEMA plus an optional `"line"` — the original
inbox line, echoed into the processing log so the audit trail maps 1:1.
"""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import echo                    # noqa: E402

INBOX_PATH = "inbox/captures/inbox.md"
LOG_DIR = "inbox/processing-log"

_CAPTURE_LINE = re.compile(r"^\s*-\s*(\d{4}-\d{2}-\d{2})\s*:?\s*(.*\S)\s*$")


def parse_inbox(text: str) -> list[dict]:
    """Dated capture bullets -> [{line, date, text, age_days}] (age from ECHO_TODAY)."""
    import datetime as dt
    try:
        today = dt.date.fromisoformat(echo.today())
    except ValueError:
        today = dt.date.today()
    items = []
    for raw_line in text.splitlines():
        m = _CAPTURE_LINE.match(raw_line)
        if not m:
            continue
        try:
            age = (today - dt.date.fromisoformat(m.group(1))).days
        except ValueError:
            age = None
        items.append({"line": raw_line.strip(), "date": m.group(1),
                      "text": m.group(2), "age_days": age})
    return items


def list_inbox(as_json: bool = False) -> int:
    status, body = echo.request("GET", echo.vault_url(INBOX_PATH))
    if status == 404:
        items = []
    else:
        echo.check(status, body, f"triage list {INBOX_PATH}")
        items = parse_inbox(body.decode(errors="replace"))
    if as_json:
        import echo_output
        env = echo_output.envelope("triage-list", {"path": INBOX_PATH, "items": items,
                                                   "count": len(items)})
        print(json.dumps(env, ensure_ascii=False))
        return 0
    if not items:
        print("triage: inbox is empty — nothing to route.")
        return 0
    print(f"triage: {len(items)} capture(s) in {INBOX_PATH}")
    for it in items:
        age = f"{it['age_days']}d" if it["age_days"] is not None else "?"
        print(f"  [{age:>4}] {it['date']}: {it['text']}")
    print("\nBuild a proposals JSON (reflect schema + optional \"line\") and run "
          "`echo.py triage <file>` to preview, `--apply` to route.")
    return 0


def apply(proposals: list[dict], confirm: bool = False) -> int:
    """Validate -> classify -> preview; with confirm, route each via capture AND write
    the processing-log audit line. Mirrors echo_reflect.apply's contract exactly."""
    import echo_reflect
    valid, errors = echo_reflect.validate(proposals)
    for e in errors:
        print(f"skip: {e}", file=sys.stderr)
    if not valid:
        print("triage: no valid proposals to route.")
        return 0

    echo_reflect.classify(valid)
    counts = {a: sum(1 for p in valid if p.get("_action") == a)
              for a in ("create", "update", "inbox", "error")}
    print(f"triage: {len(valid)} proposal(s) — "
          f"{counts['create']} new, {counts['update']} update, {counts['inbox']} stay-in-inbox"
          + (f", {counts['error']} error" if counts["error"] else ""))
    print(echo_reflect.preview(valid))

    if not confirm:
        print("\ntriage: dry-run — re-run with --apply to route these and log the moves.")
        return 0

    import echo_ops
    today_s = echo.today()
    applied = gated = 0
    for p in valid:
        if p.get("_action") == "error":
            continue
        if p.get("_action") == "inbox":
            continue  # routing an inbox line back to the inbox is a no-op, not a move
        bodyfile = echo.temp_file((p.get("body") or "").encode()) if p.get("body") else None
        rc = echo_ops.capture(
            p.get("kind"), p["title"], bodyfile,
            aliases=p.get("aliases") or [], sources=p.get("sources") or [],
            tags=p.get("tags") or [],
            date=p.get("date"), domain=p.get("domain", "business"))
        if rc == 76:
            gated += 1
            continue
        if rc != 0:
            continue
        applied += 1
        original = (p.get("line") or p["title"]).strip()
        echo.cmd_append(f"{LOG_DIR}/{today_s}.md",
                        f"- {original} → {p.get('_path', '?')}")
    print(f"triage: routed {applied}/{len(valid)} item(s); audit in {LOG_DIR}/{today_s}.md. "
          "Originals kept in the inbox (deletion is explicit-only)."
          + (f" {gated} stopped at the duplicate gate — re-propose with the existing "
             f"entity's title or use capture --merge-into." if gated else ""))
    return 0
