#!/usr/bin/env python3
"""
cleanup_stale_tasks.py — cancel tasks that are still open in finished/archived workstreams.

Run manually:   python3 scripts/cleanup_stale_tasks.py
Run via make:   make cleanup-stale
Cron example:   0 3 * * * cd ~/state-hub && .venv/bin/python scripts/cleanup_stale_tasks.py

Exit codes:
  0  — ran successfully (zero or more tasks canceled)
  1  — API unreachable or unexpected error
"""

import asyncio
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone

# Make the api package importable when running as `python scripts/cleanup_stale_tasks.py`
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from api.workplan_status import CLOSED_WORKSTREAM_STATUSES, normalize_workstream_status
from api.task_status import OPEN_TASK_STATUSES, normalize_task_status

try:
    from api.events import EventEnvelope, publish_event, shutdown_publisher
except Exception:  # pragma: no cover — event publishing is optional
    EventEnvelope = None  # type: ignore[assignment]
    publish_event = None  # type: ignore[assignment]
    shutdown_publisher = None  # type: ignore[assignment]

API = "http://127.0.0.1:8000"
STALE_STATUSES   = set(OPEN_TASK_STATUSES)
CLOSED_WS_STATUS = set(CLOSED_WORKSTREAM_STATUSES)


def get(path: str) -> list | dict:
    with urllib.request.urlopen(f"{API}{path}") as r:
        return json.loads(r.read())


def _request(method: str, url: str, payload: dict) -> dict:
    """Send a JSON request, following 307/308 redirects with the same method."""
    data = json.dumps(payload).encode()
    for _ in range(5):  # max redirects
        req = urllib.request.Request(
            url,
            data=data,
            headers={"Content-Type": "application/json"},
            method=method,
        )
        try:
            with urllib.request.urlopen(req) as r:
                return json.loads(r.read())
        except urllib.error.HTTPError as e:
            if e.code in (307, 308):
                url = e.headers.get("Location", url)
                if not url.startswith("http"):
                    url = API + url
            else:
                raise
    raise RuntimeError(f"Too many redirects for {url}")


def patch(path: str, payload: dict) -> dict:
    return _request("PATCH", f"{API}{path}", payload)


def post(path: str, payload: dict) -> dict:
    return _request("POST", f"{API}{path}", payload)


def main() -> int:
    print(f"[cleanup-stale] {datetime.now(timezone.utc).isoformat(timespec='seconds')} — scanning…")

    try:
        tasks      = get("/tasks/?limit=500")
        workstreams = get("/workstreams/")
    except urllib.error.URLError as e:
        print(f"[cleanup-stale] ERROR: API unreachable — {e}", file=sys.stderr)
        print("[cleanup-stale] Start the API with: cd ~/state-hub && make api", file=sys.stderr)
        return 1

    closed_ws = {
        w["id"]: w
        for w in workstreams
        if normalize_workstream_status(w["status"]) in CLOSED_WS_STATUS
    }

    stale = [
        t for t in tasks
        if normalize_task_status(t["status"]) in STALE_STATUSES
        and t["workstream_id"] in closed_ws
    ]

    if not stale:
        print("[cleanup-stale] Nothing to cancel — all open tasks belong to active workstreams.")
        return 0

    print(f"[cleanup-stale] Found {len(stale)} stale task(s) in finished/archived workstreams:")

    cancelled = []
    errors    = []
    nats_events: list[tuple[str, "EventEnvelope"]] = []

    for t in stale:
        ws = closed_ws[t["workstream_id"]]
        reason = (
            f"Workstream '{ws['title']}' is {ws['status']}. "
            f"Task was still '{t['status']}' at cleanup time. "
            f"See workplan closure review for actual outcome."
        )
        try:
            patch(
                f"/tasks/{t['id']}/",
                {"status": "cancel", "blocking_reason": reason},
            )
            cancelled.append(t)
            print(f"  canceled   [{t['priority']:8}] {t['title'][:70]}")
            if EventEnvelope is not None:
                subject = "org.statehub.task.stale"
                nats_events.append((
                    subject,
                    EventEnvelope.new(
                        subject,
                        attributes={
                            "task_id": t["id"],
                            "workstream_id": t["workstream_id"],
                            "workstream_status": ws["status"],
                            "task_title": t["title"],
                            "task_status_before": t["status"],
                        },
                    ),
                ))
        except Exception as e:
            errors.append((t, str(e)))
            print(f"  ERROR      {t['title'][:60]} — {e}", file=sys.stderr)

    if nats_events and publish_event is not None and shutdown_publisher is not None:
        async def _flush_events() -> None:
            for subject, env in nats_events:
                await publish_event(subject, env)
            await shutdown_publisher()
        try:
            asyncio.run(_flush_events())
        except Exception as e:  # pragma: no cover — publishing is best-effort
            print(f"[cleanup-stale] WARNING: NATS publish failed — {e}", file=sys.stderr)

    # Emit a single progress event summarising the run
    if cancelled:
        by_ws: dict[str, list] = {}
        for t in cancelled:
            by_ws.setdefault(closed_ws[t["workstream_id"]]["title"], []).append(t["title"])

        summary = (
            f"Stale-task cleanup: canceled {len(cancelled)} task(s) "
            f"across {len(by_ws)} finished workstream(s)"
        )
        detail = {
            "canceled_count": len(cancelled),
            "by_workstream": {ws: titles for ws, titles in by_ws.items()},
            "error_count": len(errors),
        }
        try:
            post("/progress/", {"summary": summary, "event_type": "cleanup", "detail": detail})
            print(f"[cleanup-stale] Progress event recorded.")
        except Exception as e:
            print(f"[cleanup-stale] WARNING: could not record progress event — {e}", file=sys.stderr)

    if errors:
        print(f"[cleanup-stale] Completed with {len(errors)} error(s).")
        return 1

    print(f"[cleanup-stale] Done. {len(cancelled)} task(s) canceled.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
