"""
Inputs Snapshot Library — Phase 1

Pure functions that build immutable inputs snapshot dicts from pipeline context.
No side effects, no imports from step_runner or execution_store.
"""

import hashlib
import json
from datetime import datetime


def _hash_prefix(obj: dict, length: int = 12) -> str:
    """Hash first 100 chars of JSON-serialized dict, return hex prefix."""
    raw = json.dumps(obj, sort_keys=True)[:100]
    return hashlib.md5(raw.encode("utf-8")).hexdigest()[:length]


def build_inputs_snapshot(
    prompt_sections: list[dict],
    routing: dict,
    refs_sent: list[dict],
    bible: dict,
    project_config: dict,
    generation_params: dict | None = None,
    overrides: list[dict] | None = None,
    bypassed_layers: list[str] | None = None,
    parent_take_id: str | None = None,
    builder_name: str | None = None,
    bible_files: list[dict] | None = None,
) -> dict:
    """
    Build an immutable inputs snapshot dict from production pipeline context.

    prompt_sections: list from build_prompt_sections_from_plan(), each dict has
        {id, label, text, color, active, bypass_key}
    routing: dict with {pipeline, model, tier, reason}
    refs_sent: list of dicts {type, id, url, label, sent_to_model}
    bible: full bible dict (hashed, not stored)
    project_config: full config dict (hashed, not stored)
    generation_params: optional dict (seed, guidance_scale, CFG, etc.)
    overrides: optional list of {type, description, author, original_text?}
    bypassed_layers: optional list of layer IDs that were bypassed
    parent_take_id: optional id of the parent take whose output fed this take
        (typically a keyframe id when generating i2v video). Used by the lineage
        adapter to render a "parent_take" manifest node with a jump-to-inspect
        affordance.
    builder_name: optional name of the prompt builder that produced this
        prompt (e.g. "build_kling_i2v_prompt"). Used by the lineage adapter
        to label the params/prompt card.
    bible_files: optional list of {file, sections} dicts describing which
        bible YAML files (and which top-level keys) were assembled into the
        prompt context. Currently not populated by any caller — wired for
        a future pass that will surface source attribution in the lineage UI.
    """
    # Transform sections into prompt layers (drop bypass_key)
    prompt_layers = [
        {
            "id": s["id"],
            "label": s["label"],
            "text": s["text"],
            "color": s["color"],
            "active": s["active"],
        }
        for s in prompt_sections
    ]

    # Build flat prompt from active sections
    prompt_flat = "\n\n".join(
        s["text"] for s in prompt_sections if s.get("active")
    )

    return {
        "source": "snapshot",
        "prompt_layers": prompt_layers,
        "prompt_flat": prompt_flat,
        "refs_used": list(refs_sent),
        "routing": dict(routing),
        "overrides": list(overrides) if overrides else [],
        "bypassed_layers": list(bypassed_layers) if bypassed_layers else [],
        "bible_version": _hash_prefix(bible),
        "config_hash": _hash_prefix(project_config),
        "generation_params": dict(generation_params) if generation_params else None,
        "captured_at": datetime.utcnow().isoformat() + "Z",
        "parent_take_id": parent_take_id,
        "builder_name": builder_name,
        "bible_files": list(bible_files) if bible_files else None,
    }


def build_previz_inputs_snapshot(
    context_parts: list[tuple],
    shot: dict,
    bible: dict,
    continuity_shots: list[dict] | None = None,
    generation_params: dict | None = None,
    parent_take_id: str | None = None,
    builder_name: str | None = None,
    bible_files: list[dict] | None = None,
) -> dict:
    """
    Build an immutable inputs snapshot dict from previz pipeline context.

    context_parts: list from build_previz_context(), each tuple is
        (data_bytes|None, mime_type|"text", label_text)
    shot: the shot dict being rendered
    bible: full bible dict (hashed, not stored)
    continuity_shots: optional list of shot dicts in the 3-shot window
    generation_params: optional dict (seed, guidance_scale, etc.)
    """
    prompt_layers = []
    refs_used = []
    other_text_counter = 0

    for data, mime_or_text, label_text in context_parts:
        is_text = mime_or_text == "text"

        if is_text:
            text_content = label_text
            upper = (text_content or "").upper()

            if "BEHAVIORAL" in upper or "SYSTEM" in upper:
                prompt_layers.append({
                    "id": "behavioral_preamble",
                    "label": "Behavioral Preamble",
                    "text": text_content,
                    "color": "#9CA3AF",
                    "active": True,
                })
            elif "BIBLE" in upper:
                prompt_layers.append({
                    "id": "bible_excerpt",
                    "label": "Scoped Bible",
                    "text": text_content,
                    "color": "#4A9E8E",
                    "active": True,
                })
            elif "WINDOW" in upper or "CONTINUITY" in upper:
                prompt_layers.append({
                    "id": "continuity_window",
                    "label": "3-Shot Window",
                    "text": text_content,
                    "color": "#4A7EC8",
                    "active": True,
                })
            elif "DIRECTIVE" in upper or "GENERATIVE" in upper:
                prompt_layers.append({
                    "id": "shot_directive",
                    "label": "Shot Directive",
                    "text": text_content,
                    "color": "#E8A735",
                    "active": True,
                })
            else:
                other_text_counter += 1
                prompt_layers.append({
                    "id": f"context_text_{other_text_counter}",
                    "label": (text_content or "")[:40],
                    "text": text_content,
                    "color": "#6B7280",
                    "active": True,
                })
        else:
            # Image part — classify by label substring
            lower_label = (label_text or "").lower()
            if "location" in lower_label or "moodboard" in lower_label:
                ref_type = "location"
            elif (
                "character" in lower_label
                or "identity" in lower_label
                or "hero" in lower_label
                or "turnaround" in lower_label
            ):
                ref_type = "character"
            elif "expression" in lower_label:
                ref_type = "expression"
            else:
                ref_type = "other"

            refs_used.append({
                "type": ref_type,
                "id": "",
                "url": "",
                "label": label_text,
                "sent_to_model": True,
            })

    # Build flat prompt from all layers
    prompt_flat = "\n\n".join(
        layer["text"] for layer in prompt_layers if layer.get("active") and layer.get("text")
    )

    # Extract continuity shot IDs if provided
    continuity_shot_ids = []
    if continuity_shots:
        continuity_shot_ids = [
            s.get("shot_id", s.get("id", "")) for s in continuity_shots
        ]

    return {
        "source": "snapshot",
        "prompt_layers": prompt_layers,
        "prompt_flat": prompt_flat,
        "refs_used": refs_used,
        "routing": {
            "pipeline": "previz",
            "model": "gemini-3.1-flash-image-preview",
            "tier": "previz",
            "reason": "Flash full-context",
        },
        "overrides": [],
        "bypassed_layers": [],
        "bible_version": _hash_prefix(bible),
        "config_hash": "",
        "generation_params": dict(generation_params) if generation_params else None,
        "continuity_shots": continuity_shot_ids,
        "captured_at": datetime.utcnow().isoformat() + "Z",
        "parent_take_id": parent_take_id,
        "builder_name": builder_name,
        "bible_files": list(bible_files) if bible_files else None,
    }


def reconstruct_inputs(
    take: dict,
    shot: dict,
    bible: dict,
    project_config: dict,
    prompt_sections: list | None = None,
    reference_images: list | None = None,
) -> dict:
    """
    Build a best-effort inputs snapshot for legacy takes without inputs_snapshot.

    Returns a dict matching TakeInputs schema with source="reconstructed".
    The UI shows an amber "Reconstructed" badge for these.

    prompt_sections: pre-computed from _compute_prompt_sections() if available
    reference_images: pre-computed from _resolve_reference_images() if available
    """
    prompt_flat = take.get("prompt_used", "")

    # Recover prompt layers from shot-level prompt_sections
    prompt_layers = []
    if prompt_sections:
        for s in prompt_sections:
            prompt_layers.append({
                "id": s.get("id", ""),
                "label": s.get("label", ""),
                "text": s.get("text", ""),
                "color": s.get("color", "#888"),
                "active": s.get("active", True),
            })
        # If we have layers but no flat prompt, build it from active layers
        if not prompt_flat and prompt_layers:
            prompt_flat = "\n\n".join(
                layer["text"] for layer in prompt_layers if layer.get("active") and layer.get("text")
            )

    # Recover refs from shot-level reference_images
    refs_used = []
    if reference_images:
        for ref in reference_images:
            refs_used.append({
                "type": ref.get("type", ""),
                "id": ref.get("id", ""),
                "url": ref.get("url", ""),
                "label": ref.get("label", ""),
                "sent_to_model": True,
            })

    # Recover bypassed layers from prompt_sections
    bypassed_layers = []
    if prompt_sections:
        bypassed_layers = [s["id"] for s in prompt_sections if not s.get("active", True)]

    # Try to recover routing from shot-level data
    routing = {}
    if shot.get("routing"):
        routing = dict(shot["routing"])
    elif shot.get("pipeline"):
        routing = {
            "pipeline": shot.get("pipeline", ""),
            "model": shot.get("model", ""),
            "tier": shot.get("tier", ""),
            "reason": "reconstructed from shot metadata",
        }

    return {
        "source": "reconstructed",
        "prompt_layers": prompt_layers,
        "prompt_flat": prompt_flat,
        "refs_used": refs_used,
        "routing": routing,
        "overrides": [],
        "bypassed_layers": bypassed_layers,
        "bible_version": _hash_prefix(bible) if bible else "",
        "config_hash": _hash_prefix(project_config) if project_config else "",
        "generation_params": None,
        "captured_at": datetime.utcnow().isoformat() + "Z",
    }
