#!/usr/bin/env python3
"""
config_loader.py — Shared config loader for the Recoil visual pipeline.

Eliminates duplicated defaults across engine_shootout.py, batch_generate_refs.py,
and prompt_compiler.py. All tools read from the same source of truth.

Two functions:
  - load_project_config(project_path)  — merges file with defaults
  - load_rendering_directives(project_path, character_key) — per-character directives
"""

import json
import logging
from pathlib import Path

from recoil.core.exceptions import ConfigParseError

log = logging.getLogger(__name__)


# ── Default Project Config ────────────────────────────────────────────────
# These values are used when project_config.json is missing or incomplete.
# The production pipeline (prompt_compiler) and candidate gen tools share them.

DEFAULT_PROJECT_CONFIG = {
    "camera_body": "Arri Alexa Mini LF",
    "film_stock": "Kodak Vision3 500T",
    "film_style_suffix": "visible grain, photorealistic, ultra-detailed",
    "quality_guard": (
        "Correct human anatomy, anatomically correct proportions, "
        "five fingers per hand, sharp focus, clean detailed image, "
        "natural skin texture with pores"
    ),
    "negative_prompt": (
        "deformed hands, extra fingers, mutated hands, poorly drawn hands, "
        "extra limbs, fused fingers, too many fingers, long neck, "
        "blurry, low quality, illustration, cartoon, painting, drawing, "
        "3d render, anime, cgi, digital art, smooth skin"
    ),
    "hex_object_map": {},
    "candidate_lenses": {
        "face": "85mm f/1.8 prime",
        "body": "35mm f/2.8 prime",
        "close_up": "85mm f/1.4 prime",
    },
}


# ── Default Rendering Directives ──────────────────────────────────────────
# Fallback for characters without rendering_directives in breakdown.json.
# Assumes a human character with natural skin.

DEFAULT_RENDERING_DIRECTIVES = {
    "texture_prompt": (
        "Photograph on Kodak Portra 400. Real human skin — visible pores on "
        "nose, cheeks, chin, and forehead. Natural imperfections: uneven skin tone, "
        "minor redness, faint acne scars, moles, stray facial hair, subtle under-eye "
        "darkness, micro-wrinkles around the eyes. Natural subsurface scattering — "
        "slightly translucent at ears and nostrils. Peach fuzz on jawline and temples, "
        "individual eyebrow hairs, zone-appropriate oiliness or dryness. "
        "Candid editorial photograph — Magnum Photos or Gregory Crewdson. "
        "Real humans with real skin in real light. "
        "If input has plastic or overly smooth skin, ADD texture and imperfections."
    ),
    "texture_negative": (
        "smooth plastic skin, airbrushed, wax figure, porcelain, doll-like, "
        "CGI render, video game character, overly clean skin, beauty filter"
    ),
    "mandatory_traits": "",
    "identity_type": "human",  # "human" or "non_human" — controls identity lock language
}

# ── Identity Lock Templates ─────────────────────────────────────────────
# Used by engine_shootout.py to build the face-identity lock portion of
# the NBP prompt. "human" uses full anatomical anchors. "non_human" drops
# skull/brow cues that cause NBP to strip helmets and chassis elements.

IDENTITY_LOCK_HUMAN = (
    "Maintain identical skull structure, brow ridge, nose bridge, nose width, "
    "cheekbone position, chin shape, ear shape, eye spacing, eye size, iris color, "
    "skin tone, and skin texture.\n"
)

IDENTITY_LOCK_NON_HUMAN = (
    "Maintain identical facial proportions, nose bridge, nose width, "
    "cheekbone position, chin shape, eye spacing, eye size, iris color, "
    "skin tone, and skin texture.\n"
    "This character is NOT a baseline human. Do NOT infer a bare human head. "
    "Preserve whatever helmet, chassis, head covering, or non-human structure "
    "is visible in the input image. Do NOT add human hair where none exists.\n"
)


def get_identity_lock(identity_type: str = "human") -> str:
    """Return the appropriate identity lock prompt for the character type."""
    if identity_type == "non_human":
        return IDENTITY_LOCK_NON_HUMAN
    return IDENTITY_LOCK_HUMAN


def load_project_config(project_path) -> dict:
    """Load project_config.json, deep-merging with defaults.

    Args:
        project_path: Path to project directory (e.g. 'leviathan' or Path object).
                      Can be a string name resolved relative to the engine root,
                      or an absolute/relative Path.

    Returns:
        dict with all default keys guaranteed present, overridden by file values.
    """
    project_path = Path(project_path)

    # If it's a bare name like 'leviathan', resolve relative to projects dir or recoil root
    if not project_path.is_absolute() and not (project_path / "visual").is_dir():
        engine_lib = Path(__file__).resolve().parent
        recoil_root = engine_lib.parent
        # Check sibling projects/ directory first (new layout)
        projects_dir = recoil_root.parent / "projects"
        candidate = projects_dir / project_path
        if (candidate / "visual").is_dir():
            project_path = candidate
        else:
            # Fallback to recoil root (legacy layout)
            candidate = recoil_root / project_path
            if (candidate / "visual").is_dir():
                project_path = candidate

    config_path = project_path / "visual" / "project_config.json"

    # Start with defaults
    merged = dict(DEFAULT_PROJECT_CONFIG)

    if config_path.exists():
        try:
            with open(config_path) as f:
                file_config = json.load(f)
        except FileNotFoundError:
            file_config = {}  # legitimate missing file
        except (json.JSONDecodeError, OSError) as e:
            log.warning("config_loader: parse error at %s — %s", config_path, e)
            raise ConfigParseError(str(config_path), message=str(e)) from e

        # Shallow merge top-level keys; deep-merge dicts
        for key, value in file_config.items():
            if isinstance(value, dict) and isinstance(merged.get(key), dict):
                merged[key] = {**merged[key], **value}
            else:
                merged[key] = value

    return merged


def load_rendering_directives(project_path, character_key: str) -> dict:
    """Load character-specific rendering directives from breakdown.json.

    Falls back per-field to DEFAULT_RENDERING_DIRECTIVES. For mandatory_traits,
    falls back to the character's visual_description if no explicit traits set.

    Args:
        project_path: Path to project directory.
        character_key: Character key (e.g. 'JINX', 'KIAN').

    Returns:
        dict with keys: texture_prompt, texture_negative, mandatory_traits, identity_type.
    """
    project_path = Path(project_path)

    # Resolve bare names
    if not project_path.is_absolute() and not (project_path / "visual").is_dir():
        engine_lib = Path(__file__).resolve().parent
        recoil_root = engine_lib.parent
        # Check sibling projects/ directory first (new layout)
        projects_dir = recoil_root.parent / "projects"
        candidate = projects_dir / project_path
        if (candidate / "visual").is_dir():
            project_path = candidate
        else:
            # Fallback to recoil root (legacy layout)
            candidate = recoil_root / project_path
            if (candidate / "visual").is_dir():
                project_path = candidate

    breakdown_path = project_path / "visual" / "breakdown.json"
    char_upper = character_key.upper()

    # Start with defaults
    result = dict(DEFAULT_RENDERING_DIRECTIVES)

    if breakdown_path.exists():
        try:
            bd = json.loads(breakdown_path.read_text())
        except FileNotFoundError:
            return result  # legitimate missing file
        except (json.JSONDecodeError, OSError) as e:
            log.warning(
                "config_loader: breakdown parse error at %s — %s",
                breakdown_path, e,
            )
            raise ConfigParseError(str(breakdown_path), message=str(e)) from e

        char_data = bd.get("characters", {}).get(char_upper, {})
        directives = char_data.get("rendering_directives", {})

        # Override each field if present in breakdown
        if directives.get("texture_prompt"):
            result["texture_prompt"] = directives["texture_prompt"]
        if directives.get("texture_negative"):
            result["texture_negative"] = directives["texture_negative"]
        if directives.get("mandatory_traits"):
            result["mandatory_traits"] = directives["mandatory_traits"]
        elif not result["mandatory_traits"]:
            # Fall back to visual_description if no mandatory_traits anywhere
            result["mandatory_traits"] = char_data.get("visual_description", "")
        if directives.get("identity_type"):
            result["identity_type"] = directives["identity_type"]

    return result
