"""
Deep logic checks (19-23): runtime behavior, cross-file consistency, error handling.

Checks:
  19. JSON Error Handling
  20. Hardcoded Python Thresholds
  21. State Field Consistency
  22. Constants Runtime Health
  23. Prose File References
"""

import json
import os
import re
import subprocess
import sys

from . import register_check, register_section


def check_json_error_handling(base, discovered):
    """Verify json.load() calls have JSONDecodeError handling."""
    results = {"pass": [], "fail": [], "warn": []}

    JSON_LOAD = re.compile(r'json\.load\s*\(')
    JSON_DECODE_ERROR = re.compile(r'JSONDecodeError|json\.JSONDecodeError')

    for rel in sorted(discovered):
        if not (rel.startswith("tools/") or rel.startswith(".claude/hooks/")):
            continue
        if not rel.endswith(".py"):
            continue

        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        basename = os.path.basename(rel)

        if JSON_LOAD.search(content):
            if JSON_DECODE_ERROR.search(content):
                results["pass"].append(
                    f"{basename}: json.load() has JSONDecodeError handling"
                )
            else:
                results["fail"].append(
                    f"{basename}: json.load() without JSONDecodeError handling — "
                    f"will crash on corrupted JSON files"
                )

    return results


def check_hardcoded_python_thresholds(base, discovered):
    """Detect hardcoded threshold values in Python scripts."""
    results = {"pass": [], "fail": [], "warn": []}

    SHADOW_PATTERNS = [
        (
            re.compile(
                r'(?:percent|pct|ratio|dialogue)\S{0,20}\s*(?:>|<|>=|<=)\s*(?:35|0\.35)\b'
                r'|(?:>|<|>=|<=)\s*(?:35|0\.35)\b\S{0,20}(?:percent|pct|ratio|dialogue)',
                re.IGNORECASE,
            ),
            "hardcoded 35% dialogue threshold",
            "DIALOGUE_MAX_PERCENT",
        ),
        (
            re.compile(
                r'(?:word|count|total)\S{0,20}\s*(?:>|<|>=|<=)\s*(?:315|400)\b',
                re.IGNORECASE,
            ),
            "hardcoded pre-V12 word count threshold",
            "WORD_COUNT_MIN/MAX",
        ),
    ]

    IMPORTS_ENGINE = re.compile(
        r'(?:from\s+engine_constants\s+import|import\s+engine_constants)'
    )

    for rel in sorted(discovered):
        if not (rel.startswith("tools/") or rel.startswith(".claude/hooks/")):
            continue
        if not rel.endswith(".py"):
            continue
        if "engine_constants.py" in rel or "engine_check.py" in rel:
            continue

        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        basename = os.path.basename(rel)

        if not IMPORTS_ENGINE.search(content):
            continue

        file_issues = []
        for pattern, desc, const_name in SHADOW_PATTERNS:
            if pattern.search(content):
                file_issues.append(f"{desc} (should use {const_name})")

        if file_issues:
            for issue in file_issues:
                results["fail"].append(f"{basename}: {issue}")
        else:
            results["pass"].append(f"{basename}: no shadow constants")

    return results


def check_state_field_consistency(base, _discovered):
    """Verify JSON template field names match Python code expectations."""
    results = {"pass": [], "fail": [], "warn": []}

    state_template = os.path.join(base, "templates", "state_template.json")
    if not os.path.exists(state_template):
        results["warn"].append("state_template.json not found")
        return results

    try:
        with open(state_template) as f:
            template = json.load(f)
    except (json.JSONDecodeError, IOError):
        results["fail"].append("state_template.json cannot be parsed")
        return results

    def extract_fields(obj):
        fields = set()
        if isinstance(obj, dict):
            for key in obj:
                fields.add(key)
                fields.update(extract_fields(obj[key]))
        return fields

    template_fields = extract_fields(template)

    EXPECTED_FIELDS = {
        "last_completed_episode": "meta.last_completed_episode",
        "current_act": "meta.current_act",
        "word_count_target": "v12_constraints.word_count_target",
        "dialogue_cap_percent": "v12_constraints.dialogue_cap_percent",
        "dialogue_exchanges_max": "v12_constraints.dialogue_exchanges_max",
    }

    for field, location in EXPECTED_FIELDS.items():
        if field in template_fields:
            results["pass"].append(f"state_template: '{field}' present ({location})")
        else:
            results["fail"].append(
                f"state_template: missing '{field}' — "
                f"Python scripts expect this field at {location}"
            )

    return results


def check_engine_constants_runtime(base, _discovered):
    """Run engine_constants.py and check for WARNING output."""
    results = {"pass": [], "fail": [], "warn": []}

    constants_script = os.path.abspath(os.path.join(base, "tools", "engine_constants.py"))
    if not os.path.exists(constants_script):
        results["fail"].append("engine_constants.py not found")
        return results

    try:
        result = subprocess.run(
            [sys.executable, constants_script],
            capture_output=True,
            text=True,
            timeout=10,
            cwd=os.path.abspath(base),
        )

        output = result.stdout + result.stderr

        if "WARNING" in output:
            for line in output.split('\n'):
                if "WARNING" in line:
                    results["fail"].append(
                        f"engine_constants.py: {line.strip()}"
                    )
        elif result.returncode != 0:
            error_preview = (result.stderr or result.stdout)[:300]
            results["fail"].append(
                f"engine_constants.py: exit code {result.returncode} — "
                f"{error_preview}"
            )
        else:
            results["pass"].append(
                "engine_constants.py: all constants parsed cleanly from CONSTANTS.md"
            )

    except subprocess.TimeoutExpired:
        results["warn"].append("engine_constants.py: timed out (10s)")
    except Exception as e:
        results["warn"].append(f"engine_constants.py: {str(e)}")

    return results


def check_prose_file_references(base, discovered):
    """Detect .md/.py/.json filenames in prose that don't exist on disk."""
    results = {"pass": [], "fail": [], "warn": []}

    PROSE_FILE_REF = re.compile(
        r'(?<![`/\w])\b([a-zA-Z][a-zA-Z0-9_-]+\.(?:md|py|json))\b(?![`/])'
    )

    EXAMPLE_PATTERNS = [
        re.compile(r'^ep_\d+\.md$'),
        re.compile(r'^ep_[A-Z]+\.md$'),
        re.compile(r'^storyboard_ep_\d+\.json$'),
        re.compile(r'^storyboard_ep_[A-Z]+\.json$'),
        re.compile(r'^batch_\d+_summary\.json$'),
        re.compile(r'^[A-Z]+-COMPLETE.*\.json$'),
        re.compile(r'^THE-.*\.json$'),
    ]

    known_basenames_lower = set()
    known_basenames_exact = set()
    for rel in discovered:
        basename = os.path.basename(rel)
        known_basenames_exact.add(basename)
        known_basenames_lower.add(basename.lower())

    KNOWN_PROJECT_FILES = {
        # Project-scoped docs (live in projects/{project}/)
        'treatment.md', 'ORCHESTRATION.md', 'STATUS.md',
        'characters.md', 'character_voices.md', 'series_bible.md',
        'episode_arc.md', 'structure_outline.md', 'thematic_spine.md',
        'world.md', 'concept.md', 'visual_bible.md', 'CHANGELOG.md',
        'README.md', 'CLAUDE.md', 'CONSTANTS.md',
        'plant_payoff_plan.md', 'relationship_map.md',
        'pressure_test_results.md', 'relationship.md', 'structure.md',
        # Project-scoped JSON state (live in projects/{project}/state/)
        'current_state.json', 'orchestrator_state.json',
        'batch_summary.json', 'annotations.json',
        'decisions_log.json', 'revision_log.json', 'rewrite_log.json',
        'record.json', 'settings.json', 'state.json',
        'breakdown.json', 'storyboard.json', 'sb.json',
        'project_config.json', 'manifest.json', 'credit_log.json',
        # Starsend project-scoped files
        'global_bible.json', 'ep_NNN_plan.json',
        # Runtime-generated by script_doctor.py
        'script_doctor_annotations.json', 'script_doctor_structural.json',
        'script_doctor_broad.json', 'script_doctor_verify.json',
        'script_doctor_brief.json', 'script_doctor_close_read.json',
        'script_doctor_focus_texture_tone_vitality.json',
        'script_doctor_focus_arc_earning.json',
        'script_doctor_focus_continuity.json',
        'deep_fix_F002.json',
        # Runtime-generated by revision agent
        'revision_session.json', 'transition_check.json',
        # Golden record examples
        'jinx_hero_shot.json',
        # Cross-repo files (Pipeline)
        'visual_sync.py', 'recoil_check.py',
        # External files (ComfyUI)
        'generate_from_storyboard.py',
        # Legacy/renamed docs (may be referenced in older docs)
        'FINDINGS.md', 'WORKFLOW_GUIDE.md', 'VISUAL_PIPELINE_STATUS.md',
        'generation_workflows.md', 'microdrama_engine_v12.md',
        'PROMOTE_TO_SCRIPTING.md',
        # Build files
        'requirements.txt', 'setup.py',
    }
    known_basenames_exact.update(KNOWN_PROJECT_FILES)
    known_basenames_lower.update(f.lower() for f in KNOWN_PROJECT_FILES)

    seen = set()
    stale_refs = []

    for rel in sorted(discovered):
        if not rel.endswith(".md"):
            continue

        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        for match in PROSE_FILE_REF.finditer(content):
            ref_name = match.group(1)
            key = (rel, ref_name)
            if key in seen:
                continue
            seen.add(key)

            if any(p.match(ref_name) for p in EXAMPLE_PATTERNS):
                continue

            if ref_name not in known_basenames_exact:
                if ref_name.lower() in known_basenames_lower:
                    stale_refs.append((rel, ref_name, "case_mismatch"))
                else:
                    stale_refs.append((rel, ref_name, "missing"))

    if stale_refs:
        for entry in stale_refs:
            rel, ref, kind = entry
            if kind == "case_mismatch":
                results["warn"].append(
                    f"{rel}: references '{ref}' — wrong case "
                    f"(would fail on case-sensitive filesystem)"
                )
            else:
                results["warn"].append(
                    f"{rel}: references '{ref}' — file doesn't exist in codebase"
                )
    else:
        results["pass"].append("No stale prose file references found")

    return results


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

register_check("json_handling", "JSON Error Handling", check_json_error_handling, "deep_logic")
register_check("py_thresholds", "Hardcoded Python Thresholds", check_hardcoded_python_thresholds, "deep_logic")
register_check("state_fields", "State Field Consistency", check_state_field_consistency, "deep_logic")
register_check("constants_rt", "Constants Runtime Health", check_engine_constants_runtime, "deep_logic", quick=True)
register_check("prose_refs", "Prose File References", check_prose_file_references, "deep_logic")

# Section alias
register_section("logic", [
    "json_handling", "py_thresholds", "state_fields", "constants_rt", "prose_refs",
])
