#!/usr/bin/env python3
"""ACG Bug & Project Tracker — Gitea Issues CLI.

Usage:
    py tracker.py list [--repo REPO] [--type TYPE] [--state STATE] [--client CLIENT] [--json]
    py tracker.py create --repo REPO --title TITLE [--body BODY] --type TYPE --priority PRIO
                         [--component COMP] [--client CLIENT] [--assign WHO] [--discovered DATE]
    py tracker.py show REPO NUMBER
    py tracker.py comment REPO NUMBER --body TEXT
    py tracker.py label REPO NUMBER --add LABEL
    py tracker.py label REPO NUMBER --remove LABEL
    py tracker.py priority REPO NUMBER --set PRIO
    py tracker.py assign REPO NUMBER --to WHO
    py tracker.py close REPO NUMBER [--comment TEXT]
    py tracker.py reopen REPO NUMBER
    py tracker.py status

Environment:
    Reads Gitea API token from the SOPS vault via vault.sh.
    Falls back to GITEA_TOKEN env var if vault is unavailable.
"""

import argparse
import io
import json
import os
import subprocess
import sys
import urllib.request
import urllib.error
from pathlib import Path

# Fix Windows console encoding for unicode issue titles
if sys.stdout.encoding != 'utf-8':
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

API = "https://git.azcomputerguru.com/api/v1"
OWNER = "azcomputerguru"
TRACKED_REPOS = ["gururmm", "guru-rmm", "guru-connect", "claudetools"]
PRIORITY_LABELS = ["P1-critical", "P2-high", "P3-normal", "P4-low"]
ASSIGN_LABELS = ["assigned:howard", "assigned:mike"]

SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent.parent.parent.parent


def get_token():
    """Get Gitea API token from vault or env."""
    tok = os.environ.get("GITEA_TOKEN")
    if tok:
        return tok
    vault_sh = REPO_ROOT / ".claude" / "scripts" / "vault.sh"
    if vault_sh.exists():
        try:
            result = subprocess.run(
                ["bash", str(vault_sh), "get-field", "services/gitea", "credentials.api.api-token"],
                capture_output=True, text=True, timeout=10
            )
            if result.returncode == 0 and result.stdout.strip():
                return result.stdout.strip()
        except Exception:
            pass
    print("[ERROR] No Gitea token found. Set GITEA_TOKEN or configure vault.", file=sys.stderr)
    sys.exit(1)


def api(method, path, token, data=None):
    """Make a Gitea API call using urllib (no external deps)."""
    url = f"{API}{path}"
    headers = {"Authorization": f"token {token}", "Accept": "application/json"}
    body = None
    if data is not None:
        headers["Content-Type"] = "application/json"
        body = json.dumps(data).encode()

    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            raw = resp.read().decode()
            return resp.status, json.loads(raw) if raw else {}
    except urllib.error.HTTPError as e:
        raw = e.read().decode() if e.fp else ""
        try:
            return e.code, json.loads(raw)
        except Exception:
            return e.code, {"error": raw}
    except Exception as e:
        return 0, {"error": str(e)}


def fetch_all_issues(token, repo, state):
    """Fetch all issues for a repo+state with pagination."""
    results = []
    page = 1
    while True:
        code, issues = api("GET",
            f"/repos/{OWNER}/{repo}/issues?state={state}&type=issues&limit=50&page={page}",
            token)
        if code != 200 or not issues:
            break
        results.extend(issues)
        page += 1
        if len(issues) < 50:
            break
    return results


def get_label_ids(token, repo):
    """Fetch label name->id map for a repo."""
    code, labels = api("GET", f"/repos/{OWNER}/{repo}/labels?limit=200", token)
    if code != 200:
        return {}
    return {l["name"]: l["id"] for l in labels}


def cmd_status(args, token):
    """Show issue counts across all tracked repos."""
    print(f"{'Repo':<20} {'Open':>6} {'Closed':>8}")
    print("-" * 36)
    total_open = 0
    total_closed = 0
    for repo in TRACKED_REPOS:
        open_issues = fetch_all_issues(token, repo, "open")
        closed_issues = fetch_all_issues(token, repo, "closed")
        o = len(open_issues)
        c = len(closed_issues)
        total_open += o
        total_closed += c
        print(f"{repo:<20} {o:>6} {c:>8}")
    print("-" * 36)
    print(f"{'Total':<20} {total_open:>6} {total_closed:>8}")


def cmd_list(args, token):
    """List issues with optional filters."""
    repos = [args.repo] if args.repo else TRACKED_REPOS
    state_filter = args.state or "open"
    states = ["open", "closed"] if state_filter == "all" else [state_filter]
    all_issues = []

    for repo in repos:
        for state in states:
            for issue in fetch_all_issues(token, repo, state):
                issue["_repo"] = repo
                all_issues.append(issue)

    if args.type:
        all_issues = [i for i in all_issues
                      if any(l["name"] == args.type for l in i.get("labels", []))]

    if args.client:
        tag = args.client if args.client.startswith("client:") else f"client:{args.client}"
        all_issues = [i for i in all_issues
                      if any(l["name"] == tag for l in i.get("labels", []))]

    if args.json:
        print(json.dumps(all_issues, indent=2, default=str))
        return

    if not all_issues:
        print("[INFO] No issues found.")
        return

    for i in sorted(all_issues, key=lambda x: x["number"]):
        labels = ", ".join(l["name"] for l in i.get("labels", []))
        print(f"  {i['_repo']:<16} #{i['number']:<4} {i['title'][:60]}")
        if labels:
            print(f"  {'':16}       [{labels}]")


def cmd_create(args, token):
    """Create a new issue."""
    # Validate client-project requires --client (fail fast)
    if args.type == "client-project" and not args.client:
        print("[ERROR] --client is required for client-project type", file=sys.stderr)
        sys.exit(1)

    repo = args.repo
    lbl_map = get_label_ids(token, repo)
    label_ids = []

    if args.type not in lbl_map:
        print(f"[ERROR] Type label '{args.type}' not found in {repo}", file=sys.stderr)
        sys.exit(1)
    label_ids.append(lbl_map[args.type])

    if args.priority not in lbl_map:
        print(f"[ERROR] Priority label '{args.priority}' not found in {repo}", file=sys.stderr)
        sys.exit(1)
    label_ids.append(lbl_map[args.priority])

    if args.component and args.component in lbl_map:
        label_ids.append(lbl_map[args.component])

    if args.client:
        tag = args.client if args.client.startswith("client:") else f"client:{args.client}"
        if tag in lbl_map:
            label_ids.append(lbl_map[tag])
        else:
            print(f"[WARNING] Client label '{tag}' not found in {repo} — issue created without it", file=sys.stderr)

    if args.assign:
        atag = f"assigned:{args.assign}" if not args.assign.startswith("assigned:") else args.assign
        if atag in lbl_map:
            label_ids.append(lbl_map[atag])

    body = args.body or ""
    if args.discovered:
        body = f"**Discovered:** {args.discovered}\n\n{body}"

    data = {"title": args.title, "body": body, "labels": label_ids}
    code, result = api("POST", f"/repos/{OWNER}/{repo}/issues", token, data)

    if code == 201:
        print(f"[OK] Created #{result['number']}: {result['title']}")
        print(f"     URL: https://git.azcomputerguru.com/{OWNER}/{repo}/issues/{result['number']}")
    else:
        print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
        sys.exit(1)


def cmd_show(args, token):
    """Show issue detail + comments."""
    code, issue = api("GET", f"/repos/{OWNER}/{args.repo}/issues/{args.number}", token)
    if code != 200:
        print(f"[ERROR] HTTP {code}", file=sys.stderr)
        sys.exit(1)

    labels = ", ".join(l["name"] for l in issue.get("labels", []))
    print(f"#{issue['number']} [{issue['state']}] {issue['title']}")
    print(f"Labels: {labels}")
    print(f"Created: {issue['created_at'][:10]}  Updated: {issue['updated_at'][:10]}")
    if issue.get("body"):
        print(f"\n{issue['body']}")

    code2, comments = api("GET", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/comments", token)
    if code2 == 200 and comments:
        print(f"\n--- {len(comments)} comment(s) ---")
        for c in comments:
            print(f"\n  {c['user']['login']} ({c['created_at'][:10]}):")
            for line in (c.get("body") or "").split("\n"):
                print(f"  {line}")


def cmd_comment(args, token):
    """Add a comment to an issue."""
    code, result = api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/comments",
                       token, {"body": args.body})
    if code == 201:
        print(f"[OK] Comment added to {args.repo} #{args.number}")
    else:
        print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
        sys.exit(1)


def cmd_priority(args, token):
    """Change issue priority (swap labels)."""
    lbl_map = get_label_ids(token, args.repo)
    for p in PRIORITY_LABELS:
        pid = lbl_map.get(p)
        if pid:
            api("DELETE", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels/{pid}", token)
    new_id = lbl_map.get(args.set)
    if new_id:
        api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels", token, {"labels": [new_id]})
        print(f"[OK] {args.repo} #{args.number} priority set to {args.set}")
    else:
        print(f"[ERROR] Priority label '{args.set}' not found", file=sys.stderr)
        sys.exit(1)


def cmd_assign(args, token):
    """Change issue assignee (swap labels)."""
    lbl_map = get_label_ids(token, args.repo)
    for a in ASSIGN_LABELS:
        aid = lbl_map.get(a)
        if aid:
            api("DELETE", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels/{aid}", token)
    atag = f"assigned:{args.to}" if not args.to.startswith("assigned:") else args.to
    new_id = lbl_map.get(atag)
    if new_id:
        api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels", token, {"labels": [new_id]})
        print(f"[OK] {args.repo} #{args.number} assigned to {args.to}")
    else:
        print(f"[ERROR] Assignee label '{atag}' not found", file=sys.stderr)
        sys.exit(1)


def cmd_close(args, token):
    """Close an issue, optionally with a comment."""
    if args.comment:
        api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/comments",
            token, {"body": args.comment})
    code, result = api("PATCH", f"/repos/{OWNER}/{args.repo}/issues/{args.number}",
                       token, {"state": "closed"})
    if code in (200, 201):
        print(f"[OK] Closed {args.repo} #{args.number}")
    else:
        print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
        sys.exit(1)


def cmd_reopen(args, token):
    """Reopen an issue."""
    code, result = api("PATCH", f"/repos/{OWNER}/{args.repo}/issues/{args.number}",
                       token, {"state": "open"})
    if code in (200, 201):
        print(f"[OK] Reopened {args.repo} #{args.number}")
    else:
        print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
        sys.exit(1)


def cmd_label(args, token):
    """Add or remove a label."""
    lbl_map = get_label_ids(token, args.repo)
    if args.add:
        lid = lbl_map.get(args.add)
        if not lid:
            print(f"[ERROR] Label '{args.add}' not found in {args.repo}", file=sys.stderr)
            sys.exit(1)
        api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels", token, {"labels": [lid]})
        print(f"[OK] Added '{args.add}' to {args.repo} #{args.number}")
    elif args.remove:
        lid = lbl_map.get(args.remove)
        if not lid:
            print(f"[ERROR] Label '{args.remove}' not found in {args.repo}", file=sys.stderr)
            sys.exit(1)
        api("DELETE", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels/{lid}", token)
        print(f"[OK] Removed '{args.remove}' from {args.repo} #{args.number}")


def main():
    parser = argparse.ArgumentParser(description="ACG Bug & Project Tracker")
    sub = parser.add_subparsers(dest="command", required=True)

    sub.add_parser("status", help="Show issue counts")

    p_list = sub.add_parser("list", help="List issues")
    p_list.add_argument("--repo", choices=TRACKED_REPOS)
    p_list.add_argument("--type", choices=["bug", "feature", "skill", "project", "security", "client-project", "thought"])
    p_list.add_argument("--state", choices=["open", "closed", "all"], default="open")
    p_list.add_argument("--client")
    p_list.add_argument("--json", action="store_true")

    p_create = sub.add_parser("create", help="Create an issue")
    p_create.add_argument("--repo", required=True, choices=TRACKED_REPOS)
    p_create.add_argument("--title", required=True)
    p_create.add_argument("--body", default="")
    p_create.add_argument("--type", required=True,
                          choices=["bug", "feature", "skill", "project", "security", "client-project", "thought"])
    p_create.add_argument("--priority", required=True, choices=PRIORITY_LABELS)
    p_create.add_argument("--component")
    p_create.add_argument("--client")
    p_create.add_argument("--assign", choices=["howard", "mike"])
    p_create.add_argument("--discovered")

    p_show = sub.add_parser("show", help="Show issue detail")
    p_show.add_argument("repo", choices=TRACKED_REPOS)
    p_show.add_argument("number", type=int)

    p_comment = sub.add_parser("comment", help="Add a comment")
    p_comment.add_argument("repo", choices=TRACKED_REPOS)
    p_comment.add_argument("number", type=int)
    p_comment.add_argument("--body", required=True)

    p_prio = sub.add_parser("priority", help="Change priority")
    p_prio.add_argument("repo", choices=TRACKED_REPOS)
    p_prio.add_argument("number", type=int)
    p_prio.add_argument("--set", required=True, choices=PRIORITY_LABELS)

    p_assign = sub.add_parser("assign", help="Change assignee")
    p_assign.add_argument("repo", choices=TRACKED_REPOS)
    p_assign.add_argument("number", type=int)
    p_assign.add_argument("--to", required=True, choices=["howard", "mike"])

    p_close = sub.add_parser("close", help="Close an issue")
    p_close.add_argument("repo", choices=TRACKED_REPOS)
    p_close.add_argument("number", type=int)
    p_close.add_argument("--comment")

    p_reopen = sub.add_parser("reopen", help="Reopen an issue")
    p_reopen.add_argument("repo", choices=TRACKED_REPOS)
    p_reopen.add_argument("number", type=int)

    p_label = sub.add_parser("label", help="Add/remove a label")
    p_label.add_argument("repo", choices=TRACKED_REPOS)
    p_label.add_argument("number", type=int)
    grp = p_label.add_mutually_exclusive_group(required=True)
    grp.add_argument("--add")
    grp.add_argument("--remove")

    args = parser.parse_args()
    token = get_token()

    commands = {
        "status": cmd_status, "list": cmd_list, "create": cmd_create,
        "show": cmd_show, "comment": cmd_comment, "priority": cmd_priority,
        "assign": cmd_assign, "close": cmd_close, "reopen": cmd_reopen,
        "label": cmd_label,
    }
    commands[args.command](args, token)


if __name__ == "__main__":
    main()
