#!/usr/bin/env python3
"""Synchronize the authoritative Ralph PRD with Gitea issues.

Gitea is a projection of prd.json. Story completion remains authoritative in
prd.json; this tool only creates/reconciles issue bodies, labels, milestones,
and open/closed/in-progress presentation.
"""
from __future__ import annotations

import argparse
import base64
import hashlib
import json
import os
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[1]
DEFAULT_FEATURE = ROOT / ".scratch" / "distributed-gguf-runtime"
STATUS_LABELS = {
    "ready": ("status:ready", "Ready for Ralph execution"),
    "in-progress": ("status:in-progress", "Currently selected by Ralph"),
    "blocked": ("status:blocked", "Blocked by incomplete dependencies or human gate"),
    "completed": ("status:completed", "Completed and verified in prd.json"),
}


def credential_for(host: str) -> tuple[str, str]:
    result = subprocess.run(
        ["git", "credential", "fill"],
        input=f"protocol=https\nhost={host}\n\n",
        text=True,
        capture_output=True,
        check=True,
    )
    values = dict(line.split("=", 1) for line in result.stdout.splitlines() if "=" in line)
    username, password = values.get("username"), values.get("password")
    if not password:
        raise RuntimeError(f"No Git credential available for {host}")
    return username or "", password


class Gitea:
    def __init__(self, base_url: str, username: str, password: str, dry_run: bool = False):
        self.base = base_url.rstrip("/")
        self.auth = "Basic " + base64.b64encode(f"{username}:{password}".encode()).decode()
        self.dry_run = dry_run

    def request(self, method: str, path: str, payload: Any = None) -> Any:
        url = self.base + path
        body = None if payload is None else json.dumps(payload).encode()
        req = urllib.request.Request(
            url,
            data=body,
            method=method,
            headers={
                "Authorization": self.auth,
                "Accept": "application/json",
                "Content-Type": "application/json",
            },
        )
        if self.dry_run and method not in {"GET", "HEAD"}:
            print(f"DRY-RUN {method} {path}")
            return {}
        try:
            with urllib.request.urlopen(req, timeout=60) as response:
                raw = response.read()
                return json.loads(raw) if raw else {}
        except urllib.error.HTTPError as exc:
            detail = exc.read().decode(errors="replace")[:500]
            raise RuntimeError(f"Gitea {method} {path} -> HTTP {exc.code}: {detail}") from exc

    def paginate(self, path: str) -> list[dict[str, Any]]:
        result: list[dict[str, Any]] = []
        page = 1
        while True:
            separator = "&" if "?" in path else "?"
            batch = self.request("GET", f"{path}{separator}limit=50&page={page}")
            if not batch:
                return result
            result.extend(batch)
            if len(batch) < 50:
                return result
            page += 1


def color(name: str) -> str:
    return "#" + hashlib.sha256(name.encode()).hexdigest()[:6]


def load_schema(feature: Path) -> Any:
    module_path = ROOT / "scripts" / "ralph_prd_schema.py"
    import importlib.util
    spec = importlib.util.spec_from_file_location("ralph_prd_schema", module_path)
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def issue_body(schema: Any, story: dict[str, Any]) -> str:
    data = schema.load_prd(DEFAULT_FEATURE / "prd.json")
    return schema.render_issue_markdown(story, data) + f"\n<!-- ralph-story-id: {story['id']} -->\n"


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("command", choices=["sync"])
    parser.add_argument("--feature-dir", type=Path, default=DEFAULT_FEATURE)
    parser.add_argument("--repo-url", default="https://git.d-popov.com/popov/neuron-tai")
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--reset-in-progress", action="store_true", help="Do not claim a next story after a failed Ralph start")
    args = parser.parse_args()

    feature = args.feature_dir.resolve()
    prd_path = feature / "prd.json"
    data = json.loads(prd_path.read_text())
    stories = data["userStories"]
    repo = urllib.parse.urlparse(args.repo_url)
    if not repo.hostname or not repo.path.strip("/"):
        raise SystemExit("--repo-url must be an HTTPS Gitea repository URL")
    owner, name = repo.path.strip("/").removesuffix(".git").split("/", 1)
    api_base = f"{repo.scheme}://{repo.hostname}/api/v1/repos/{owner}/{name}"
    username, password = credential_for(repo.hostname)
    gitea = Gitea(api_base, username, password, args.dry_run)
    schema = load_schema(feature)
    schema_errors = schema.validate_backlog(data)
    if schema_errors:
        raise SystemExit("PRD validation failed:\n" + "\n".join(schema_errors))

    labels = {x["name"]: x for x in gitea.paginate("/labels")}
    milestones = {x["title"]: x for x in gitea.paginate("/milestones?state=all")}
    issue_list = gitea.paginate("/issues?state=all")
    by_story = {}
    for issue in issue_list:
        marker = f"<!-- ralph-story-id: DGR-"
        body = issue.get("body") or ""
        if marker in body:
            story_id = body.split(marker, 1)[1].split(" -->", 1)[0]
            by_story["DGR-" + story_id] = issue
        elif issue.get("title", "").startswith("DGR-"):
            by_story[issue["title"].split(":", 1)[0]] = issue

    def ensure_label(name_: str, description: str = "") -> dict[str, Any]:
        if name_ in labels:
            return labels[name_]
        created = gitea.request("POST", "/labels", {"name": name_, "color": color(name_), "description": description})
        if args.dry_run:
            created = {"id": -len(labels) - 1, "name": name_}
        labels[name_] = created
        return created

    for story in stories:
        for label in story["labels"]:
            ensure_label(label)
    for label, description in STATUS_LABELS.values():
        ensure_label(label, description)
    for milestone in data.get("milestones", []):
        title = milestone["id"]
        if title not in milestones:
            milestones[title] = gitea.request("POST", "/milestones", {"title": title, "description": milestone["name"] + ": " + milestone["outcome"]})
            if args.dry_run:
                milestones[title] = {"id": -len(milestones), "title": title}

    passed = {s["id"] for s in stories if s.get("passes")}
    next_story = None if args.reset_in_progress else next(
        (
            s
            for s in stories
            if not s.get("passes")
            and s.get("triage") == "ready-for-agent"
            and all(d in passed for d in s["dependsOn"])
        ),
        None,
    )
    mapping = {}
    for story in stories:
        sid = story["id"]
        desired_status = "completed" if story.get("passes") else ("in-progress" if next_story and sid == next_story["id"] else ("ready" if all(d in passed for d in story["dependsOn"]) else "blocked"))
        label_names = list(dict.fromkeys(story["labels"] + [STATUS_LABELS[desired_status][0]]))
        label_ids = [ensure_label(label)["id"] for label in label_names]
        milestone_id = milestones[story["milestone"]]["id"]
        title = f"{sid}: {story['title']}"
        body = issue_body(schema, story)
        issue = by_story.get(sid)
        payload = {"title": title, "body": body, "labels": label_ids, "milestone": milestone_id, "state": "closed" if story.get("passes") else "open"}
        if issue is None:
            issue = gitea.request("POST", "/issues", payload)
            print(f"created {sid} #{issue.get('number')} {desired_status}")
        else:
            issue = gitea.request("PATCH", f"/issues/{issue['number']}", payload)
            print(f"reconciled {sid} #{issue.get('number')} {desired_status}")
        # Gitea's issue PATCH does not reliably replace labels; use the
        # dedicated endpoint so stale status:in-progress labels are removed.
        gitea.request("PUT", f"/issues/{issue['number']}/labels", {"labels": label_ids})
        mapping[sid] = {"number": issue.get("number"), "url": issue.get("html_url"), "state": "closed" if story.get("passes") else "open", "status": desired_status}

    if not args.dry_run:
        (feature / "gitea-issues.json").write_text(json.dumps({"repository": args.repo_url, "stories": mapping}, indent=2) + "\n")
    print(f"synced={len(stories)} next={next_story['id'] if next_story else 'none'} dry_run={args.dry_run}")
    return 0


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