"""
Structural checks (1-6): chain wiring, file existence, cross-references.

Checks:
  1. Skill -> Agent -> Validator Chain
  2. Gate Enforcement Language
  3. Constants Propagation
  4. Cross-Reference Resolution
  5. CLAUDE.md Command Coverage
  6. Orphan Detection
"""

import os
import re

from . import register_check, register_section

# ═══════════════════════════════════════════════════════════════
# PROTOCOL MAPS — the contractual relationships in the pipeline
# ═══════════════════════════════════════════════════════════════

SKILL_AGENT_MAP = {
    ".claude/skills/assess/SKILL.md": "agents/assess_agent.md",
    ".claude/skills/breakdown/SKILL.md": "agents/breakdown_agent.md",
    ".claude/skills/develop/SKILL.md": "agents/develop_agent.md",
    ".claude/skills/dramatic-qc/SKILL.md": "agents/dramatic_qc_agent.md",
    ".claude/skills/engine-check/SKILL.md": "agents/engine_check_agent.md",
    ".claude/skills/generate-orchestrated/SKILL.md": "agents/orchestrator_agent.md",
    ".claude/skills/promote/SKILL.md": "agents/promote_agent.md",
    ".claude/skills/rewrite/SKILL.md": "agents/rewrite_agent.md",
    ".claude/skills/showrunner/SKILL.md": "agents/showrunner_agent.md",
    ".claude/skills/storyboard/SKILL.md": "agents/storyboard_agent.md",
    ".claude/skills/thread/SKILL.md": "agents/thread_agent.md",
    ".claude/skills/treatment/SKILL.md": "agents/treatment_agent.md",
    ".claude/skills/validate/SKILL.md": "agents/validate_agent.md",
    ".claude/skills/visual-design/SKILL.md": "agents/visual_design_agent.md",
}

AGENT_VALIDATOR_MAP = {
    "agents/breakdown_agent.md": "tools/validate_breakdown.py",
    "agents/storyboard_agent.md": "tools/validate_storyboard.py",
    "agents/visual_design_agent.md": "tools/validate_visual_bible.py",
    "agents/treatment_agent.md": "tools/validate_treatment.py",
}

CONSTANTS_CHECKS = [
    {
        "name": "Word count range 450-500",
        "pattern": r"450.*500",
        "files": ["skills/format_v12/SKILL.md", "CLAUDE.md"],
    },
    {
        "name": "Dialogue max 40%",
        "pattern": r"40%|≤\s*40|DIALOGUE_MAX_PERCENT",
        "files": ["skills/format_v12/SKILL.md", "CLAUDE.md"],
    },
    {
        "name": "Max 8 exchanges",
        "pattern": r"8\s*max|max.*8",
        "files": ["CLAUDE.md"],
    },
    {
        "name": "Primary lens 50mm f/2.0",
        "pattern": r"50mm.*f/2\.0",
        "files": [
            "agents/visual_design_agent.md",
            "agents/shotlist_agent.md",
            "appendix_e_flux2_protocols.md",
        ],
    },
    {
        "name": "Close-up lens 85mm f/1.4",
        "pattern": r"85mm.*f/1\.4",
        "files": [
            "agents/visual_design_agent.md",
            "agents/shotlist_agent.md",
            "appendix_e_flux2_protocols.md",
        ],
    },
    {
        "name": "Wide lens 24mm f/8",
        "pattern": r"24mm.*f/8",
        "files": [
            "agents/visual_design_agent.md",
            "agents/shotlist_agent.md",
            "appendix_e_flux2_protocols.md",
        ],
    },
    {
        "name": "Film stock Kodak Vision3 500T in CONSTANTS.md",
        "pattern": r"FILM_STOCK.*Kodak Vision3 500T|Kodak Vision3 500T.*FILM_STOCK",
        "files": ["CONSTANTS.md"],
    },
    {
        "name": "Film stock Kodak Vision3 500T",
        "pattern": r"Kodak Vision3 500T",
        "files": [
            "agents/shotlist_agent.md",
            "agents/visual_design_agent.md",
            "appendix_e_flux2_protocols.md",
        ],
    },
    {
        "name": "Hook silent ratio 70-85%",
        "pattern": r"70.*85.*silent|70-85%.*silent|HOOK_SILENT_MIN.*70",
        "files": ["CLAUDE.md"],
    },
    {
        "name": "Cliffhanger mid-action ratio 70-85%",
        "pattern": r"70.*85.*mid.action|70-85%.*mid.action|CLIFF_MID_ACTION_MIN.*70",
        "files": ["CLAUDE.md"],
    },
    {
        "name": "Pattern limit max 3 consecutive",
        "pattern": r"[Mm]ax\s*3\s*consecutive|3\s*consecutive|PATTERN_CONSEC_MAX.*3",
        "files": ["CLAUDE.md"],
    },
    {
        "name": "Reference slots 1-5 character identity",
        "pattern": r"1-5.*[Cc]haracter\s*identity",
        "files": [
            "agents/visual_design_agent.md",
            "appendix_e_flux2_protocols.md",
        ],
    },
    {
        "name": "Storyboard target 18-24 shots",
        "pattern": r"18.*24|TARGET_SHOTS",
        "files": ["tools/validate_storyboard.py"],
    },
]

AGENTLESS_SKILLS = {
    "load-context",
    "compile",
    "autogenerate",
    "generate",
    "validate-docs",
    "update-guides",
    "batch",
    "novella",
    "assess",
    "imagine",
    "golden-record",
    "revise",
    "engine-check",
    "editors",
    "script-doctor",
    "camera-test",
    "listen",
}


# ═══════════════════════════════════════════════════════════════
# CHECK FUNCTIONS
# ═══════════════════════════════════════════════════════════════

def check_skill_agent_chain(base, discovered):
    """Verify every skill -> agent -> validator link exists and is referenced."""
    results = {"pass": [], "fail": [], "warn": []}

    for skill_rel, agent_rel in sorted(SKILL_AGENT_MAP.items()):
        skill_path = os.path.join(base, skill_rel)
        agent_path = os.path.join(base, agent_rel)
        skill_name = os.path.basename(os.path.dirname(skill_rel))
        agent_name = os.path.basename(agent_rel)

        if not os.path.exists(skill_path):
            results["warn"].append(f"Skill not found: {skill_rel}")
            continue
        if not os.path.exists(agent_path):
            results["fail"].append(f"{skill_name}: agent {agent_name} doesn't exist")
            continue

        with open(skill_path) as f:
            text = f.read()

        if agent_name in text or agent_rel.lstrip("_") in text:
            results["pass"].append(f"{skill_name} -> {agent_name}")
        else:
            results["warn"].append(f"{skill_name} doesn't reference {agent_name}")

    for agent_rel, val_rel in sorted(AGENT_VALIDATOR_MAP.items()):
        agent_path = os.path.join(base, agent_rel)
        val_path = os.path.join(base, val_rel)
        agent_name = os.path.basename(agent_rel)
        val_name = os.path.basename(val_rel)

        if not os.path.exists(agent_path) or not os.path.exists(val_path):
            results["fail"].append(f"{agent_name}: validator {val_name} doesn't exist")
            continue

        with open(agent_path) as f:
            text = f.read()

        if val_name in text:
            results["pass"].append(f"{agent_name} -> {val_name}")
        else:
            results["fail"].append(f"{agent_name} doesn't reference {val_name}")

    skills_dir = os.path.join(base, ".claude", "skills")
    if os.path.isdir(skills_dir):
        for entry in sorted(os.listdir(skills_dir)):
            skill_md = os.path.join(skills_dir, entry, "SKILL.md")
            if not os.path.exists(skill_md):
                continue
            skill_rel = f".claude/skills/{entry}/SKILL.md"
            if skill_rel in SKILL_AGENT_MAP:
                continue
            if entry in AGENTLESS_SKILLS:
                continue
            results["warn"].append(
                f"Skill /{entry} exists but not in SKILL_AGENT_MAP "
                f"(add to map or AGENTLESS_SKILLS)"
            )

    return results


def check_gate_enforcement(base, _discovered):
    """Verify agents with validators use mandatory gate language."""
    results = {"pass": [], "fail": [], "warn": []}

    GATE_PATTERNS = [
        re.compile(r"MANDATORY", re.IGNORECASE),
        re.compile(r"Do NOT proceed", re.IGNORECASE),
        re.compile(r"HARD GATE", re.IGNORECASE),
        re.compile(r"must.*valid|must.*pass|must.*run", re.IGNORECASE),
        re.compile(r"is_valid"),
        re.compile(r"exit\s*code\s*[012]", re.IGNORECASE),
    ]

    for agent_rel, val_rel in sorted(AGENT_VALIDATOR_MAP.items()):
        agent_path = os.path.join(base, agent_rel)
        if not os.path.exists(agent_path):
            continue

        agent_name = os.path.basename(agent_rel)
        val_name = os.path.basename(val_rel)

        with open(agent_path) as f:
            text = f.read()

        found = [p.pattern for p in GATE_PATTERNS if p.search(text)]

        if len(found) >= 2:
            results["pass"].append(
                f"{agent_name}: gate enforced ({len(found)} enforcement markers)"
            )
        elif len(found) == 1:
            results["warn"].append(
                f"{agent_name}: weak enforcement for {val_name} "
                f"(only 1 marker: {found[0]})"
            )
        else:
            results["fail"].append(
                f"{agent_name}: NO enforcement language for {val_name} — "
                f"validator may be treated as optional"
            )

    PREREQ_SKILLS = {
        ".claude/skills/treatment/SKILL.md": "validate_pre_treatment",
        ".claude/skills/visual-design/SKILL.md": "breakdown.json",
    }

    for skill_rel, prereq_term in sorted(PREREQ_SKILLS.items()):
        skill_path = os.path.join(base, skill_rel)
        if not os.path.exists(skill_path):
            continue

        skill_name = os.path.basename(os.path.dirname(skill_rel))
        with open(skill_path) as f:
            text = f.read()

        has_prereq = prereq_term.lower() in text.lower()
        has_gate_language = bool(
            re.search(r"prerequisite|HARD GATE|must exist|must have", text, re.IGNORECASE)
        )

        if has_prereq and has_gate_language:
            results["pass"].append(
                f"{skill_name}: prerequisite gate documented ({prereq_term})"
            )
        elif has_prereq:
            results["warn"].append(
                f"{skill_name}: mentions {prereq_term} but no gate enforcement language"
            )
        else:
            results["warn"].append(
                f"{skill_name}: prerequisite {prereq_term} not documented"
            )

    return results


def check_constants_sync(base, _discovered):
    """Verify CONSTANTS.md canonical values are propagated to every consumer."""
    results = {"pass": [], "fail": [], "warn": []}

    for check in CONSTANTS_CHECKS:
        name = check["name"]
        pattern = re.compile(check["pattern"], re.IGNORECASE)

        for rel in check["files"]:
            full = os.path.join(base, rel)
            if not os.path.exists(full):
                results["warn"].append(f"Cannot verify '{name}' in {rel} (missing)")
                continue

            with open(full) as f:
                content = f.read()

            if pattern.search(content):
                results["pass"].append(f"'{name}' in {os.path.basename(rel)}")
            else:
                results["fail"].append(f"'{name}' NOT FOUND in {rel}")

    return results


def check_cross_references(base, discovered):
    """Verify backtick file-path references in .md files resolve to real files."""
    results = {"pass": [], "fail": [], "warn": []}

    path_re = re.compile(
        r'`(/?(?:_engine|\.claude|_docs|_development)/[^`\s]+\.\w+)`'
    )
    seen = set()

    for rel in sorted(discovered):
        if not rel.endswith(".md"):
            continue
        full = os.path.join(base, rel)
        with open(full) as f:
            content = f.read()

        for ref in path_re.findall(content):
            clean = ref.lstrip("/")
            if "[" in clean or "*" in clean:
                continue
            key = (rel, clean)
            if key in seen:
                continue
            seen.add(key)

            ref_full = os.path.join(base, clean)
            if os.path.exists(ref_full):
                results["pass"].append(f"{rel} -> {clean}")
            else:
                if clean.startswith("claude/"):
                    alt = os.path.join(base, "." + clean)
                    if os.path.exists(alt):
                        results["pass"].append(f"{rel} -> .{clean}")
                        continue
                results["warn"].append(f"Stale ref in {rel}: `{ref}`")

    return results


def check_command_coverage(base, _discovered):
    """Verify CLAUDE.md documents every skill that exists on disk."""
    results = {"pass": [], "fail": [], "warn": []}

    claude_md = os.path.join(base, "CLAUDE.md")
    if not os.path.exists(claude_md):
        results["fail"].append("CLAUDE.md not found")
        return results

    with open(claude_md) as f:
        content = f.read()

    skills_dir = os.path.join(base, ".claude", "skills")
    if not os.path.isdir(skills_dir):
        results["fail"].append(".claude/skills/ directory not found")
        return results

    for entry in sorted(os.listdir(skills_dir)):
        if not os.path.exists(os.path.join(skills_dir, entry, "SKILL.md")):
            continue
        cmd = f"/{entry}"
        if cmd in content or entry in content:
            results["pass"].append(f"/{entry} documented")
        else:
            results["warn"].append(f"/{entry} skill exists but not in CLAUDE.md")

    return results


def check_orphans(base, discovered):
    """Find agents and tools not referenced by any skill or CLAUDE.md."""
    results = {"pass": [], "warn": []}

    reference_corpus = ""
    for rel in discovered:
        if (rel.startswith(".claude/skills/") or
                rel.startswith("agents/") or
                rel == "CLAUDE.md"):
            full = os.path.join(base, rel)
            try:
                with open(full) as f:
                    reference_corpus += f.read() + "\n"
            except (IOError, OSError):
                pass

    for rel in sorted(discovered):
        if not rel.startswith("agents/") or not rel.endswith(".md"):
            continue
        basename = os.path.basename(rel)
        if basename == "engine_check_agent.md":
            results["pass"].append(f"{basename} (engine-check)")
            continue
        if basename in reference_corpus:
            results["pass"].append(f"{basename} wired")
        else:
            results["warn"].append(f"Orphan: {basename} — no skill or agent references it")

    for rel in sorted(discovered):
        if not rel.startswith("tools/") or not rel.endswith(".py"):
            continue
        basename = os.path.basename(rel)
        if basename == "engine_check.py":
            results["pass"].append(f"{basename} (engine-check)")
            continue
        if basename in reference_corpus:
            results["pass"].append(f"{basename} wired")
        else:
            results["warn"].append(f"Orphan: {basename} — no skill or agent references it")

    for rel in sorted(discovered):
        if not rel.startswith(".claude/hooks/") or not rel.endswith(".py"):
            continue
        basename = os.path.basename(rel)
        if basename in reference_corpus:
            results["pass"].append(f"{basename} wired")
        else:
            results["warn"].append(f"Orphan hook: {basename} — not referenced in pipeline docs")

    return results


# ═══════════════════════════════════════════════════════════════
# REGISTRATION
# ═══════════════════════════════════════════════════════════════

register_check("chain", "Skill -> Agent -> Validator Chain", check_skill_agent_chain, "structural", quick=True)
register_check("gates", "Gate Enforcement Language", check_gate_enforcement, "structural")
register_check("constants", "Constants Propagation", check_constants_sync, "structural")
register_check("xrefs", "Cross-Reference Resolution", check_cross_references, "structural")
register_check("commands", "CLAUDE.md Command Coverage", check_command_coverage, "structural")
register_check("orphans", "Orphan Detection", check_orphans, "structural")
