#!/usr/bin/env python3
"""
Sprint CLI — orchestrates sprint lifecycle and context for agents.

Calls the ticket CLI for data queries (no SQL duplication).
Direct DB access only for sprint lifecycle mutations.

Usage:
  sprint status [--sprint N] [--team T]      Sprint progress and ticket overview
  sprint start [--sprint N]                  Activate a planned sprint
  sprint stop [--sprint N]                   Complete an active sprint
  sprint start-work [--sprint N] [--team T]  Full context dump for starting work
  sprint prepare [--sprint N] [--team T]     Prepare next sprint (candidates + gaps)
"""

import json
import os
import sqlite3
import subprocess
import sys
from pathlib import Path

SCRIPT_DIR = Path(__file__).resolve().parent
CONFIG_PATH = SCRIPT_DIR / "config.json"
TICKET_CLI = str(SCRIPT_DIR / "ticket")
PROJECT_ROOT = (SCRIPT_DIR / ".." / "..").resolve()

REMINDER = """---
Reminder: Keep ticket status up to date after finishing work.
  tooling/db/ticket status <id> in_progress  (when starting)
  tooling/db/ticket status <id> done          (when finished)"""


def load_config():
    with open(CONFIG_PATH, "r") as f:
        return json.load(f)


def resolve_db_path():
    """Resolve database path from environment or config."""
    env_path = os.environ.get("PROJECT_DB")
    if env_path:
        return Path(env_path).resolve()
    cfg = load_config()
    db_name = cfg.get("db_name", "project.db")
    db_location = cfg.get("db_location", "parent")
    if db_location == "parent":
        return (SCRIPT_DIR / ".." / ".." / ".." / db_name).resolve()
    elif db_location == "local":
        return (SCRIPT_DIR / ".." / ".." / db_name).resolve()
    else:
        return Path(db_location).resolve() / db_name


def run_ticket(*args):
    """Call the ticket CLI and return parsed JSON."""
    result = subprocess.run(
        [sys.executable, TICKET_CLI] + list(args),
        capture_output=True, text=True
    )
    if result.returncode != 0:
        return {"ok": False, "error": result.stderr.strip()}
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError:
        return {"ok": False, "error": f"Bad ticket output: {result.stdout[:200]}"}


def get_connection():
    """Direct DB connection for lifecycle mutations only."""
    db_path = resolve_db_path()
    conn = sqlite3.connect(str(db_path))
    conn.execute("PRAGMA journal_mode=WAL;")
    conn.execute("PRAGMA foreign_keys=ON;")
    conn.row_factory = sqlite3.Row
    return conn


def parse_flags(args, known_flags):
    """Parse --flag value pairs from args, return (flags_dict, positional_args)."""
    flags = {}
    positional = []
    i = 0
    while i < len(args):
        if args[i].startswith("--") and args[i][2:] in known_flags:
            key = args[i][2:]
            if i + 1 < len(args):
                flags[key] = args[i + 1]
                i += 2
            else:
                positional.append(args[i])
                i += 1
        else:
            positional.append(args[i])
            i += 1
    return flags, positional


def detect_team(flags):
    """Detect team from flags or git branch."""
    if "team" in flags:
        return flags["team"]
    try:
        result = subprocess.run(
            ["git", "branch", "--show-current"],
            capture_output=True, text=True, cwd=str(PROJECT_ROOT)
        )
        branch = result.stdout.strip()
        if branch and branch != "main":
            return branch
    except Exception:
        pass
    return None


def get_all_sprints():
    """Get all sprints via ticket CLI."""
    data = run_ticket("sprint")
    if not data.get("ok"):
        return []
    return data.get("sprints", [])


def detect_sprint(flags, prefer_status=None):
    """Detect sprint from flags or by status preference.

    prefer_status: which status to prefer when auto-detecting.
      'active' for status/start-work/stop
      'planning' for start
      None for prepare (targets next sprint)
    """
    if "sprint" in flags:
        sprint_id = int(flags["sprint"])
        sprints = get_all_sprints()
        for s in sprints:
            if s["id"] == sprint_id:
                return s
        print(f"Error: Sprint {sprint_id} not found.")
        sys.exit(1)

    sprints = get_all_sprints()
    if not sprints:
        print("Error: No sprints found in database.")
        sys.exit(1)

    if prefer_status:
        matching = [s for s in sprints if s["status"] == prefer_status]
        if len(matching) == 1:
            return matching[0]
        if len(matching) > 1:
            ids = ", ".join(str(s["id"]) for s in matching)
            print(f"Error: Multiple {prefer_status} sprints: {ids}. Use --sprint N to specify.")
            sys.exit(1)
        # Fall through: no match for preferred status
        if prefer_status == "active":
            # No active sprint
            print("Error: No active sprint. Use --sprint N to specify.")
            sys.exit(1)
        if prefer_status == "planning":
            print("Error: No sprint in planning status. Use sprint prepare first.")
            sys.exit(1)

    return None


def detect_sprint_for_prepare(flags):
    """For prepare: target the next sprint after the most recent one."""
    if "sprint" in flags:
        sprint_id = int(flags["sprint"])
        sprints = get_all_sprints()
        for s in sprints:
            if s["id"] == sprint_id:
                return s
        # Sprint doesn't exist yet — return a stub
        return {"id": sprint_id, "status": "new", "name": None}

    sprints = get_all_sprints()
    # If there's a planning sprint, use it
    planning = [s for s in sprints if s["status"] == "planning"]
    if len(planning) == 1:
        return planning[0]
    if len(planning) > 1:
        ids = ", ".join(str(s["id"]) for s in planning)
        print(f"Error: Multiple planning sprints: {ids}. Use --sprint N to specify.")
        sys.exit(1)

    # Otherwise target max_id + 1
    if sprints:
        next_id = max(s["id"] for s in sprints) + 1
        return {"id": next_id, "status": "new", "name": None}

    return {"id": 1, "status": "new", "name": None}


def get_tickets_for_sprint(sprint_id, team=None):
    """Get tickets for a sprint, optionally filtered by team."""
    args = ["list", "--sprint", str(sprint_id)]
    if team:
        args += ["--team", team]
    data = run_ticket(*args)
    if not data.get("ok"):
        return []
    return data.get("rows", [])


def get_ticket_deps(ticket_id):
    """Get dependencies for a ticket."""
    data = run_ticket("deps", str(ticket_id))
    if not data.get("ok"):
        return {"blocked_by": [], "blocks": []}
    return data


def get_ticket_detail(ticket_id):
    """Get full ticket detail."""
    data = run_ticket("show", str(ticket_id))
    if not data.get("ok"):
        return None
    return data.get("ticket")


def briefing_path(sprint_id, team):
    """Find the briefing file for a sprint/team if it exists."""
    p = PROJECT_ROOT / "docs" / "sprints" / f"sprint-{sprint_id}" / f"{team}.md"
    if p.exists():
        return str(p.relative_to(PROJECT_ROOT))
    return None


def format_ticket_table(tickets):
    """Format tickets as an aligned table."""
    if not tickets:
        print("  (none)")
        return
    # Header
    print(f"  {'#':<6} {'Title':<50} {'Status':<12} {'Assigned':<10} {'Priority'}")
    print(f"  {'---':<6} {'---':<50} {'---':<12} {'---':<10} {'---'}")
    for t in tickets:
        title = t.get("title", "")
        if len(title) > 48:
            title = title[:45] + "..."
        assigned = t.get("assigned_to") or ""
        print(f"  {t['id']:<6} {title:<50} {t['status']:<12} {assigned:<10} {t['priority']}")


# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------

def cmd_status(args):
    flags, _ = parse_flags(args, ["sprint", "team"])
    sprint = detect_sprint(flags, prefer_status="active")
    team = detect_team(flags)

    tickets = get_tickets_for_sprint(sprint["id"], team)

    # Header
    name = sprint.get("name", f"Sprint {sprint['id']}")
    print(f"=== {name} ({sprint['status']}) ===")
    if sprint.get("goal"):
        print(f"Goal: {sprint['goal']}")
    parts = []
    if sprint.get("start_date"):
        parts.append(f"Started: {sprint['start_date']}")
    if sprint.get("end_date"):
        parts.append(f"Ended: {sprint['end_date']}")
    if team:
        parts.append(f"Team: {team}")
    if parts:
        print(" | ".join(parts))
    print()

    # Progress
    total = len(tickets)
    done = sum(1 for t in tickets if t["status"] == "done")
    pct = int(done / total * 100) if total > 0 else 0
    print(f"Progress: {done}/{total} done ({pct}%)")

    # Status breakdown
    statuses = {}
    for t in tickets:
        statuses[t["status"]] = statuses.get(t["status"], 0) + 1
    status_parts = []
    for s in ["backlog", "ready", "in_progress", "review", "done", "cancelled"]:
        if s in statuses:
            status_parts.append(f"{s}: {statuses[s]}")
    if status_parts:
        print(f"  {' | '.join(status_parts)}")
    print()

    # Ticket table
    print("Tickets:")
    format_ticket_table(tickets)
    print()

    # Blocked tickets
    blocked_lines = []
    for t in tickets:
        if t["status"] == "done":
            continue
        deps = get_ticket_deps(t["id"])
        for b in deps.get("blocked_by", []):
            if b["status"] != "done":
                blocked_lines.append(f"  #{t['id']} blocked by #{b['id']} ({b['status']})")
    if blocked_lines:
        print("Blocked:")
        for line in blocked_lines:
            print(line)
        print()

    # Briefing
    if team:
        bp = briefing_path(sprint["id"], team)
        if bp:
            print(f"Briefing: {bp}")
    else:
        # Show all available briefings
        briefings = []
        for t_name in ["server", "client", "copy", "audio", "visual", "ci", "joint"]:
            bp = briefing_path(sprint["id"], t_name)
            if bp:
                briefings.append(bp)
        if briefings:
            print("Briefings:")
            for bp in briefings:
                print(f"  {bp}")

    print()
    print(REMINDER)


def cmd_start(args):
    flags, _ = parse_flags(args, ["sprint"])
    sprint = detect_sprint(flags, prefer_status="planning")

    if sprint["status"] != "planning":
        print(f"Error: Sprint {sprint['id']} is '{sprint['status']}', expected 'planning'.")
        sys.exit(1)

    # Check ticket count
    tickets = get_tickets_for_sprint(sprint["id"])
    if not tickets:
        print(f"Error: Sprint {sprint['id']} has no tickets. Run sprint prepare first.")
        sys.exit(1)

    # Activate
    conn = get_connection()
    conn.execute(
        "UPDATE sprints SET status='active', start_date=date('now') WHERE id=?",
        (sprint["id"],)
    )
    conn.commit()
    conn.close()

    # Summary
    teams = {}
    for t in tickets:
        team = t.get("team") or "unassigned"
        teams[team] = teams.get(team, 0) + 1

    name = sprint.get("name", f"Sprint {sprint['id']}")
    print(f"Started: {name}")
    print(f"Tickets: {len(tickets)}")
    for team, count in sorted(teams.items()):
        print(f"  {team}: {count}")
    print()
    print(REMINDER)


def cmd_stop(args):
    flags, _ = parse_flags(args, ["sprint"])
    sprint = detect_sprint(flags, prefer_status="active")

    if sprint["status"] != "active":
        print(f"Error: Sprint {sprint['id']} is '{sprint['status']}', expected 'active'.")
        sys.exit(1)

    tickets = get_tickets_for_sprint(sprint["id"])
    done = [t for t in tickets if t["status"] == "done"]
    cancelled = [t for t in tickets if t["status"] == "cancelled"]
    incomplete = [t for t in tickets if t["status"] not in ("done", "cancelled")]

    # Complete
    conn = get_connection()
    conn.execute(
        "UPDATE sprints SET status='completed', end_date=date('now') WHERE id=?",
        (sprint["id"],)
    )
    conn.commit()
    conn.close()

    name = sprint.get("name", f"Sprint {sprint['id']}")
    print(f"Completed: {name}")
    print(f"Done: {len(done)}/{len(tickets)}")
    if cancelled:
        print(f"Cancelled: {len(cancelled)}")
    print()

    if incomplete:
        print("Carry-over candidates (incomplete):")
        format_ticket_table(incomplete)
        print()

    print(REMINDER)


def cmd_start_work(args):
    flags, _ = parse_flags(args, ["sprint", "team"])
    sprint = detect_sprint(flags, prefer_status="active")
    team = detect_team(flags)

    if sprint["status"] != "active":
        print(f"Error: Sprint {sprint['id']} is '{sprint['status']}', expected 'active'.")
        sys.exit(1)

    tickets = get_tickets_for_sprint(sprint["id"], team)

    # Header
    name = sprint.get("name", f"Sprint {sprint['id']}")
    team_label = f" \u2014 {team.title()}" if team else ""
    print(f"=== {name}{team_label} ===")
    if sprint.get("goal"):
        print(f"Goal: {sprint['goal']}")
    parts = [f"Status: {sprint['status']}"]
    if sprint.get("start_date"):
        parts.append(f"Started: {sprint['start_date']}")
    print(" | ".join(parts))
    print()

    # Briefing
    if team:
        bp = briefing_path(sprint["id"], team)
        if bp:
            print(f"Briefing: {bp}")
    # Also check joint briefing
    jbp = briefing_path(sprint["id"], "joint")
    if jbp:
        print(f"Joint briefing: {jbp}")

    # Collect decision refs
    decision_refs = set()
    for t in tickets:
        detail = get_ticket_detail(t["id"])
        if detail and detail.get("decision_ref"):
            decision_refs.add(detail["decision_ref"])
    if decision_refs:
        print(f"Decisions: {', '.join(sorted(decision_refs))}")
    print()

    # Build dependency map
    blocked_by_map = {}  # ticket_id -> [blocker tickets]
    blocks_map = {}      # ticket_id -> [blocked ticket ids]
    for t in tickets:
        deps = get_ticket_deps(t["id"])
        open_blockers = [b for b in deps.get("blocked_by", []) if b["status"] != "done"]
        if open_blockers:
            blocked_by_map[t["id"]] = open_blockers
        blocking = deps.get("blocks", [])
        if blocking:
            blocks_map[t["id"]] = blocking

    # Categorize
    done_tickets = [t for t in tickets if t["status"] == "done"]
    blocked_tickets = [t for t in tickets if t["status"] != "done" and t["id"] in blocked_by_map]
    actionable_tickets = [t for t in tickets if t["status"] != "done" and t["id"] not in blocked_by_map]

    # Actionable
    if actionable_tickets:
        print("Actionable (not blocked, not done):")
        for t in actionable_tickets:
            print(f"  #{t['id']}: {t['title']}")
            # Metadata line
            meta = [t.get("type", ""), f"P:{t['priority']}", f"S:{t['status']}"]
            if t.get("assigned_to"):
                meta.append(f"@{t['assigned_to']}")
            if t.get("team"):
                meta.append(f"Team:{t['team']}")
            detail = get_ticket_detail(t["id"])
            if detail and detail.get("decision_ref"):
                meta.append(f"Ref:{detail['decision_ref']}")
            print(f"    {' | '.join(meta)}")
            if t["id"] in blocks_map:
                block_ids = ", ".join(f"#{b['id']}" for b in blocks_map[t["id"]])
                print(f"    Blocks: {block_ids}")
            print()

    # Blocked
    if blocked_tickets:
        print("Blocked:")
        for t in blocked_tickets:
            blockers = blocked_by_map[t["id"]]
            blocker_str = ", ".join(f"#{b['id']} ({b['status']})" for b in blockers)
            print(f"  #{t['id']}: {t['title']} \u2190 blocked by {blocker_str}")
        print()

    # Done
    if done_tickets:
        print("Done:")
        for t in done_tickets:
            print(f"  #{t['id']}: {t['title']} \u2713")
        print()

    print(REMINDER)


def cmd_prepare(args):
    flags, _ = parse_flags(args, ["sprint", "team"])
    sprint = detect_sprint_for_prepare(flags)
    team = detect_team(flags)

    # Create sprint record if it doesn't exist
    if sprint.get("status") == "new":
        conn = get_connection()
        conn.execute(
            "INSERT INTO sprints (id, name, status) VALUES (?, ?, 'planning')",
            (sprint["id"], f"Sprint {sprint['id']}")
        )
        conn.commit()
        conn.close()
        print(f"Created Sprint {sprint['id']} (planning)")
        sprint["status"] = "planning"
        sprint["name"] = f"Sprint {sprint['id']}"
    elif sprint["status"] not in ("planning", "new"):
        print(f"Warning: Sprint {sprint['id']} is '{sprint['status']}', not 'planning'.")

    print(f"=== Preparing Sprint {sprint['id']} ===")
    print()

    # Previous sprint info
    all_sprints = get_all_sprints()
    prev_sprints = [s for s in all_sprints if s["id"] < sprint["id"]]
    if prev_sprints:
        prev = max(prev_sprints, key=lambda s: s["id"])
        prev_tickets = get_tickets_for_sprint(prev["id"])
        prev_done = sum(1 for t in prev_tickets if t["status"] == "done")
        prev_name = prev.get("name", f"Sprint {prev['id']}")
        print(f"Previous: {prev_name} ({prev['status']}, {prev_done}/{len(prev_tickets)} done)")
        print()

        # Carry-over candidates
        incomplete = [t for t in prev_tickets if t["status"] not in ("done", "cancelled")]
        if team:
            incomplete = [t for t in incomplete if team in (t.get("team") or "")]
        if incomplete:
            print("Carry-over candidates (incomplete from previous sprint):")
            format_ticket_table(incomplete)
            print()

    # Backlog candidates
    backlog_args = ["list", "--status", "backlog"]
    if team:
        backlog_args += ["--team", team]
    backlog_data = run_ticket(*backlog_args)
    backlog = backlog_data.get("rows", []) if backlog_data.get("ok") else []
    # Filter out tickets already assigned to a sprint
    backlog = [t for t in backlog if not t.get("sprint_id")]

    if backlog:
        if team:
            print(f"Backlog candidates ({team}):")
            format_ticket_table(backlog)
        else:
            # Group by team
            by_team = {}
            for t in backlog:
                t_team = t.get("team") or "unassigned"
                by_team.setdefault(t_team, []).append(t)
            print("Backlog candidates (unassigned to any sprint):")
            for t_name in sorted(by_team.keys()):
                print(f"\n  {t_name.title()}:")
                format_ticket_table(by_team[t_name])
        print()

    # Decision coverage gaps
    conn = get_connection()
    cursor = conn.execute("""
        SELECT id, title FROM decisions
        WHERE type='confirmed' AND status='active'
        AND id NOT IN (SELECT DISTINCT decision_ref FROM tickets WHERE decision_ref IS NOT NULL)
        ORDER BY id
    """)
    orphans = cursor.fetchall()
    conn.close()

    if orphans:
        print("Decision coverage gaps (active decisions without tickets):")
        for row in orphans:
            print(f"  {row[0]}: {row[1]}")
        print()

    # Already assigned to this sprint
    assigned = get_tickets_for_sprint(sprint["id"], team)
    if assigned:
        print(f"Already assigned to Sprint {sprint['id']}:")
        format_ticket_table(assigned)
        print()

    print(REMINDER)


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

HELP = """sprint \u2014 sprint lifecycle and context for agents

Usage:
  sprint status [--sprint N] [--team T]      Sprint progress and ticket overview
  sprint start [--sprint N]                  Activate a planned sprint
  sprint stop [--sprint N]                   Complete an active sprint
  sprint start-work [--sprint N] [--team T]  Full context dump for starting work
  sprint prepare [--sprint N] [--team T]     Prepare next sprint (candidates + gaps)

Sprint auto-detection:
  status/start-work  prefer the active sprint
  start              prefer the planning sprint
  stop               prefer the active sprint
  prepare            target next sprint (max id + 1)

Team auto-detection:
  If --team is omitted, uses the current git branch name (unless on main).
  On main with no --team, shows all teams."""


def main():
    if len(sys.argv) < 2 or sys.argv[1] in ("--help", "-h", "help"):
        print(HELP)
        sys.exit(0)

    cmd = sys.argv[1]
    args = sys.argv[2:]

    commands = {
        "status": cmd_status,
        "start": cmd_start,
        "stop": cmd_stop,
        "start-work": cmd_start_work,
        "prepare": cmd_prepare,
    }

    if cmd not in commands:
        print(f"Error: Unknown command '{cmd}'. Use --help for usage.")
        sys.exit(1)

    commands[cmd](args)


if __name__ == "__main__":
    main()
