"""
Visual pipeline integrity checks (31-38).

Checks:
  31. config_loader_exports — Importers expecting names that config_loader.py doesn't export
  32. engine_cost_map — ENGINE_COST_MAP keys missing from pricing_rates.json
  33. pricing_completeness — Models referenced in tracker.log() calls not in pricing_rates.json
  34. project_config_template — Template keys diverging from DEFAULT_PROJECT_CONFIG
  35. rendering_directives_schema — breakdown.json rendering_directives missing expected fields
  36. cost_tracker_categories — tracker.log(category=...) using categories not in CATEGORIES dict
  37. lora_registry_integrity — LoRA registry with invalid structure or missing referenced files
  38. visual_bible_template — visual_bible_template.md sections not matching validator expectations
"""

import json
import os
import re

from . import register_check, register_section


def check_config_loader_exports(base, discovered):
    """Verify importers of config_loader.py use names it actually exports."""
    results = {"pass": [], "fail": [], "warn": []}

    config_loader_path = os.path.join(base, "lib", "config_loader.py")
    if not os.path.exists(config_loader_path):
        results["warn"].append("config_loader.py not found")
        return results

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

    # Extract exported names (top-level defs, assignments, classes)
    exported = set()
    for match in re.finditer(r'^(?:def|class)\s+(\w+)', content, re.MULTILINE):
        exported.add(match.group(1))
    for match in re.finditer(r'^([A-Z][A-Z_0-9]+)\s*=', content, re.MULTILINE):
        exported.add(match.group(1))

    # Find all files that import from config_loader
    import_pattern = re.compile(r'from\s+config_loader\s+import\s+(.*?)(?:\n|$)')

    for rel in sorted(discovered):
        if not rel.endswith(".py"):
            continue
        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                file_content = f.read()
        except (IOError, OSError):
            continue

        for match in import_pattern.finditer(file_content):
            imports_str = match.group(1).strip()
            # Handle multi-line imports
            if imports_str.startswith("("):
                imports_str = imports_str.strip("()")
            # Parse individual names
            names = [n.strip().split(" as ")[0].strip()
                     for n in imports_str.split(",") if n.strip()]

            for name in names:
                if name and name not in exported:
                    results["fail"].append(
                        f"{os.path.basename(rel)}: imports '{name}' "
                        f"from config_loader.py but it's not exported"
                    )

    if not results["fail"]:
        results["pass"].append("All config_loader imports resolve to exported names")

    return results


def check_engine_cost_map(base, _discovered):
    """Verify cost models referenced in code exist in pricing_rates.json."""
    results = {"pass": [], "fail": [], "warn": []}

    rates_path = os.path.join(base, "config", "pricing_rates.json")
    if not os.path.exists(rates_path):
        results["fail"].append("pricing_rates.json not found")
        return results

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

    # Extract all model keys from the most recent rate card
    rate_cards = rates_data.get("rate_cards", [])
    if not rate_cards:
        results["fail"].append("pricing_rates.json has no rate_cards")
        return results

    latest_card = rate_cards[-1]
    all_models = set()
    for provider, models in latest_card.items():
        if provider in ("effective_date", "note"):
            continue
        if isinstance(models, dict):
            all_models.update(models.keys())

    # Scan cost_tracker.py for ENGINE_COST_MAP or similar mappings
    tracker_path = os.path.join(base, "tools", "cost_tracker.py")
    if not os.path.exists(tracker_path):
        results["warn"].append("cost_tracker.py not found")
        return results

    with open(tracker_path) as f:
        tracker_content = f.read()

    # Find model keys referenced in estimate_cost or log calls
    model_refs = set()
    for match in re.finditer(r'model\s*[=:]\s*["\']([^"\']+)["\']', tracker_content):
        model_refs.add(match.group(1))
    # Also find models in rate lookup patterns
    for match in re.finditer(r'\["\s*(\w+)\s*"\]', tracker_content):
        candidate = match.group(1)
        if "_" in candidate and candidate.lower() == candidate:
            model_refs.add(candidate)

    results["pass"].append(
        f"pricing_rates.json: {len(all_models)} models across "
        f"{len([k for k in latest_card if k not in ('effective_date', 'note')])} providers"
    )

    return results


def check_pricing_completeness(base, discovered):
    """Verify models used in tracker.log() calls exist in pricing_rates.json."""
    results = {"pass": [], "fail": [], "warn": []}

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

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

    rate_cards = rates_data.get("rate_cards", [])
    if not rate_cards:
        return results

    latest_card = rate_cards[-1]
    all_models = set()
    for provider, models in latest_card.items():
        if provider in ("effective_date", "note"):
            continue
        if isinstance(models, dict):
            all_models.update(models.keys())

    # Scan all .py files for tracker.log(..., model="X", ...) calls
    # Exclude engine_checks/ (contains regex patterns that produce false matches)
    model_pattern = re.compile(r'tracker\.log\([^)]*model\s*=\s*["\']([^"\']+)["\']')
    models_in_code = {}

    for rel in sorted(discovered):
        if not rel.endswith(".py"):
            continue
        if "engine_checks/" in rel:
            continue
        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        for match in model_pattern.finditer(content):
            model = match.group(1)
            if model not in models_in_code:
                models_in_code[model] = []
            models_in_code[model].append(os.path.basename(rel))

    missing = []
    for model, files in sorted(models_in_code.items()):
        if model not in all_models:
            missing.append((model, files))

    if missing:
        for model, files in missing:
            results["warn"].append(
                f"Model '{model}' used in {', '.join(set(files))} "
                f"but not in pricing_rates.json"
            )
    else:
        results["pass"].append(
            f"All {len(models_in_code)} models in tracker.log() calls "
            f"exist in pricing_rates.json"
        )

    return results


def check_project_config_template(base, _discovered):
    """Verify project_config_template.json keys match DEFAULT_PROJECT_CONFIG."""
    results = {"pass": [], "fail": [], "warn": []}

    template_path = os.path.join(base, "templates", "project_config_template.json")
    config_loader_path = os.path.join(base, "lib", "config_loader.py")

    if not os.path.exists(template_path):
        results["warn"].append("project_config_template.json not found")
        return results

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

    if not os.path.exists(config_loader_path):
        results["warn"].append("config_loader.py not found — cannot compare defaults")
        return results

    # Extract DEFAULT_PROJECT_CONFIG top-level keys from config_loader.py
    with open(config_loader_path) as f:
        loader_content = f.read()

    # Find the dict literal for DEFAULT_PROJECT_CONFIG — only top-level keys
    # (keys at indent level 4, not nested dict keys at indent level 8+)
    default_keys = set()
    in_default = False
    brace_depth = 0
    for line in loader_content.split("\n"):
        if "DEFAULT_PROJECT_CONFIG" in line and "=" in line:
            in_default = True
            brace_depth = 0
            continue
        if in_default:
            brace_depth += line.count("{") - line.count("}")
            # Only capture top-level keys (brace_depth == 1, indent ~4 spaces)
            if brace_depth == 1:
                key_match = re.match(r'^\s{4}"(\w+)":', line)
                if key_match:
                    default_keys.add(key_match.group(1))
            if brace_depth <= 0 and line.strip() == "}":
                break

    template_keys = set(template.keys())

    missing_from_template = default_keys - template_keys
    extra_in_template = template_keys - default_keys

    if missing_from_template:
        for key in sorted(missing_from_template):
            results["warn"].append(
                f"project_config_template.json: missing key '{key}' "
                f"(present in DEFAULT_PROJECT_CONFIG)"
            )
    if extra_in_template:
        for key in sorted(extra_in_template):
            results["warn"].append(
                f"project_config_template.json: extra key '{key}' "
                f"(not in DEFAULT_PROJECT_CONFIG)"
            )

    if not missing_from_template and not extra_in_template:
        results["pass"].append(
            f"project_config_template.json: all {len(template_keys)} keys "
            f"match DEFAULT_PROJECT_CONFIG"
        )
    elif not missing_from_template:
        results["pass"].append(
            f"project_config_template.json: has all required keys "
            f"(+ {len(extra_in_template)} project-specific extras)"
        )

    return results


def check_rendering_directives_schema(base, discovered):
    """Verify breakdown.json rendering_directives have expected fields."""
    results = {"pass": [], "fail": [], "warn": []}

    EXPECTED_FIELDS = {"texture_prompt", "texture_negative", "mandatory_traits", "identity_type"}

    # Find breakdown.json files in project dirs
    breakdown_files = [
        rel for rel in discovered
        if rel.endswith("breakdown.json") and "visual" in rel
    ]

    if not breakdown_files:
        results["pass"].append("No breakdown.json files found (pre-production)")
        return results

    for rel in breakdown_files:
        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                data = json.load(f)
        except (json.JSONDecodeError, IOError):
            results["warn"].append(f"{rel}: cannot parse")
            continue

        characters = data.get("characters", {})
        for char_key, char_data in characters.items():
            directives = char_data.get("rendering_directives", {})
            if not directives:
                continue  # Not all characters need directives

            missing = EXPECTED_FIELDS - set(directives.keys())
            if missing:
                results["warn"].append(
                    f"{rel}: {char_key} rendering_directives missing fields: "
                    f"{', '.join(sorted(missing))}"
                )
            else:
                results["pass"].append(
                    f"{rel}: {char_key} has all rendering_directives fields"
                )

    return results


def check_cost_tracker_categories(base, discovered):
    """Verify tracker.log(category=...) uses categories defined in CATEGORIES dict."""
    results = {"pass": [], "fail": [], "warn": []}

    tracker_path = os.path.join(base, "tools", "cost_tracker.py")
    if not os.path.exists(tracker_path):
        results["warn"].append("cost_tracker.py not found")
        return results

    with open(tracker_path) as f:
        tracker_content = f.read()

    # Extract CATEGORIES keys
    categories = set()
    in_categories = False
    for line in tracker_content.split("\n"):
        if "CATEGORIES = {" in line:
            in_categories = True
            continue
        if in_categories:
            if line.strip() == "}":
                break
            key_match = re.match(r'\s*"(\w+)":', line)
            if key_match:
                categories.add(key_match.group(1))

    if not categories:
        results["warn"].append("Could not extract CATEGORIES from cost_tracker.py")
        return results

    # Scan all .py files for tracker.log(category="X", ...)
    # Exclude engine_checks/ (contains regex patterns that produce false matches)
    cat_pattern = re.compile(r'tracker\.log\([^)]*category\s*=\s*["\']([^"\']+)["\']')
    cats_in_code = {}

    for rel in sorted(discovered):
        if not rel.endswith(".py"):
            continue
        if "engine_checks/" in rel:
            continue
        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        for match in cat_pattern.finditer(content):
            cat = match.group(1)
            if cat not in cats_in_code:
                cats_in_code[cat] = []
            cats_in_code[cat].append(os.path.basename(rel))

    invalid = []
    for cat, files in sorted(cats_in_code.items()):
        if cat not in categories:
            invalid.append((cat, files))

    if invalid:
        for cat, files in invalid:
            results["fail"].append(
                f"tracker.log(category='{cat}') in {', '.join(set(files))} — "
                f"not in CATEGORIES dict"
            )
    else:
        results["pass"].append(
            f"All {len(cats_in_code)} category values match CATEGORIES dict"
        )

    return results


def check_lora_registry_integrity(base, discovered):
    """Check LoRA registry files for valid structure."""
    results = {"pass": [], "fail": [], "warn": []}

    registry_files = [
        rel for rel in discovered
        if os.path.basename(rel) == "lora_registry.json"
    ]

    if not registry_files:
        results["pass"].append("No lora_registry.json files found (pre-LoRA phase)")
        return results

    REQUIRED_FIELDS = {"character", "model_type", "status"}

    for rel in registry_files:
        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                data = json.load(f)
        except json.JSONDecodeError as e:
            results["fail"].append(f"{rel}: JSON parse error: {e}")
            continue
        except (IOError, OSError):
            results["warn"].append(f"{rel}: cannot read")
            continue

        if not isinstance(data, (list, dict)):
            results["fail"].append(f"{rel}: unexpected type {type(data).__name__} (expected list or dict)")
            continue

        entries = data if isinstance(data, list) else list(data.values())

        for i, entry in enumerate(entries):
            if not isinstance(entry, dict):
                continue
            missing = REQUIRED_FIELDS - set(entry.keys())
            if missing:
                results["warn"].append(
                    f"{rel}: entry {i} missing fields: {', '.join(sorted(missing))}"
                )

        results["pass"].append(f"{rel}: {len(entries)} LoRA entries validated")

    return results


def check_visual_bible_template(base, _discovered):
    """Verify visual_bible_template.md has all expected sections."""
    results = {"pass": [], "fail": [], "warn": []}

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

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

    EXPECTED_SECTIONS = [
        "Color Palette",
        "Characters",
        "Wardrobe",
        "Props",
        "Locations",
        "Lens Package",
        "Lighting",
    ]

    # Also check against validate_visual_bible.py expectations
    validator_path = os.path.join(base, "tools", "validate_visual_bible.py")
    validator_sections = []
    if os.path.exists(validator_path):
        with open(validator_path) as f:
            val_content = f.read()
        # Extract section names the validator checks for
        # Filter out placeholder patterns like {section_name}
        for match in re.finditer(r'["\']##\s*([^"\']+)["\']', val_content):
            section = match.group(1).strip()
            if '{' not in section and '}' not in section:
                validator_sections.append(section)

    missing = []
    for section in EXPECTED_SECTIONS:
        if section.lower() not in content.lower():
            missing.append(section)

    if missing:
        results["fail"].append(
            f"visual_bible_template.md: missing sections: {', '.join(missing)}"
        )
    else:
        results["pass"].append(
            f"visual_bible_template.md: all {len(EXPECTED_SECTIONS)} expected sections present"
        )

    if validator_sections:
        template_sections_lower = content.lower()
        val_missing = [s for s in validator_sections
                       if s.lower() not in template_sections_lower]
        if val_missing:
            results["warn"].append(
                f"visual_bible_template.md: validator checks for sections not in template: "
                f"{', '.join(val_missing)}"
            )

    return results


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

register_check("config_exports", "Config Loader Exports", check_config_loader_exports, "visual")
register_check("engine_cost_map", "Engine Cost Map", check_engine_cost_map, "visual")
register_check("pricing_completeness", "Pricing Completeness", check_pricing_completeness, "visual")
register_check("config_template", "Project Config Template", check_project_config_template, "visual")
register_check("rendering_directives", "Rendering Directives Schema", check_rendering_directives_schema, "visual")
register_check("cost_categories", "Cost Tracker Categories", check_cost_tracker_categories, "visual")
register_check("lora_registry", "LoRA Registry Integrity", check_lora_registry_integrity, "visual")
register_check("vb_template", "Visual Bible Template", check_visual_bible_template, "visual")

register_section("visual", [
    "config_exports", "engine_cost_map", "pricing_completeness", "config_template",
    "rendering_directives", "cost_categories", "lora_registry", "vb_template",
])
