from __future__ import annotations

import re
import os
import subprocess
from datetime import date
from pathlib import Path
from typing import Literal

from mcp.server.fastmcp import FastMCP

from .gates import apply_text_rules, get_gate_labels
from .knowledge import get_paths, read_knowledge_doc, read_text, search_knowledge, search_global_docs
from .schemas import CONFIDENCE_HIGH, CONFIDENCE_LOW, CONFIDENCE_MEDIUM, add_reference, empty_gate_output
from .triage import parse_capitalisation_entries, novelty_level, scope_level

Domain = Literal["backend", "frontend", "ux", "n8n", "product", "workflow"]
TaskType = Literal["analysis", "implementation", "review", "debug", "architecture"]
AgentRole = Literal["analyst", "builder", "reviewer", "curator"]


def _build_transport_security():
    """Configure les hôtes/origines autorisés pour le transport HTTP.

    En streamable-http bindé sur 0.0.0.0, la protection anti-DNS-rebinding de MCP
    rejette par défaut tout `Host` non-localhost (HTTP 421 "Invalid Host header").
    Derrière le sidecar Tailscale, le client tape `leadtech-mcp.<tailnet>.ts.net`,
    qu'il faut donc whitelister via `LEADTECH_MCP_ALLOWED_HOSTS` (CSV).
    Retourne None en mode stdio/local → comportement par défaut inchangé.
    """
    allowed = os.getenv("LEADTECH_MCP_ALLOWED_HOSTS", "").strip()
    if not allowed:
        return None
    try:
        from mcp.server.transport_security import TransportSecuritySettings
    except ImportError:
        return None
    hosts: list[str] = []
    origins: list[str] = []
    for raw in allowed.split(","):
        h = raw.strip()
        if not h:
            continue
        hosts.append(h)
        origins.append(f"https://{h}")
        origins.append(f"http://{h}")
    return TransportSecuritySettings(allowed_hosts=hosts, allowed_origins=origins)


_transport_security = _build_transport_security()
if _transport_security is not None:
    mcp = FastMCP(name="leadtech-bmad-mcp", transport_security=_transport_security)
else:
    mcp = FastMCP(name="leadtech-bmad-mcp")
def _base_output() -> dict:
    payload = empty_gate_output()
    payload["gates"] = get_gate_labels()
    return payload


def _metadata_label(item: dict[str, str]) -> str:
    parts: list[str] = []
    if item.get("severity"):
        parts.append(f"severity={item['severity']}")
    if item.get("applies_to"):
        parts.append(f"applies_to={item['applies_to']}")
    if item.get("tags"):
        parts.append(f"tags={item['tags']}")
    if not parts:
        return ""
    return " [" + " | ".join(parts) + "]"


@mcp.resource("leadtech://index")
def resource_index() -> str:
    return read_text(get_paths().root / "00_INDEX.md")


@mcp.resource("leadtech://capitalisation/pending")
def resource_capitalisation() -> str:
    return read_text(get_paths().capitalisation)


@mcp.resource("leadtech://projects/conf")
def resource_projects_conf() -> str:
    return read_text(get_paths().projects_conf)


@mcp.resource("leadtech://knowledge/{domain}/{bucket}/{slug}")
def resource_knowledge_doc(domain: str, bucket: str, slug: str) -> str:
    return read_knowledge_doc(domain=domain, bucket=bucket, slug=slug)


@mcp.resource("leadtech://global/architecture")
def resource_architecture() -> str:
    return read_text(get_paths().root / "40_decisions_et_archi.md")


@mcp.resource("leadtech://global/debug")
def resource_debug() -> str:
    return read_text(get_paths().root / "90_debug_et_postmortem.md")


@mcp.resource("leadtech://global/conventions")
def resource_conventions() -> str:
    return read_text(get_paths().root / "10_conventions_redaction.md")


@mcp.tool(description="Retourne patterns + risques utiles pour une story BMAD")
def get_guidance(domain: Domain, task_type: TaskType, story_text: str = "", keywords: list[str] | None = None, max_items: int = 6) -> dict:
    out = _base_output()
    query = " ".join(keywords or []) or story_text or task_type
    matches = search_knowledge(domain=domain, query=query, max_items=max_items)

    for item in matches:
        slug = Path(item["path"]).stem
        read_uri = f"leadtech://knowledge/{domain}/{item['bucket']}/{slug}"
        metadata = _metadata_label(item)
        label = f"{item['title']}{metadata} — {item['excerpt']}  [lire complet: {read_uri}]"
        if item["bucket"] == "patterns":
            out["must_do"].append(f"Appliquer: {label}")
        else:
            out["red_flags"].append(f"Surveiller: {label}")
        add_reference(out, item["path"], f"Match {item['bucket']} score={item['score']}")
        out["matched_docs"].append(
            {
                "path": item["path"],
                "bucket": item["bucket"],
                "title": item["title"],
                "score": item["score"],
                "severity": item.get("severity", ""),
                "applies_to": item.get("applies_to", ""),
                "tags": item.get("tags", ""),
                "read_uri": read_uri,
            }
        )

    # Fichiers globaux (decisions, postmortems, conventions)
    global_matches = search_global_docs(query=query, max_items=3)
    for item in global_matches:
        read_uri = f"leadtech://global/{item['title']}"
        label = f"{item['title']} — {item['excerpt']}  [lire complet: {read_uri}]"
        out["should_do"].append(f"Consulter doc global: {label}")
        add_reference(out, item["path"], f"Match global score={item['score']}")

    out["should_do"].append(f"Adapter la guidance au role et aux ACs de la story ({task_type}).")

    all_top = (matches + global_matches)[:3]
    if all_top:
        uris = []
        for m in all_top:
            if m["bucket"] == "global":
                uris.append(f"leadtech://global/{m['title']}")
            else:
                uris.append(f"leadtech://knowledge/{domain}/{m['bucket']}/{Path(m['path']).stem}")
        out["should_do"].append(
            "Lire le contenu complet des docs prioritaires via les resources MCP: " + ", ".join(uris)
        )

    out["confidence"] = CONFIDENCE_HIGH if (matches or global_matches) else CONFIDENCE_LOW
    return out


@mcp.tool(description="Valide un plan BMAD contre patterns et pieges")
def validate_plan(domain: Domain, plan_text: str, agent_role: AgentRole = "builder", strict: bool = True) -> dict:
    out = _base_output()

    for finding in apply_text_rules("validate_plan", domain, plan_text, strict):
        out[finding["target"]].append(finding["message"])

    add_reference(out, str(get_paths().root / "80_bmad/process_llm_et_parallelisation.md"), "Regles de synchronisation BMAD")
    out["confidence"] = CONFIDENCE_HIGH if not out["blocking_issues"] else CONFIDENCE_MEDIUM
    return out


@mcp.tool(description="Valide un patch/diff contre les gates Lead_tech")
def validate_patch(domain: Domain, diff_text: str, changed_files: list[str] | None = None, strict: bool = True) -> dict:
    out = _base_output()

    for finding in apply_text_rules("validate_patch", domain, diff_text, strict):
        out[finding["target"]].append(finding["message"])

    if changed_files and all("_bmad-output/" in f for f in changed_files):
        out["blocking_issues"].append("Patch sans fichier source: seulement des artefacts BMAD.")

    if strict and not re.search(r"test|spec|describe\s*\(|it\s*\(|expect\s*\(", diff_text, re.IGNORECASE):
        out["should_do"].append("Aucun test visible dans le diff: verifier couverture manuelle.")

    add_reference(out, str(get_paths().root / "knowledge/workflow/risques/story-tracking.md"), "Anti-pattern story done sans code")
    out["confidence"] = CONFIDENCE_HIGH if not out["blocking_issues"] else CONFIDENCE_MEDIUM
    return out


@mcp.tool(description="Retourne une checklist actionnable par role BMAD")
def emit_checklist(agent_role: AgentRole, domain: Domain, story_text: str = "") -> dict:
    out = _base_output()
    out["must_do"].append("Lire les README patterns+risques du domaine avant action.")
    out["must_do"].append("Verifier coherence story.md <-> sprint-status avant cloture.")

    if agent_role == "analyst":
        out["must_do"].append("Rendre explicites Depends-on et Can-run-with si parallel-safe.")
    elif agent_role == "builder":
        out["must_do"].append("Executer un gate validate_plan avant impl et validate_patch apres diff.")
    elif agent_role == "reviewer":
        out["must_do"].append("Refuser done si aucun fichier source dans File List.")
    else:
        out["must_do"].append("Capitaliser uniquement via 95_a_capitaliser.md, jamais direct knowledge/.")

    if story_text and "parallel-safe" not in story_text.lower():
        out["should_do"].append("Ajouter le champ Parallel-safe dans la story.")

    out["confidence"] = CONFIDENCE_HIGH
    return out


@mcp.tool(description="Ajoute une proposition FILE_UPDATE_PROPOSAL dans 95_a_capitaliser.md")
def propose_capitalization(project_name: str, target_file: str, why: str, proposal: str, dry_run: bool = True) -> dict:
    today = date.today().isoformat()
    block = (
        f"\n\n{today} — {project_name}\n\n"
        f"FILE_UPDATE_PROPOSAL\n"
        f"Fichier cible : {target_file}\n\n"
        f"Pourquoi :\n{why.strip()}\n\n"
        f"Proposition :\n{proposal.strip()}\n"
    )

    out = {"dry_run": dry_run, "target": str(get_paths().capitalisation), "proposal_block": block.strip("\n")}
    if not dry_run:
        if not bool(int(os.getenv("LEADTECH_MCP_ALLOW_WRITE", "0"))):
            return {"error": "Ecriture desactivee. Exporter LEADTECH_MCP_ALLOW_WRITE=1"}
        with get_paths().capitalisation.open("a", encoding="utf-8") as f:
            f.write(block)
        out["written"] = True
    return out


@mcp.tool(description="Trie les propositions du buffer de capitalisation")
def triage_capitalization(project_filter: str = "", max_entries: int = 20) -> dict:
    raw = read_text(get_paths().capitalisation)
    entries = parse_capitalisation_entries(raw)
    if project_filter:
        entries = [e for e in entries if project_filter.lower() in e.header.lower()]
    entries = entries[:max_entries]

    reports = []
    for entry in entries:
        novelty = novelty_level(entry)
        scope = scope_level(entry)

        if novelty in {"DOUBLON_EXACT", "DOUBLON_SEMANTIQUE"}:
            decision = "REJETER"
            confidence = CONFIDENCE_HIGH
        elif scope == "PROJET":
            decision = "A_DEPLACER_CLAUDE_PROJET"
            confidence = CONFIDENCE_MEDIUM
        else:
            decision = "INTEGRER_KNOWLEDGE"
            confidence = CONFIDENCE_MEDIUM

        reports.append(
            {
                "entry": entry.header,
                "decision": decision,
                "confidence": confidence,
                "nouveaute": novelty,
                "portee": scope,
                "target_file": entry.target,
                "why": entry.why,
                "proposal": entry.proposal,
            }
        )

    return {"count": len(reports), "reports": reports}


@mcp.tool(description="Route un apprentissage vers le CLAUDE.md du projet cible")
def route_to_project_memory(project_name: str, section: Literal["Lecons apprises", "Points sensibles"], content: str, dry_run: bool = True) -> dict:
    paths = get_paths()
    resolver = paths.root / "scripts" / "resolve-project-path.sh"

    cmd = [str(resolver), project_name]
    result = subprocess.run(cmd, capture_output=True, text=True, check=False)
    project_path = result.stdout.strip()

    if result.returncode != 0 or not project_path:
        return {
            "error": "Projet introuvable via resolve-project-path.sh",
            "project": project_name,
            "stdout": result.stdout,
            "stderr": result.stderr,
        }

    claude_file = Path(project_path) / "CLAUDE.md"
    section_header = f"# {section}"
    block = f"\n- {content.strip()}\n"

    if dry_run:
        return {
            "dry_run": True,
            "project": project_name,
            "claude_file": str(claude_file),
            "section": section,
            "preview": block.strip(),
        }

    if not bool(int(os.getenv("LEADTECH_MCP_ALLOW_WRITE", "0"))):
        return {"error": "Ecriture desactivee. Exporter LEADTECH_MCP_ALLOW_WRITE=1"}

    if not claude_file.exists():
        return {"error": "CLAUDE.md introuvable", "claude_file": str(claude_file)}

    original = claude_file.read_text(encoding="utf-8")
    if section_header in original:
        updated = original.replace(section_header, f"{section_header}\n{block}", 1)
    else:
        updated = f"{original.rstrip()}\n\n{section_header}\n{block}"

    claude_file.write_text(updated, encoding="utf-8")
    return {
        "written": True,
        "project": project_name,
        "claude_file": str(claude_file),
        "section": section,
    }


def main() -> None:
    """Point d'entrée du serveur.

    Transport piloté par l'environnement pour ne rien casser en local :
    - `LEADTECH_MCP_TRANSPORT` non défini ou `stdio` (défaut) → stdio, comme avant.
    - `streamable-http` → serveur HTTP réseau (déploiement central NUC/Tailscale).
      `LEADTECH_MCP_HOST` (défaut 0.0.0.0) et `LEADTECH_MCP_PORT` (défaut 8080)
      pilotent le bind. En sidecar Tailscale, on bind 0.0.0.0 dans le réseau
      Docker interne — l'exposition au tailnet est faite par le sidecar.
    """
    transport = os.getenv("LEADTECH_MCP_TRANSPORT", "stdio").strip().lower()

    if transport in {"http", "streamable-http", "streamable_http"}:
        mcp.settings.host = os.getenv("LEADTECH_MCP_HOST", "0.0.0.0")
        mcp.settings.port = int(os.getenv("LEADTECH_MCP_PORT", "8080"))
        mcp.run(transport="streamable-http")
    elif transport == "sse":
        mcp.settings.host = os.getenv("LEADTECH_MCP_HOST", "0.0.0.0")
        mcp.settings.port = int(os.getenv("LEADTECH_MCP_PORT", "8080"))
        mcp.run(transport="sse")
    else:
        mcp.run()


if __name__ == "__main__":
    main()
