"""Requirement-traceability MCP server.

Provides tools for mapping requirements to Linear tickets,
generating implementation checklists, and analyzing traceability coverage.

Replaces the former requirement-traceability Claude Code plugin.
Uses elspais for requirement data and the Linear GraphQL API for tickets.
"""

from __future__ import annotations

import json
import os
import re
import subprocess
from pathlib import Path

import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("traceability", instructions="Requirement-to-ticket traceability tools")

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

LINEAR_API = "https://api.linear.app/graphql"


def _linear_token() -> str:
    token = os.environ.get("LINEAR_API_TOKEN", "")
    if not token:
        raise ValueError("LINEAR_API_TOKEN environment variable not set")
    return token


def _linear_query(query: str, variables: dict | None = None) -> dict:
    """Execute a Linear GraphQL query."""
    payload: dict = {"query": query}
    if variables:
        payload["variables"] = variables
    resp = httpx.post(
        LINEAR_API,
        json=payload,
        headers={"Authorization": _linear_token(), "Content-Type": "application/json"},
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    if "errors" in data:
        raise ValueError(f"Linear API error: {data['errors']}")
    return data.get("data", {})


def _get_requirement(req_id: str) -> dict | None:
    """Get requirement details via elspais CLI."""
    try:
        result = subprocess.run(
            ["elspais", "get", req_id, "--json"],
            capture_output=True, text=True, timeout=10,
        )
        if result.returncode == 0 and result.stdout.strip():
            return json.loads(result.stdout)
    except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):
        pass
    return None


def _parse_reqs_from_text(text: str) -> list[str]:
    """Extract REQ-{p|o|d}NNNNN references from text."""
    return re.findall(r"REQ-[pod]\d{5}", text)


# ---------------------------------------------------------------------------
# Subsystem definitions for checklist generation
# ---------------------------------------------------------------------------

SUBSYSTEMS = {
    "Database & Auth (Supabase)": [
        "database", "schema", "rls", "postgres", "auth", "user", "role",
        "permission", "audit", "event", "migration",
    ],
    "Mobile App (Flutter)": [
        "mobile", "app", "flutter", "dart", "offline", "sync", "patient", "diary", "ui",
    ],
    "Web Portal": [
        "web", "portal", "sponsor", "dashboard", "investigator",
    ],
    "CI/CD Pipeline": [
        "workflow", "deployment", "build", "test", "automation", "ci", "cd",
    ],
    "Compliance & Documentation": [
        "fda", "alcoa", "audit trail", "traceability", "validation", "compliance",
    ],
    "Infrastructure": [
        "docker", "container", "hosting", "deploy", "cdn",
    ],
    "Secrets Management": [
        "secrets", "credentials", "token", "env var", "doppler",
    ],
    "Backup & Recovery": [
        "backup", "recovery", "retention", "restore", "archive",
    ],
}


def _detect_subsystems(text: str) -> list[str]:
    """Detect which subsystems are relevant based on keyword matching."""
    text_lower = text.lower()
    matched = []
    for subsystem, keywords in SUBSYSTEMS.items():
        if any(kw in text_lower for kw in keywords):
            matched.append(subsystem)
    return matched


# ---------------------------------------------------------------------------
# MCP Tools
# ---------------------------------------------------------------------------


@mcp.tool()
def find_untracked_requirements(
    level: str = "all",
    team_key: str = "CUR",
) -> str:
    """Find requirements that don't have corresponding Linear tickets.

    Args:
        level: Filter by requirement level ('prd', 'ops', 'dev', or 'all').
        team_key: Linear team key prefix for ticket identifiers (default: CUR).
    """
    # Get all requirements via elspais
    try:
        result = subprocess.run(
            ["elspais", "list", "--json"],
            capture_output=True, text=True, timeout=30,
        )
        if result.returncode != 0:
            return f"Error running elspais list: {result.stderr}"
        requirements = json.loads(result.stdout)
    except (FileNotFoundError, json.JSONDecodeError) as e:
        return f"Error getting requirements: {e}"

    # Filter by level if specified
    if level != "all":
        type_char = {"prd": "p", "ops": "o", "dev": "d"}.get(level, level)
        requirements = [r for r in requirements if r.get("id", "").startswith(f"REQ-{type_char}")]

    # Search Linear for tickets mentioning each requirement
    untracked = []
    for req in requirements:
        req_id = req.get("id", "")
        query = f"""
        query {{ issueSearch(filter: {{ or: [
            {{ title: {{ contains: "{req_id}" }} }},
            {{ description: {{ contains: "{req_id}" }} }}
        ] }}, first: 1) {{ nodes {{ id identifier }} }} }}
        """
        try:
            data = _linear_query(query)
            nodes = data.get("issueSearch", {}).get("nodes", [])
            if not nodes:
                untracked.append({"id": req_id, "title": req.get("title", "")})
        except Exception:
            untracked.append({"id": req_id, "title": req.get("title", ""), "note": "search failed"})

    return json.dumps({
        "total_requirements": len(requirements),
        "untracked_count": len(untracked),
        "untracked": untracked,
    }, indent=2)


@mcp.tool()
def create_requirement_ticket(
    req_id: str,
    team_id: str | None = None,
    priority: int = 3,
) -> str:
    """Create a Linear ticket for a requirement.

    Args:
        req_id: Requirement ID (e.g., REQ-p00042).
        team_id: Linear team UUID. Defaults to LINEAR_TEAM_ID env var.
        priority: Ticket priority (1=Urgent, 2=High, 3=Normal, 4=Low).
    """
    team = team_id or os.environ.get("LINEAR_TEAM_ID", "")
    if not team:
        return "Error: LINEAR_TEAM_ID not set and no team_id provided"

    req = _get_requirement(req_id)
    if not req:
        return f"Error: Requirement {req_id} not found"

    title = req.get("title", req_id)
    file_path = req.get("file", "")
    description = f"""## Requirement: {req_id}

**{title}**

**Source**: `{file_path}`

### Assertions
"""
    for assertion in req.get("assertions", []):
        label = assertion.get("label", "")
        text = assertion.get("text", "")
        description += f"- **{label}**: {text}\n"

    mutation = """
    mutation($title: String!, $teamId: String!, $description: String!, $priority: Int) {
        issueCreate(input: {
            title: $title
            teamId: $teamId
            description: $description
            priority: $priority
        }) {
            success
            issue { id identifier url title }
        }
    }
    """
    data = _linear_query(mutation, {
        "title": f"[{req_id}] {title}",
        "teamId": team,
        "description": description,
        "priority": priority,
    })

    issue = data.get("issueCreate", {}).get("issue", {})
    return json.dumps({
        "created": True,
        "ticket": issue.get("identifier"),
        "url": issue.get("url"),
        "title": issue.get("title"),
    }, indent=2)


@mcp.tool()
def generate_checklist(req_id: str) -> str:
    """Generate an implementation checklist for a requirement.

    Args:
        req_id: Requirement ID (e.g., REQ-d00027).
    """
    req = _get_requirement(req_id)
    if not req:
        return f"Error: Requirement {req_id} not found"

    title = req.get("title", req_id)
    assertions = req.get("assertions", [])
    full_text = title + " " + " ".join(a.get("text", "") for a in assertions)
    subsystems = _detect_subsystems(full_text)

    sections = []

    # Implementation tasks from assertions
    if assertions:
        tasks = [f"- [ ] Implement assertion {a['label']}: {a['text']}" for a in assertions]
        sections.append(f"### Implementation\n" + "\n".join(tasks))

    # Subsystem-specific tasks
    if subsystems:
        tasks = [f"- [ ] Verify {s} impact" for s in subsystems]
        sections.append(f"### Subsystem Verification\n" + "\n".join(tasks))

    # Standard tasks
    sections.append("""### Testing & Documentation
- [ ] Write unit tests covering all assertions
- [ ] Update integration tests if applicable
- [ ] Update documentation/spec references
- [ ] Verify requirement traceability (commit references REQ ID)""")

    checklist = f"## Checklist: {req_id} - {title}\n\n" + "\n\n".join(sections)

    return json.dumps({
        "req_id": req_id,
        "title": title,
        "subsystems": subsystems,
        "assertion_count": len(assertions),
        "checklist_markdown": checklist,
    }, indent=2)


@mcp.tool()
def get_traceability_coverage(level: str = "all") -> str:
    """Report on requirement-to-ticket traceability coverage.

    Args:
        level: Filter by level ('prd', 'ops', 'dev', or 'all').
    """
    try:
        result = subprocess.run(
            ["elspais", "summary", "--json"],
            capture_output=True, text=True, timeout=30,
        )
        if result.returncode != 0:
            return f"Error: {result.stderr}"
        summary = json.loads(result.stdout)
    except (FileNotFoundError, json.JSONDecodeError) as e:
        return f"Error getting summary: {e}"

    return json.dumps({
        "summary": summary,
        "note": "Use find_untracked_requirements for detailed gap analysis",
    }, indent=2)


def main() -> None:
    """Start the traceability MCP server."""
    mcp.run()
