#!/usr/bin/env python3
"""Backlog operations CLI — request-to-release tracking.

Manages the full lifecycle: requested -> planned -> in_progress -> done -> promoted

Core commands:
    backlog_ops.py add "Title" [--category CAT] [--priority P]
                              [--type afk|hitl|prd] [--parent SR-ID]
                              [--blocked-by SR-ID,SR-ID] [--github]
    backlog_ops.py plan SR-ID
    backlog_ops.py start SR-ID
    backlog_ops.py done SR-ID [--commits HASH1,HASH2]
    backlog_ops.py cancel SR-ID [--reason "why"]
    backlog_ops.py promote [--all] [--no-bump]
        # Default: promote ONE done item → CHANGELOG + auto-bump patch.
        # --all:   promote ALL done items in a single bump (recommended when
        #          multiple items finished the same day; avoids N separate
        #          patches).
        # --no-bump: skip the auto version bump (manual control).
    backlog_ops.py list [--status STATUS] [--category CAT] [--type T]
                        [--parent SR-ID] [--children-of SR-ID]
    backlog_ops.py check
    backlog_ops.py validate

Sub-skills (integrated as sub-commands; see sub-skills/*.md for the playbook):
    backlog_ops.py prd "title" [--summary "..."]
        # Create PRD doc in docs/PRDs/ + parent SR (type=prd)
    backlog_ops.py split SR-PRD-ID --slice "Title|type|blocked_by" [--slice ...]
        # Break a PRD into vertical-slice children
    backlog_ops.py tdd SR-ID
        # Open a TDD worksheet (sub-skills/tdd.md)
    backlog_ops.py diagnose SR-ID
        # Open a bug-diagnosis worksheet (sub-skills/diagnose.md)
    backlog_ops.py prototype SR-PRD-ID --branch logic|ui
        # Scaffold a throwaway prototype dir (sub-skills/prototype.md)
    backlog_ops.py wontfix SR-ID --reason "..." [--alternatives "..."]
        # Move SR to wontfix + write .out-of-scope/ entry (sub-skills/triage.md)
    backlog_ops.py transition SR-ID --to STATUS [--reason "..."]
        # Generic state transition (incl. triage states needs-info, ready-for-agent)
    backlog_ops.py sync-github [--apply]
        # Mirror BACKLOG.md → GitHub Issues (one-way push). Needs gh + JURIS_TRACKER=hybrid.

Tracker backend (env var):
    JURIS_TRACKER=git       # default; file-only, no GitHub
    JURIS_TRACKER=hybrid    # local canonical + GitHub mirror
    JURIS_TRACKER=github    # GitHub-first (loses atomic commits; not recommended)
"""
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
BACKLOG = ROOT / "BACKLOG.md"
CHANGELOG = ROOT / "CHANGELOG.md"
VERSION_BUMP = ROOT / "scripts" / "docs" / "version_bump.py"
PRDS_DIR = ROOT / "docs" / "PRDs"
TDD_DIR = ROOT / "tmp" / "tdd"
DIAGNOSE_DIR = ROOT / "tmp" / "diagnose"
PROTOTYPES_DIR = ROOT / "tmp" / "prototypes"
OUT_OF_SCOPE_DIR = ROOT / ".out-of-scope"

CATEGORIES = [
    "feature", "fix", "refactor", "docs", "test",
    "infra", "security", "performance", "ux",
]

# Type is orthogonal to Category. Category = what kind of change; Type = how it
# is implemented and tracked. `prd` = parent doc; `afk`/`hitl` = vertical slice.
VALID_TYPES = {"afk", "hitl", "prd"}
DEFAULT_TYPE = "afk"

VALID_STATUSES = [
    "requested", "planned", "in_progress", "done", "promoted", "cancelled",
    # Triage states (opt-in; used for bug reports and incoming issues)
    "needs-triage", "needs-info", "ready-for-agent", "ready-for-human", "wontfix",
]

ALLOWED_TRANSITIONS = {
    "requested": {"planned", "in_progress", "needs-triage", "cancelled"},
    "planned": {"in_progress", "cancelled"},
    "in_progress": {"done", "cancelled"},
    "done": {"promoted"},
    # Triage transitions
    "needs-triage": {"needs-info", "ready-for-agent", "ready-for-human",
                     "wontfix", "planned", "cancelled"},
    "needs-info": {"needs-triage", "cancelled"},
    "ready-for-agent": {"in_progress", "cancelled"},
    "ready-for-human": {"in_progress", "cancelled"},
    # Terminal: wontfix has no outgoing transitions (mirror of cancelled)
}

CATEGORY_TO_CHANGELOG = {
    "feature": "Added",
    "fix": "Fixed",
    "refactor": "Changed",
    "docs": "Changed",
    "test": "Changed",
    "infra": "Changed",
    "security": "Security",
    "performance": "Changed",
    "ux": "Changed",
}


def _now():
    return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M GMT")


def _today():
    return datetime.now(timezone.utc).strftime("%Y-%m-%d")


def _read(p):
    return p.read_text(encoding="utf-8") if p.exists() else ""


def _write(p, s):
    p.write_text(s, encoding="utf-8")


def _gen_id():
    prefix = f"SR-{_today()}-"
    existing = re.findall(rf"{re.escape(prefix)}(\d{{3}})", _read(BACKLOG))
    n = max((int(x) for x in existing), default=0) + 1
    return f"{prefix}{n:03d}"


def _find_block(content, item_id):
    # NB: the block regex must accept indented children bullets (for fields
    # like Acceptance criteria) — we match any line that starts with whitespace
    # then `-` up to (but not including) the next blank line + `### `.
    m = re.search(
        rf"(### {re.escape(item_id)}\n(?:[ ]*- .*\n)*)",
        content,
    )
    if not m:
        return None
    block = m.group(1)
    sm = re.search(r"\*\*Status\*\*: `(\w+)`", block)
    return block, (sm.group(1) if sm else "unknown")


def _move_to_section(content, item_id, block, section_header):
    if section_header not in content:
        return None
    content = re.sub(
        rf"### {re.escape(item_id)}\n(?:[ ]*- .*\n)*\n?", "", content,
    )
    idx = content.find(section_header) + len(section_header)
    return content[:idx] + "\n\n" + block + "\n" + content[idx:]


def _append_changelog(title, section, backlog_id):
    c = _read(CHANGELOG)
    marker = "## [Unreleased]"
    u = c.find(marker)
    if u < 0:
        print("ERROR: CHANGELOG.md missing [Unreleased] section", file=sys.stderr)
        return False
    ts = datetime.now(timezone.utc).strftime("%H:%M")
    entry = f"**{title}** _({ts} GMT)_ [{backlog_id}]\n"
    header = f"### {section}"
    s = c.find(header, u)
    if s >= 0:
        eol = c.find("\n", s) + 1
        c = c[:eol] + "\n" + entry + c[eol:]
    else:
        eol = c.find("\n", u) + 1
        c = c[:eol] + f"\n{header}\n\n{entry}" + c[eol:]
    _write(CHANGELOG, c)
    return True


def _tracker_backend() -> str:
    """Returns one of: git | hybrid | github. Default git."""
    val = os.environ.get("JURIS_TRACKER", "git").strip().lower()
    if val not in ("git", "hybrid", "github"):
        return "git"
    return val


def _gh_available() -> bool:
    """Check if gh CLI is installed AND a remote 'origin' exists."""
    try:
        r1 = subprocess.run(["gh", "--version"], capture_output=True, text=True)
        if r1.returncode != 0:
            return False
        r2 = subprocess.run(
            ["git", "remote", "get-url", "origin"],
            capture_output=True, text=True, cwd=ROOT,
        )
        return r2.returncode == 0
    except FileNotFoundError:
        return False


# ─────────────────────────────────────────────────────────────────────────────
# Core commands (backward-compatible)
# ─────────────────────────────────────────────────────────────────────────────


def cmd_add(args):
    if not args:
        print(
            "Usage: backlog_ops.py add \"Title\" [--category CAT] [--priority P] "
            "[--type afk|hitl|prd] [--parent SR-ID] [--blocked-by SR-A,SR-B] [--github]",
            file=sys.stderr,
        )
        return 1
    title = args[0]
    cat, pri, typ = "feature", "normal", DEFAULT_TYPE
    parent, blocked_by, mirror_github = "", "", False
    i = 1
    while i < len(args):
        a = args[i]
        if a == "--category" and i + 1 < len(args):
            cat = args[i + 1]; i += 2
        elif a == "--priority" and i + 1 < len(args):
            pri = args[i + 1]; i += 2
        elif a == "--type" and i + 1 < len(args):
            typ = args[i + 1]; i += 2
        elif a == "--parent" and i + 1 < len(args):
            parent = args[i + 1]; i += 2
        elif a == "--blocked-by" and i + 1 < len(args):
            blocked_by = args[i + 1]; i += 2
        elif a == "--github":
            mirror_github = True; i += 1
        else:
            i += 1
    if cat not in CATEGORIES:
        print(f"ERROR: Invalid category '{cat}'. Valid: {', '.join(CATEGORIES)}", file=sys.stderr)
        return 1
    if typ not in VALID_TYPES:
        print(f"ERROR: Invalid type '{typ}'. Valid: {', '.join(sorted(VALID_TYPES))}", file=sys.stderr)
        return 1
    item_id = _gen_id()
    fields = [
        f"- **Title**: {title}",
        f"- **Status**: `requested`",
        f"- **Type**: `{typ}`",
        f"- **Category**: `{cat}`",
        f"- **Priority**: `{pri}`",
    ]
    if parent:
        fields.append(f"- **Parent**: {parent}")
    if blocked_by:
        fields.append(f"- **Blocked by**: {blocked_by}")
    fields.append(f"- **Requested**: {_now()}")
    entry = f"### {item_id}\n" + "\n".join(fields) + "\n"

    c = _read(BACKLOG)
    section = "## Active"
    idx = c.find(section)
    if idx < 0:
        print("ERROR: BACKLOG.md missing '## Active' section", file=sys.stderr)
        return 1
    pos = idx + len(section)
    c = c[:pos] + "\n\n" + entry + c[pos:]
    _write(BACKLOG, c)

    # If parent given, append item_id to Parent's Children list
    if parent:
        _add_child_to_parent(parent, item_id)

    print(f"Added: {item_id} — {title} [{typ}/{cat}]")
    # GitHub mirror (opt-in)
    backend = _tracker_backend()
    if mirror_github or backend in ("hybrid", "github"):
        _gh_create(item_id, title, typ, cat, parent, blocked_by)
    return 0


def _add_child_to_parent(parent_id: str, child_id: str) -> None:
    c = _read(BACKLOG)
    found = _find_block(c, parent_id)
    if not found:
        return  # parent doesn't exist — skip silently
    block, _ = found
    if "**Children**:" in block:
        new_block = re.sub(
            r"(\*\*Children\*\*: )([^\n]+)",
            lambda m: f"{m.group(1)}{m.group(2).rstrip()}, {child_id}",
            block,
        )
    else:
        # Insert Children after Type or after Title
        anchor = "**Type**:" if "**Type**:" in block else "**Title**:"
        new_block = re.sub(
            rf"(- \*\*{re.escape(anchor[2:-2])}\*\*:[^\n]+\n)",
            lambda m: m.group(1) + f"- **Children**: {child_id}\n",
            block,
            count=1,
        )
    c = c.replace(block, new_block)
    _write(BACKLOG, c)


def cmd_transition(item_id, to_status, commits="", reason=""):
    c = _read(BACKLOG)
    found = _find_block(c, item_id)
    if not found:
        print(f"ERROR: Item {item_id} not found", file=sys.stderr)
        return 1
    block, current = found

    allowed = ALLOWED_TRANSITIONS.get(current, set())
    if to_status not in allowed:
        print(
            f"ERROR: Cannot transition {item_id} from `{current}` to `{to_status}`. "
            f"Allowed: {', '.join(sorted(allowed)) or 'none (terminal)'}",
            file=sys.stderr,
        )
        return 1

    nb = block.replace(f"`{current}`", f"`{to_status}`")
    if to_status == "done":
        nb = nb.rstrip("\n") + f"\n- **Completed**: {_now()}\n"
        if commits:
            nb = nb.rstrip("\n") + f"\n- **Commits**: `{commits}`\n"
    if to_status == "cancelled":
        nb = nb.rstrip("\n") + f"\n- **Cancelled**: {_now()}\n"
        if reason:
            nb = nb.rstrip("\n") + f"\n- **Reason**: {reason}\n"
    if to_status == "wontfix":
        nb = nb.rstrip("\n") + f"\n- **Wontfix**: {_now()}\n"
        if reason:
            nb = nb.rstrip("\n") + f"\n- **Reason**: {reason}\n"
    if to_status in ("needs-info", "needs-triage", "ready-for-agent", "ready-for-human"):
        # Annotate triage transitions so reviewers can audit the state machine.
        nb = nb.rstrip("\n") + f"\n- **{to_status.replace('-', ' ').title()}**: {_now()}\n"
        if reason:
            nb = nb.rstrip("\n") + f"\n- **Reason**: {reason}\n"

    c = c.replace(block, nb)

    section_map = {
        "done": "## Done (Pending Promotion)",
        "promoted": "## Promoted",
        "cancelled": "## Promoted",
        "wontfix": "## Promoted",  # terminal: same section as cancelled/promoted
    }
    if to_status in section_map:
        moved = _move_to_section(c, item_id, nb, section_map[to_status])
        if moved is not None:
            c = moved
    _write(BACKLOG, c)
    print(f"Transitioned: {item_id} → {to_status}")

    # GitHub mirror — close/reopen issue on transition
    if _tracker_backend() in ("hybrid", "github"):
        _gh_transition(item_id, to_status)
    return 0


def cmd_promote(promote_all=False, no_bump=False):
    """Promote done items to CHANGELOG and bump version.

    Default: process ONE done item per invocation. The auto-bump emits a
    new patch version per call — so running this 3 times yields 3 patches
    (0.x → 0.x+3). Use `--all` to consolidate all done items into a single
    patch bump.
    """
    c = _read(BACKLOG)
    n = 0
    for m in re.finditer(r"### (SR-[\d-]+)\n((?:[ ]*- .*\n)*)", c):
        item_id, block = m.group(1), m.group(2)
        if "`done`" not in block:
            continue
        t = re.search(r"\*\*Title\*\*: (.+)", block)
        cat = re.search(r"\*\*Category\*\*: `(\w+)`", block)
        title = t.group(1) if t else item_id
        section = CATEGORY_TO_CHANGELOG.get(cat.group(1) if cat else "feature", "Changed")
        if _append_changelog(title, section, item_id):
            cmd_transition(item_id, "promoted")
            n += 1
        if not promote_all:
            break
    if n:
        print(f"Promoted {n} item(s) to CHANGELOG.")
        if not no_bump and VERSION_BUMP.exists():
            print("\nAuto-bumping patch version...")
            result = subprocess.run(
                [sys.executable, str(VERSION_BUMP), "patch", "--skip-roadmap"],
                capture_output=True, text=True, cwd=ROOT,
            )
            if result.stdout:
                print(result.stdout.rstrip())
            if result.returncode != 0:
                print("WARNING: Version bump failed", file=sys.stderr)
    else:
        print("No done items to promote.")
    return 0


def cmd_list(status_filter="", cat_filter="", type_filter="", parent_filter="",
             children_of=""):
    c = _read(BACKLOG)
    found = False
    for m in re.finditer(r"### (SR-[\d-]+)\n((?:[ ]*- .*\n)*)", c):
        item_id, block = m.group(1), m.group(2)
        t = re.search(r"\*\*Title\*\*: (.+)", block)
        s = re.search(r"\*\*Status\*\*: `(\w+)`", block)
        k = re.search(r"\*\*Category\*\*: `(\w+)`", block)
        ty = re.search(r"\*\*Type\*\*: `(\w+)`", block)
        par = re.search(r"\*\*Parent\*\*: (SR-[\d-]+)", block)
        title = t.group(1) if t else "?"
        status = s.group(1) if s else "?"
        cat = k.group(1) if k else "?"
        typ = ty.group(1) if ty else "-"
        parent = par.group(1) if par else ""
        if status_filter and status != status_filter:
            continue
        if cat_filter and cat != cat_filter:
            continue
        if type_filter and typ != type_filter:
            continue
        if parent_filter and parent != parent_filter:
            continue
        if children_of and parent != children_of:
            continue
        prefix = f"  [{status:12}] [{typ:4}] [{cat:10}]"
        suffix = f" (parent: {parent})" if parent else ""
        print(f"{prefix} {item_id} — {title}{suffix}")
        found = True
    if not found:
        print("No items found.")
    return 0


def cmd_check():
    c = _read(BACKLOG)
    for m in re.finditer(r"### (SR-[\d-]+)\n((?:[ ]*- .*\n)*)", c):
        block = m.group(2)
        if "`in_progress`" in block:
            t = re.search(r"\*\*Title\*\*: (.+)", block)
            print(f"Active: {m.group(1)} — {t.group(1) if t else '?'}")
            return 0
    print("WARNING: No active backlog item. Capture request first: backlog_ops.py add \"Title\" --category <cat>")
    return 1


def cmd_validate():
    c = _read(BACKLOG)
    errors, warnings = [], []
    for section in ["## Active", "## Done (Pending Promotion)", "## Promoted"]:
        if section not in c:
            errors.append(f"Missing section: {section}")
    seen_ids = set()
    items = list(re.finditer(r"### (SR-[\d-]+)\n((?:[ ]*- .*\n)*)", c))
    for m in items:
        item_id, block = m.group(1), m.group(2)
        if item_id in seen_ids:
            errors.append(f"Duplicate ID: {item_id}")
        seen_ids.add(item_id)
        if "**Title**:" not in block:
            errors.append(f"{item_id}: missing Title field")
        if "**Status**:" not in block:
            errors.append(f"{item_id}: missing Status field")
        sm = re.search(r"\*\*Status\*\*: `(\w+)`", block)
        if sm and sm.group(1) not in VALID_STATUSES:
            errors.append(f"{item_id}: invalid status `{sm.group(1)}`")
        cm = re.search(r"\*\*Category\*\*: `(\w+)`", block)
        if cm and cm.group(1) not in CATEGORIES:
            warnings.append(f"{item_id}: unknown category `{cm.group(1)}`")
        elif "**Category**:" not in block:
            warnings.append(f"{item_id}: missing Category field")
        # Type is new — warn only on items that DO have it but is invalid
        ty = re.search(r"\*\*Type\*\*: `(\w+)`", block)
        if ty and ty.group(1) not in VALID_TYPES:
            errors.append(f"{item_id}: invalid type `{ty.group(1)}`")
        # Parent must exist
        par = re.search(r"\*\*Parent\*\*: (SR-[\d-]+)", block)
        if par and par.group(1) not in seen_ids and par.group(1) not in [
            re.search(r"### (SR-[\d-]+)", x.group(0)).group(1) for x in items
        ]:
            warnings.append(f"{item_id}: parent {par.group(1)} not found in backlog")
    print(f"Backlog Validation — {len(items)} items found")
    if errors:
        print(f"\nERRORS ({len(errors)}):")
        for e in errors:
            print(f"  {e}")
    if warnings:
        print(f"\nWARNINGS ({len(warnings)}):")
        for w in warnings:
            print(f"  {w}")
    if not errors and not warnings:
        print("All checks passed.")
    return 1 if errors else 0


# ─────────────────────────────────────────────────────────────────────────────
# New sub-commands: prd, split, tdd, sync-github
# ─────────────────────────────────────────────────────────────────────────────


def cmd_prd(args):
    """Create a PRD doc + parent SR (type=prd).

    Usage: backlog_ops.py prd "Title" [--category CAT] [--summary "..."]

    The PRD doc is created with a skeleton; Claude fills the sections
    (Problem Statement / Solution / User Stories / Decisions) afterwards.
    """
    if not args:
        print(
            "Usage: backlog_ops.py prd \"Title\" [--category CAT] [--summary \"...\"]",
            file=sys.stderr,
        )
        return 1
    title = args[0]
    cat, summary = "feature", ""
    i = 1
    while i < len(args):
        if args[i] == "--category" and i + 1 < len(args):
            cat = args[i + 1]; i += 2
        elif args[i] == "--summary" and i + 1 < len(args):
            summary = args[i + 1]; i += 2
        else:
            i += 1

    # 1) Create the BACKLOG parent SR
    rc = cmd_add([title, "--category", cat, "--type", "prd"])
    if rc != 0:
        return rc
    # Find the just-created id (most recent)
    c = _read(BACKLOG)
    m = re.search(r"### (SR-[\d-]+)\n", c)
    if not m:
        print("ERROR: Could not locate newly-created PRD SR", file=sys.stderr)
        return 1
    prd_id = m.group(1)

    # 2) Create the PRD doc
    PRDS_DIR.mkdir(parents=True, exist_ok=True)
    today = _today()
    slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")[:50]
    seq = len(list(PRDS_DIR.glob(f"PRD-{today}-*.md"))) + 1
    doc_name = f"PRD-{today}-{seq:02d}-{slug}.md"
    doc_path = PRDS_DIR / doc_name

    doc = f"""---
last_verified: {today}
verified_version: 0.0.0
owner: claude-agent
freshness_days: 60
sr_id: {prd_id}
status: requested
---

# {title}

> SR: [{prd_id}]({{relative_link_to_backlog}}) · Created: {_now()}
> Summary: {summary or "(fill in)"}

## Problem Statement

(From the user's perspective: what hurts, why now.)

## Solution

(From the user's perspective: what they will be able to do that they can't today.)

## User Stories

1. As a __, I want to __ so that __.
2. ...

## Implementation Decisions

- (modules to build/modify, interfaces, schema changes, API contracts)

## Testing Decisions

- (what makes a good test here, which modules to cover, prior art)

## Out of Scope

- (what is explicitly NOT in this PRD)

## Further Notes

(anything else)
"""
    doc_path.write_text(doc, encoding="utf-8")

    # 3) Link the doc back into the BACKLOG entry
    found = _find_block(_read(BACKLOG), prd_id)
    if found:
        block, _ = found
        anchor = "- **Type**: `prd`"
        new_block = block.replace(
            anchor + "\n",
            anchor + f"\n- **Doc**: docs/PRDs/{doc_name}\n",
        )
        c = _read(BACKLOG).replace(block, new_block)
        _write(BACKLOG, c)

    print(f"PRD: docs/PRDs/{doc_name}")
    print(f"SR:  {prd_id}")
    print("\nNext: edit the doc to flesh out the PRD, then `split` it into vertical slices:")
    print(f"  backlog_ops.py split {prd_id} --slice 'Title|afk|none' --slice 'Title2|hitl|none'")
    return 0


def cmd_split(args):
    """Break a PRD into vertical-slice children.

    Usage: backlog_ops.py split SR-PRD-ID --slice "Title|type|blocked_by_csv" [--slice ...]

    Examples:
      --slice "Schema migration|afk|none"
      --slice "API endpoint|afk|SR-2026-...-002"
      --slice "UX review|hitl|none"
    """
    if not args:
        print(
            "Usage: backlog_ops.py split SR-PRD-ID --slice 'Title|type|blocked_by' [--slice ...]",
            file=sys.stderr,
        )
        return 1
    prd_id = args[0]
    slices = []
    for i, a in enumerate(args[1:], 1):
        if a == "--slice" and i + 1 < len(args):
            slices.append(args[i + 1])

    if not slices:
        print("ERROR: no --slice given", file=sys.stderr)
        return 1

    # Validate parent exists and is a PRD
    c = _read(BACKLOG)
    found = _find_block(c, prd_id)
    if not found:
        print(f"ERROR: Parent {prd_id} not found", file=sys.stderr)
        return 1
    block, _ = found
    if "**Type**: `prd`" not in block:
        print(f"WARNING: Parent {prd_id} is not type=prd; continuing anyway", file=sys.stderr)

    # Inherit parent's category for children unless slice specifies otherwise
    cat_m = re.search(r"\*\*Category\*\*: `(\w+)`", block)
    parent_cat = cat_m.group(1) if cat_m else "feature"

    created = []
    for s in slices:
        parts = s.split("|")
        if len(parts) < 2:
            print(f"ERROR: bad slice spec '{s}' — need 'Title|type[|blocked_by]'", file=sys.stderr)
            continue
        title = parts[0].strip()
        typ = parts[1].strip().lower()
        if typ not in ("afk", "hitl"):
            print(f"ERROR: slice type must be afk|hitl, got '{typ}'", file=sys.stderr)
            continue
        blocked_by = parts[2].strip() if len(parts) >= 3 else "none"
        blocked_arg = ["--blocked-by", blocked_by] if blocked_by and blocked_by.lower() != "none" else []

        add_args = [title, "--category", parent_cat, "--type", typ,
                    "--parent", prd_id] + blocked_arg
        # Allow callers to inline new IDs by translating "SR-PREV" placeholders
        # using the just-created list. Useful when slices block each other:
        #   --slice "B|afk|SR-PREV-001"   means "blocked by the first slice".
        if blocked_arg and "SR-PREV-" in blocked_arg[1]:
            idx = int(blocked_arg[1].replace("SR-PREV-", "")) - 1
            if 0 <= idx < len(created):
                add_args[-1] = created[idx]

        rc = cmd_add(add_args)
        if rc != 0:
            continue
        # Pull the new id (most recent matching title)
        c2 = _read(BACKLOG)
        m = re.search(r"### (SR-[\d-]+)\n", c2)
        if m:
            created.append(m.group(1))

    print(f"\nSplit done: {len(created)}/{len(slices)} slices created under {prd_id}")
    return 0


def cmd_tdd(args):
    """Open a TDD worksheet for an item.

    Usage: backlog_ops.py tdd SR-ID

    Creates tmp/tdd/{SR-ID}.md with a red-green-refactor checklist.
    """
    if not args:
        print("Usage: backlog_ops.py tdd SR-ID", file=sys.stderr)
        return 1
    item_id = args[0]
    c = _read(BACKLOG)
    found = _find_block(c, item_id)
    if not found:
        print(f"ERROR: Item {item_id} not found", file=sys.stderr)
        return 1
    block, _ = found
    title_m = re.search(r"\*\*Title\*\*: (.+)", block)
    title = title_m.group(1) if title_m else item_id

    TDD_DIR.mkdir(parents=True, exist_ok=True)
    worksheet = TDD_DIR / f"{item_id}.md"
    if not worksheet.exists():
        worksheet.write_text(f"""# TDD worksheet — {item_id}

**Title**: {title}
**Started**: {_now()}

## Behaviors to test (prioritized, public interface only)

- [ ] (behavior 1)
- [ ] (behavior 2)
- [ ] (behavior 3)

## Red → Green log

| # | Test (one sentence) | RED commit | GREEN commit | Notes |
|---|---|---|---|---|
| 1 |   |   |   |   |
| 2 |   |   |   |   |

## Refactor candidates (after all GREEN)

- [ ] (candidate 1)

## Rules

- One test at a time.
- Only enough code to pass current test.
- Don't anticipate future tests.
- Never refactor while RED — get to GREEN first.
- Tests use public interface only; survive internal refactors.
""", encoding="utf-8")
    print(f"TDD worksheet: {worksheet}")
    print(f"Item: {item_id} — {title}")
    return 0


def cmd_diagnose(args):
    """Open a diagnosis worksheet for a bug SR.

    Usage: backlog_ops.py diagnose SR-ID

    Creates tmp/diagnose/{SR-ID}.md with the 6-phase checklist from
    sub-skills/diagnose.md. Worksheet stays local (tmp/ is gitignored).
    """
    if not args:
        print("Usage: backlog_ops.py diagnose SR-ID", file=sys.stderr)
        return 1
    item_id = args[0]
    c = _read(BACKLOG)
    found = _find_block(c, item_id)
    if not found:
        print(f"ERROR: Item {item_id} not found", file=sys.stderr)
        return 1
    block, _ = found
    title_m = re.search(r"\*\*Title\*\*: (.+)", block)
    title = title_m.group(1) if title_m else item_id

    DIAGNOSE_DIR.mkdir(parents=True, exist_ok=True)
    worksheet = DIAGNOSE_DIR / f"{item_id}.md"
    if not worksheet.exists():
        worksheet.write_text(f"""# Diagnose worksheet — {item_id}

**Title**: {title}
**Started**: {_now()}

## Phase 1 — Feedback loop (this is the skill)
- [ ] Loop type chosen: (failing test / curl / CLI / browser / replay / harness / fuzz / bisect / differential / HITL)
- [ ] Loop is fast (<10s)
- [ ] Loop is deterministic
- [ ] Signal is sharp (asserts on the specific symptom)
- [ ] Command to run the loop: `...`

## Phase 2 — Reproduce
- [ ] Loop reproduces the failure
- [ ] Failure matches the user's report (not a nearby failure)
- [ ] Exact symptom captured: `...`

## Phase 3 — Hypothesise (3-5 ranked, falsifiable)
1. **H1 (most likely)**: ... | Prediction: ...
2. **H2**: ... | Prediction: ...
3. **H3**: ... | Prediction: ...
- [ ] Shown to user before testing

## Phase 4 — Instrument
- [ ] Debug tag prefix chosen: `[DEBUG-xxxx]`
- [ ] One variable changed at a time

## Phase 5 — Fix + regression test
- [ ] Correct seam exists for regression test (else document why not)
- [ ] Failing test written
- [ ] Fix applied
- [ ] Test passes
- [ ] Original loop no longer reproduces

## Phase 6 — Cleanup + post-mortem
- [ ] All `[DEBUG-xxxx]` removed (grep)
- [ ] Throwaway harnesses deleted
- [ ] Winning hypothesis in commit message
- [ ] What would have prevented this? `...`
""", encoding="utf-8")
    print(f"Diagnose worksheet: {worksheet}")
    print(f"Item: {item_id} — {title}")
    return 0


def cmd_prototype(args):
    """Scaffold a throwaway prototype directory for a PRD-stage decision.

    Usage: backlog_ops.py prototype SR-PRD-ID --branch logic|ui
    """
    if not args:
        print("Usage: backlog_ops.py prototype SR-PRD-ID --branch logic|ui",
              file=sys.stderr)
        return 1
    item_id = args[0]
    branch = ""
    for i, a in enumerate(args[1:], 1):
        if a == "--branch" and i + 1 < len(args):
            branch = args[i + 1]
    if branch not in ("logic", "ui"):
        print("ERROR: --branch must be 'logic' or 'ui'", file=sys.stderr)
        return 1

    c = _read(BACKLOG)
    found = _find_block(c, item_id)
    if not found:
        print(f"ERROR: Item {item_id} not found", file=sys.stderr)
        return 1
    block, _ = found
    title_m = re.search(r"\*\*Title\*\*: (.+)", block)
    title = title_m.group(1) if title_m else item_id

    proto_dir = PROTOTYPES_DIR / item_id
    proto_dir.mkdir(parents=True, exist_ok=True)
    readme = proto_dir / "README.md"
    notes = proto_dir / "NOTES.md"
    if not readme.exists():
        readme.write_text(f"""# Prototype — {item_id} ({branch})

**Title**: {title}
**Branch**: {branch}
**Started**: {_now()}

## Question being answered

(One sentence: the specific design question this prototype answers.)

## Assumption (if user was unreachable when choosing branch)

(If applicable, state the assumption.)

## How to run

```bash
bash run.sh
```

## Rules

- Throwaway from day one — DO NOT productionise this code.
- No persistence by default. State lives in memory.
- Surface the full state after every action.
- Capture the answer in NOTES.md before deleting this directory.
""", encoding="utf-8")
    if not notes.exists():
        notes.write_text(f"""# NOTES — {item_id}

## The answer

(To be filled in when the prototype has done its job.)

## What we learned

- ...

## Decision

- ...

## Next step

Either: delete this directory, OR fold the validated decision into real code
(and reference this prototype in the PRD's Implementation Decisions section).
""", encoding="utf-8")
    runner = proto_dir / "run.sh"
    if not runner.exists():
        runner.write_text("#!/bin/bash\n# PROTOTYPE — wipe me. Replace with the actual command.\necho 'Add your prototype runner here'\n",
                          encoding="utf-8")
        runner.chmod(0o755)

    print(f"Prototype scaffold: {proto_dir}")
    print(f"  README: {readme.relative_to(ROOT)}")
    print(f"  NOTES:  {notes.relative_to(ROOT)}")
    print(f"  runner: {runner.relative_to(ROOT)}")
    return 0


def cmd_wontfix(args):
    """Move an enhancement SR to wontfix and log to .out-of-scope/.

    Usage: backlog_ops.py wontfix SR-ID --reason "why" [--alternatives "..."]
    """
    if not args:
        print("Usage: backlog_ops.py wontfix SR-ID --reason \"why\" [--alternatives \"...\"]",
              file=sys.stderr)
        return 1
    item_id = args[0]
    reason = ""
    alternatives = ""
    for i, a in enumerate(args[1:], 1):
        if a == "--reason" and i + 1 < len(args):
            reason = args[i + 1]
        if a == "--alternatives" and i + 1 < len(args):
            alternatives = args[i + 1]
    if not reason:
        print("ERROR: --reason is required", file=sys.stderr)
        return 1

    c = _read(BACKLOG)
    found = _find_block(c, item_id)
    if not found:
        print(f"ERROR: Item {item_id} not found", file=sys.stderr)
        return 1
    block, _ = found
    title_m = re.search(r"\*\*Title\*\*: (.+)", block)
    title = title_m.group(1) if title_m else item_id

    OUT_OF_SCOPE_DIR.mkdir(parents=True, exist_ok=True)
    slug = re.sub(r"[^a-z0-9-]+", "-", title.lower()).strip("-")[:40] or "untitled"
    doc = OUT_OF_SCOPE_DIR / f"{_today()}-{slug}.md"
    doc.write_text(f"""# Out-of-scope: {title}

**Original SR**: {item_id}
**Date**: {_today()}
**Decision**: wontfix

## Why

{reason}

## Alternatives

{alternatives or "(none documented)"}
""", encoding="utf-8")

    rc = cmd_transition(item_id, "wontfix", reason=reason)
    if rc == 0:
        print(f"Out-of-scope record: {doc.relative_to(ROOT)}")
    return rc


def cmd_sync_github(args):
    """Mirror BACKLOG.md → GitHub Issues (one-way).

    Usage: backlog_ops.py sync-github [--apply]

    Without --apply: dry-run, prints what would change.
    With --apply: actually calls `gh issue create/close/comment`.

    Requires:
      - `gh` CLI installed and authenticated
      - `origin` remote configured to a GitHub repo
      - `JURIS_TRACKER=hybrid` or `github` env var (else no-op)
    """
    apply = "--apply" in args
    backend = _tracker_backend()
    if backend not in ("hybrid", "github"):
        print(f"INFO: JURIS_TRACKER={backend} — sync-github is a no-op. "
              f"Set JURIS_TRACKER=hybrid to enable.")
        return 0
    if not _gh_available():
        print("ERROR: gh CLI not available OR no origin remote. "
              "Run `gh repo create beird/<name> --private --source . --remote origin` first.",
              file=sys.stderr)
        return 1

    c = _read(BACKLOG)
    todo = []
    for m in re.finditer(r"### (SR-[\d-]+)\n((?:[ ]*- .*\n)*)", c):
        item_id, block = m.group(1), m.group(2)
        gh_m = re.search(r"\*\*GitHub\*\*: #(\d+)", block)
        status_m = re.search(r"\*\*Status\*\*: `(\w+)`", block)
        status = status_m.group(1) if status_m else "?"
        if not gh_m:
            todo.append(("create", item_id, block, status))
        elif status in ("promoted", "cancelled"):
            todo.append(("close", item_id, gh_m.group(1), status))

    print(f"\nDry-run preview ({len(todo)} action(s)):")
    for op, *rest in todo:
        if op == "create":
            _, item_id, _, status = rest
            print(f"  CREATE issue for {item_id} (status: {status})")
        else:
            _, item_id, issue_num, status = rest
            print(f"  CLOSE issue #{issue_num} for {item_id} (status: {status})")

    if not apply:
        print("\n(dry-run; pass --apply to execute)")
        return 0

    # Real execution
    for op, *rest in todo:
        if op == "create":
            _, item_id, block, status = rest
            title_m = re.search(r"\*\*Title\*\*: (.+)", block)
            cat_m = re.search(r"\*\*Category\*\*: `(\w+)`", block)
            typ_m = re.search(r"\*\*Type\*\*: `(\w+)`", block)
            title = title_m.group(1) if title_m else item_id
            cat = cat_m.group(1) if cat_m else "feature"
            typ = typ_m.group(1) if typ_m else "afk"
            labels = [f"category/{cat}", typ]
            if typ == "afk":
                labels.append("ready-for-agent")
            body = f"Backlog ID: `{item_id}`\n\nGenerated from `BACKLOG.md` by `backlog_ops.py sync-github`.\nDo NOT edit on GitHub — edit in `BACKLOG.md` and re-sync."
            cmd = ["gh", "issue", "create", "--title", f"[{item_id}] {title}",
                   "--body", body]
            for L in labels:
                cmd += ["--label", L]
            r = subprocess.run(cmd, capture_output=True, text=True, cwd=ROOT)
            if r.returncode == 0:
                # Extract issue number from output (URL ends in /N)
                num_m = re.search(r"/(\d+)$", r.stdout.strip())
                if num_m:
                    _annotate_github_ref(item_id, num_m.group(1))
                    print(f"  OK CREATE {item_id} → #{num_m.group(1)}")
            else:
                print(f"  FAIL CREATE {item_id}: {r.stderr.strip()}")
        else:
            _, item_id, issue_num, status = rest
            r = subprocess.run(
                ["gh", "issue", "close", issue_num, "--reason",
                 "completed" if status == "promoted" else "not planned"],
                capture_output=True, text=True, cwd=ROOT,
            )
            if r.returncode == 0:
                print(f"  OK CLOSE #{issue_num} ({item_id})")
            else:
                print(f"  FAIL CLOSE #{issue_num}: {r.stderr.strip()}")
    return 0


def _annotate_github_ref(item_id: str, issue_num: str) -> None:
    c = _read(BACKLOG)
    found = _find_block(c, item_id)
    if not found:
        return
    block, _ = found
    if "**GitHub**:" in block:
        return  # already annotated
    # Insert after Status
    new_block = re.sub(
        r"(- \*\*Status\*\*: `[^`]+`\n)",
        lambda m: m.group(1) + f"- **GitHub**: #{issue_num}\n",
        block,
        count=1,
    )
    _write(BACKLOG, c.replace(block, new_block))


def _gh_create(item_id: str, title: str, typ: str, cat: str,
               parent: str, blocked_by: str) -> None:
    """Best-effort GitHub mirror on add. Silent if gh not available."""
    if not _gh_available():
        return
    body_lines = [f"Backlog ID: `{item_id}`"]
    if parent:
        body_lines.append(f"Parent: {parent}")
    if blocked_by:
        body_lines.append(f"Blocked by: {blocked_by}")
    body_lines.append("")
    body_lines.append("Generated from `BACKLOG.md` by `backlog_ops.py`.")
    labels = [f"category/{cat}", typ]
    if typ == "afk":
        labels.append("ready-for-agent")
    cmd = ["gh", "issue", "create", "--title", f"[{item_id}] {title}",
           "--body", "\n".join(body_lines)]
    for L in labels:
        cmd += ["--label", L]
    r = subprocess.run(cmd, capture_output=True, text=True, cwd=ROOT)
    if r.returncode == 0:
        num_m = re.search(r"/(\d+)$", r.stdout.strip())
        if num_m:
            _annotate_github_ref(item_id, num_m.group(1))
            print(f"  [github] mirrored as #{num_m.group(1)}")


def _gh_transition(item_id: str, to_status: str) -> None:
    """Close GitHub issue on terminal transitions."""
    if to_status not in ("promoted", "cancelled"):
        return
    if not _gh_available():
        return
    c = _read(BACKLOG)
    found = _find_block(c, item_id)
    if not found:
        return
    block, _ = found
    gh_m = re.search(r"\*\*GitHub\*\*: #(\d+)", block)
    if not gh_m:
        return
    issue_num = gh_m.group(1)
    reason = "completed" if to_status == "promoted" else "not planned"
    subprocess.run(
        ["gh", "issue", "close", issue_num, "--reason", reason],
        capture_output=True, text=True, cwd=ROOT,
    )


# ─────────────────────────────────────────────────────────────────────────────
# Entry point
# ─────────────────────────────────────────────────────────────────────────────


def main():
    if len(sys.argv) < 2:
        print(__doc__)
        return 0
    cmd = sys.argv[1]

    if cmd == "add":
        return cmd_add(sys.argv[2:])

    if cmd in ("plan", "start", "done"):
        if len(sys.argv) < 3:
            print(f"Usage: backlog_ops.py {cmd} SR-ID", file=sys.stderr)
            return 1
        target = {"plan": "planned", "start": "in_progress", "done": "done"}[cmd]
        commits = ""
        for i, a in enumerate(sys.argv[3:], 3):
            if a == "--commits" and i + 1 < len(sys.argv):
                commits = sys.argv[i + 1]
        return cmd_transition(sys.argv[2], target, commits=commits)

    if cmd == "cancel":
        if len(sys.argv) < 3:
            print("Usage: backlog_ops.py cancel SR-ID [--reason \"...\"]", file=sys.stderr)
            return 1
        reason = ""
        for i, a in enumerate(sys.argv[3:], 3):
            if a == "--reason" and i + 1 < len(sys.argv):
                reason = sys.argv[i + 1]
        return cmd_transition(sys.argv[2], "cancelled", reason=reason)

    if cmd == "promote":
        return cmd_promote("--all" in sys.argv, "--no-bump" in sys.argv)

    if cmd == "list":
        sf, cf, tf, pf, co = "", "", "", "", ""
        for i, a in enumerate(sys.argv[2:], 2):
            if a == "--status" and i + 1 < len(sys.argv):
                sf = sys.argv[i + 1]
            if a == "--category" and i + 1 < len(sys.argv):
                cf = sys.argv[i + 1]
            if a == "--type" and i + 1 < len(sys.argv):
                tf = sys.argv[i + 1]
            if a == "--parent" and i + 1 < len(sys.argv):
                pf = sys.argv[i + 1]
            if a == "--children-of" and i + 1 < len(sys.argv):
                co = sys.argv[i + 1]
        return cmd_list(sf, cf, tf, pf, co)

    if cmd == "check":
        return cmd_check()

    if cmd == "validate":
        return cmd_validate()

    if cmd == "prd":
        return cmd_prd(sys.argv[2:])

    if cmd == "split":
        return cmd_split(sys.argv[2:])

    if cmd == "tdd":
        return cmd_tdd(sys.argv[2:])

    if cmd == "diagnose":
        return cmd_diagnose(sys.argv[2:])

    if cmd == "prototype":
        return cmd_prototype(sys.argv[2:])

    if cmd == "wontfix":
        return cmd_wontfix(sys.argv[2:])

    if cmd == "transition":
        # Generic transition: backlog_ops.py transition SR-ID --to STATUS [--reason "..."]
        if len(sys.argv) < 5 or sys.argv[3] != "--to":
            print("Usage: backlog_ops.py transition SR-ID --to STATUS [--reason \"...\"]",
                  file=sys.stderr)
            return 1
        item_id, to_status = sys.argv[2], sys.argv[4]
        reason = ""
        for i, a in enumerate(sys.argv[5:], 5):
            if a == "--reason" and i + 1 < len(sys.argv):
                reason = sys.argv[i + 1]
        return cmd_transition(item_id, to_status, reason=reason)

    if cmd == "sync-github":
        return cmd_sync_github(sys.argv[2:])

    print(f"Unknown command: {cmd}", file=sys.stderr)
    return 1


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