#!/usr/bin/env python3
"""Headless inbox-to-Linear drainer for the shared Linear queue."""

from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path
from typing import Any

_REPO_ROOT = Path(__file__).resolve().parents[3]
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from recoil.pipeline.tools import linear_queue as lq  # noqa: E402
from recoil.pipeline.tools.autonomy import linear_client  # noqa: E402


def _queue_paths(queue_root: Path) -> dict[str, Path]:
    root = Path(queue_root)
    return {
        "root": root,
        "inbox": root / "inbox",
        "done": root / "done",
        "ledger": root / "ledger.jsonl",
        "quarantine": root / "quarantine",
    }


def _ledger_row(
    event: dict[str, Any],
    *,
    state: str,
    linear_issue: str | None,
    reason: str | None,
) -> dict[str, Any]:
    return {
        "schema_version": 1,
        "ledger_ts": lq.utc_now_iso(),
        "finding_key": event["finding_key"],
        "event_id": event["event_id"],
        "state": state,
        "linear_issue": linear_issue,
        "reason": reason,
    }


def _append_ledger(
    row: dict[str, Any],
    *,
    ledger: Path,
    ledger_rows: list[dict[str, Any]],
) -> None:
    lq.append_ledger(row, ledger=ledger)
    ledger_rows.append(row)


def _render_value(value: Any) -> str:
    if value is None:
        return ""
    return str(value)


def _issue_body(event: dict[str, Any]) -> str:
    return "\n".join(
        [
            "Evidence:",
            _render_value(event.get("evidence")),
            "",
            "Recommendation:",
            _render_value(event.get("recommendation")),
            "",
            "Source:",
            f"- event_id: {_render_value(event.get('event_id'))}",
            f"- source_type: {_render_value(event.get('source_type'))}",
            f"- category: {_render_value(event.get('category'))}",
            f"- subject_kind: {_render_value(event.get('subject_kind'))}",
            f"- subject_id: {_render_value(event.get('subject_id'))}",
            f"- file: {_render_value(event.get('file'))}",
            f"- pr_url: {_render_value(event.get('pr_url'))}",
            f"- branch: {_render_value(event.get('branch'))}",
            f"- head_sha: {_render_value(event.get('head_sha'))}",
            "",
            f"finding_key:{event['finding_key']}",
        ]
    )


def _quarantine_path(path: Path, *, quarantine: Path) -> Path:
    quarantine.mkdir(parents=True, exist_ok=True)
    target = quarantine / path.name
    os.replace(path, target)
    return target


def _quarantine_malformed(
    malformed: list[tuple[str, str]],
    *,
    quarantine: Path,
    summary: dict[str, Any],
) -> None:
    for raw_path, error in malformed:
        source = Path(raw_path)
        try:
            target = _quarantine_path(source, quarantine=quarantine)
            summary["quarantined"].append(str(target))
            summary["reports"].append(f"quarantined path={source} error={error}")
        except OSError as exc:
            summary["remaining"].append(str(source))
            summary["errors"].append(f"quarantine failed path={source} error={exc}")


def _quarantine_event(
    event: dict[str, Any],
    *,
    inbox: Path,
    quarantine: Path,
    reason: str,
    summary: dict[str, Any],
) -> None:
    source = inbox / f"{event.get('event_id')}.json"
    try:
        target = _quarantine_path(source, quarantine=quarantine)
        summary["quarantined"].append(str(target))
        summary["reports"].append(f"quarantined path={source} error={reason}")
    except OSError as exc:
        summary["remaining"].append(str(source))
        summary["errors"].append(f"quarantine failed path={source} error={exc}")


def _empty_summary(*, dry_run: bool) -> dict[str, Any]:
    return {
        "dry_run": dry_run,
        "exit_code": 0,
        "created": 0,
        "created_issues": [],
        "deduped": 0,
        "deduped_issues": [],
        "quarantined": [],
        "remaining": [],
        "reports": [],
        "errors": [],
        "stopped": False,
    }


def _dry_run(
    *,
    queue_root: Path,
    max_creates: int,
) -> dict[str, Any]:
    paths = _queue_paths(queue_root)
    summary = _empty_summary(dry_run=True)

    items = lq.read_inbox(inbox=paths["inbox"])
    valid = [item for item in items if isinstance(item, dict)]
    malformed = [item for item in items if isinstance(item, tuple)]
    ledger_rows = lq.load_ledger(ledger=paths["ledger"])

    summary["reports"].append(f"valid={len(valid)} malformed={len(malformed)}")
    for path, error in malformed:
        summary["reports"].append(f"quarantine-needed path={path} error={error}")

    creates_preview = 0
    deduped_preview = 0
    quarantined_preview = len(malformed)
    simulated_rows = list(ledger_rows)

    for event in sorted(valid, key=lambda ev: (ev["event_ts"], ev["event_id"])):
        problems = lq.validate_event(event)
        if problems:
            quarantined_preview += 1
            action = "quarantine-needed"
        else:
            key = event["finding_key"]
            action = (
                "dedupe-ledger"
                if lq.is_already_handled(key, simulated_rows)
                else "would-check-linear-backstop"
            )
            if action == "dedupe-ledger":
                deduped_preview += 1
            if action == "would-check-linear-backstop" and creates_preview < max_creates:
                action = "would-create-if-no-open-backstop"
                creates_preview += 1
                simulated_rows.append({"finding_key": key, "state": "filed"})
            elif action == "would-check-linear-backstop":
                action = "remaining-cap-reached"
        summary["reports"].append(
            f"{event['event_id']} {event['finding_key']} {event['source_type']} "
            f"{action} title={event['title']!r}"
        )

    remaining_preview = sum(
        1
        for event in valid
        if not lq.validate_event(event)
        and not lq.is_already_handled(event["finding_key"], simulated_rows)
    )
    summary["dry_run_summary"] = {
        "created": 0,
        "deduped_preview": deduped_preview,
        "quarantined_preview": quarantined_preview,
        "remaining_preview": remaining_preview,
    }
    return summary


def drain(
    *,
    queue_root: Path | str,
    live: bool,
    max_creates: int,
    team: str = "Recoil",
    transport: linear_client.Transport | None = None,
) -> dict[str, Any]:
    """Drain inbox events into Linear, returning a printable summary dict."""
    queue_root = Path(queue_root)
    if max_creates <= 0:
        summary = _empty_summary(dry_run=not live)
        summary["exit_code"] = 2
        summary["errors"].append("--max must be a positive integer")
        return summary
    if live and not os.environ.get("LINEAR_API_KEY"):
        summary = _empty_summary(dry_run=False)
        summary["exit_code"] = 1
        summary["errors"].append("LINEAR_API_KEY is required for --live")
        return summary
    if not live:
        return _dry_run(queue_root=queue_root, max_creates=max_creates)

    paths = _queue_paths(queue_root)
    summary = _empty_summary(dry_run=False)

    items = lq.read_inbox(inbox=paths["inbox"])
    valid = [item for item in items if isinstance(item, dict)]
    malformed = [item for item in items if isinstance(item, tuple)]
    _quarantine_malformed(
        malformed,
        quarantine=paths["quarantine"],
        summary=summary,
    )

    ledger_rows = lq.load_ledger(ledger=paths["ledger"])
    creates = 0
    auto_filed_id: str | None = None

    sorted_events = sorted(valid, key=lambda ev: (ev["event_ts"], ev["event_id"]))
    for index, event in enumerate(sorted_events):
        if creates >= max_creates:
            summary["remaining"].extend(
                str(paths["inbox"] / f"{remaining['event_id']}.json")
                for remaining in sorted_events[index:]
            )
            summary["stopped"] = True
            break

        problems = lq.validate_event(event)
        if problems:
            _quarantine_event(
                event,
                inbox=paths["inbox"],
                quarantine=paths["quarantine"],
                reason="; ".join(problems),
                summary=summary,
            )
            continue

        finding_key = event["finding_key"]
        event_id = event["event_id"]
        if lq.is_already_handled(finding_key, ledger_rows):
            try:
                lq.move_to_done(event_id, inbox=paths["inbox"], done=paths["done"])
                row = _ledger_row(
                    event,
                    state="deduped",
                    linear_issue=None,
                    reason="ledger already handled",
                )
                _append_ledger(row, ledger=paths["ledger"], ledger_rows=ledger_rows)
            except Exception as exc:
                summary["errors"].append(f"dedupe failed event_id={event_id} error={exc}")
                summary["exit_code"] = 1
                summary["stopped"] = True
                break
            summary["deduped"] += 1
            summary["reports"].append(
                f"deduped event_id={event_id} reason=ledger already handled"
            )
            continue

        try:
            open_issue = linear_client.find_open_issue_by_finding_key(
                finding_key,
                team=team,
                transport=transport,
            )
        except Exception as exc:
            summary["errors"].append(f"linear backstop failed event_id={event_id} error={exc}")
            summary["exit_code"] = 1
            summary["stopped"] = True
            break

        if open_issue:
            issue_id = open_issue.get("id")
            identifier = open_issue.get("identifier")
            if issue_id:
                linear_client.project_status(
                    str(issue_id),
                    (
                        "queue event deduped by finding_key "
                        f"{finding_key}; event_id={event_id}"
                    ),
                    transport=transport,
                )
            try:
                row = _ledger_row(
                    event,
                    state="deduped",
                    linear_issue=str(identifier) if identifier else None,
                    reason="linear backstop finding_key match",
                )
                _append_ledger(row, ledger=paths["ledger"], ledger_rows=ledger_rows)
                lq.move_to_done(event_id, inbox=paths["inbox"], done=paths["done"])
            except Exception as exc:
                summary["errors"].append(
                    f"backstop dedupe failed event_id={event_id} error={exc}"
                )
                summary["exit_code"] = 1
                summary["stopped"] = True
                break
            summary["deduped"] += 1
            if identifier:
                summary["deduped_issues"].append(str(identifier))
            summary["reports"].append(
                f"deduped event_id={event_id} linear_issue={identifier} "
                "reason=linear backstop finding_key match"
            )
            continue

        if creates >= max_creates:
            summary["remaining"].extend(
                str(paths["inbox"] / f"{remaining['event_id']}.json")
                for remaining in sorted_events[index:]
            )
            summary["stopped"] = True
            break

        try:
            if auto_filed_id is None:
                auto_filed_id = linear_client.ensure_label(
                    "auto-filed",
                    team=team,
                    transport=transport,
                )
            pending = _ledger_row(
                event,
                state="pending",
                linear_issue=None,
                reason=None,
            )
            _append_ledger(pending, ledger=paths["ledger"], ledger_rows=ledger_rows)
            created = linear_client.create_issue(
                event["title"],
                _issue_body(event),
                team=team,
                label_ids=[auto_filed_id],
                transport=transport,
            )
            if not created.get("success") or not created.get("identifier"):
                raise RuntimeError("Linear issueCreate failed")
        except Exception as exc:
            summary["errors"].append(f"create failed event_id={event_id} error={exc}")
            summary["exit_code"] = 1
            summary["stopped"] = True
            break

        identifier = str(created["identifier"])
        try:
            filed = _ledger_row(
                event,
                state="filed",
                linear_issue=identifier,
                reason=None,
            )
            _append_ledger(filed, ledger=paths["ledger"], ledger_rows=ledger_rows)
            lq.move_to_done(event_id, inbox=paths["inbox"], done=paths["done"])
        except Exception as exc:
            summary["errors"].append(
                f"post-create local write failed event_id={event_id} "
                f"linear_issue={identifier} error={exc}"
            )
            summary["exit_code"] = 1
            summary["stopped"] = True
            break

        creates += 1
        summary["created"] += 1
        summary["created_issues"].append(identifier)
        summary["reports"].append(f"created event_id={event_id} linear_issue={identifier}")

    return summary


def _print_summary(summary: dict[str, Any]) -> None:
    for report in summary.get("reports", []):
        print(report)
    for error in summary.get("errors", []):
        print(f"ERROR: {error}", file=sys.stderr)

    if summary.get("dry_run"):
        dry = summary.get("dry_run_summary") or {}
        print(
            "dry_run_summary "
            f"created={dry.get('created', 0)} "
            f"deduped_preview={dry.get('deduped_preview', 0)} "
            f"quarantined_preview={dry.get('quarantined_preview', 0)} "
            f"remaining_preview={dry.get('remaining_preview', 0)}"
        )
        return

    print(
        "summary "
        f"created={summary.get('created', 0)} "
        f"created_issues={','.join(summary.get('created_issues', []))} "
        f"deduped={summary.get('deduped', 0)} "
        f"deduped_issues={','.join(summary.get('deduped_issues', []))} "
        f"quarantined={len(summary.get('quarantined', []))} "
        f"remaining={len(summary.get('remaining', []))}"
    )


def main(
    argv: list[str] | None = None,
    *,
    transport: linear_client.Transport | None = None,
) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    mode = parser.add_mutually_exclusive_group()
    mode.add_argument("--dry-run", action="store_true", help="preview only")
    mode.add_argument("--live", action="store_true", help="write to Linear")
    parser.add_argument("--max", dest="max_creates", type=int, default=3)
    parser.add_argument("--queue-root", type=Path, default=lq.DEFAULT_QUEUE_ROOT)
    parser.add_argument("--team", default="Recoil")

    try:
        args = parser.parse_args(argv)
    except SystemExit as exc:
        return int(exc.code)

    live = bool(args.live)
    summary = drain(
        queue_root=args.queue_root,
        live=live,
        max_creates=args.max_creates,
        team=args.team,
        transport=transport,
    )
    _print_summary(summary)
    return int(summary.get("exit_code") or 0)


if __name__ == "__main__":
    raise SystemExit(main())
