"""
src/llm/dramaturg.py — LLM-based thematic beat classification (OpenRouter)

Responsibility:
  - Receive a list of TrailerBeat objects (with dialogue lines attached)
  - Send a single structured prompt to the LLM
  - Parse the JSON response to assign BeatType to each beat

IMPORTANT: This module does ZERO visual analysis.
           It classifies narrative dramaturgy from dialogue text only.
           Visual matching is handled exclusively by the CV engine.
"""

from __future__ import annotations

import json
import logging
from dataclasses import replace
from typing import Sequence

from src.core.config import AppConfig
from src.core.models import BeatType, TrailerBeat

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Prompt builder
# ---------------------------------------------------------------------------

_SYSTEM_PROMPT = """You are a film trailer editor and narrative analyst.
Your task is to classify each beat of a trailer into one of these dramatic roles:
  HOOK        - Opening attention grabber (first impression, shocking image, logo)
  SETUP       - World/character introduction
  CONFLICT    - Inciting incident, rising tension, threat revealed
  CLIMAX      - Peak action/emotion, highest stakes
  RESOLUTION  - Cool-down, tagline, final title card

You will receive a JSON array of beats with their index and dialogue text.
Respond ONLY with a valid JSON array, one object per beat, with keys:
  "beat_id" (int) and "beat_type" (one of the strings above).
Do NOT include any explanation or markdown fences."""

_USER_TEMPLATE = """Classify the following {n} trailer beats:

{beats_json}"""


def _build_beats_payload(beats: Sequence[TrailerBeat]) -> str:
    payload = []
    for b in beats:
        dialogue_text = " / ".join(line.text for line in b.dialogue) or "(no dialogue)"
        payload.append({
            "beat_id":  b.beat_id,
            "duration": round(b.duration_s, 2),
            "dialogue": dialogue_text,
        })
    return json.dumps(payload, ensure_ascii=False, indent=2)


# ---------------------------------------------------------------------------
# OpenRouter / OpenAI-compatible HTTP client
# ---------------------------------------------------------------------------

def _call_llm(prompt_user: str, cfg: AppConfig) -> str:
    """
    Send a chat completion request to the configured LLM provider.

    Supports: openrouter, openai, ollama (all use the OpenAI-compatible API).

    Returns:
        The raw text content of the first assistant message.

    Raises:
        RuntimeError: On HTTP errors or missing API key.
    """
    import urllib.request
    import urllib.error

    llm = cfg.llm

    if llm.provider in ("openrouter", "openai") and not llm.api_key:
        raise RuntimeError(
            f"LLM provider is '{llm.provider}' but no API key found. "
            "Set OPENROUTER_API_KEY (or OPENAI_API_KEY) in your .env file."
        )

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {llm.api_key}",
    }
    if llm.provider == "openrouter":
        headers["HTTP-Referer"] = "https://github.com/ai-trailer-2026"
        headers["X-Title"]      = "AI Trailer Generator v2"

    body = json.dumps({
        "model": llm.model,
        "messages": [
            {"role": "system", "content": _SYSTEM_PROMPT},
            {"role": "user",   "content": prompt_user},
        ],
        "temperature": llm.temperature,
        "max_tokens":  llm.max_tokens,
    }).encode("utf-8")

    url = f"{llm.base_url.rstrip('/')}/chat/completions"

    req = urllib.request.Request(url, data=body, headers=headers, method="POST")

    try:
        with urllib.request.urlopen(req, timeout=llm.timeout_seconds) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            return data["choices"][0]["message"]["content"]
    except urllib.error.HTTPError as exc:
        body_text = exc.read().decode(errors="replace")
        raise RuntimeError(
            f"LLM HTTP {exc.code} from {url}:\n{body_text}"
        ) from exc


# ---------------------------------------------------------------------------
# Response parser
# ---------------------------------------------------------------------------

_BEAT_TYPE_MAP: dict[str, BeatType] = {bt.name: bt for bt in BeatType}


def _parse_response(raw: str, beats: Sequence[TrailerBeat]) -> dict[int, BeatType]:
    """
    Parse the LLM JSON array response into a beat_id → BeatType mapping.

    Falls back to BeatType.UNKNOWN for any beat that cannot be parsed.
    """
    # Strip accidental markdown fences
    clean = raw.strip()
    if clean.startswith("```"):
        clean = "\n".join(clean.split("\n")[1:])
    if clean.endswith("```"):
        clean = clean[: clean.rfind("```")]
    clean = clean.strip()

    result: dict[int, BeatType] = {b.beat_id: BeatType.UNKNOWN for b in beats}

    try:
        parsed = json.loads(clean)
        if not isinstance(parsed, list):
            raise ValueError("Expected JSON array at top level.")

        for item in parsed:
            bid  = int(item["beat_id"])
            name = str(item.get("beat_type", "UNKNOWN")).upper()
            result[bid] = _BEAT_TYPE_MAP.get(name, BeatType.UNKNOWN)

    except (json.JSONDecodeError, KeyError, ValueError) as exc:
        logger.warning("LLM response parse error (%s) — all beats → UNKNOWN.\nRaw: %s", exc, raw[:300])

    return result


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def classify_beats(
    beats: Sequence[TrailerBeat],
    cfg: AppConfig,
) -> list[TrailerBeat]:
    """
    Use the LLM to assign a BeatType to each TrailerBeat.

    Args:
        beats: TrailerBeat list (dialogue should be populated for best results).
        cfg:   Application configuration (llm section + api key).

    Returns:
        New list of TrailerBeat objects with beat_type set.
        On LLM error, all beats keep BeatType.UNKNOWN (no exception raised).
    """
    if not beats:
        return list(beats)

    logger.info(
        "Classifying %d beats via %s / %s …",
        len(beats), cfg.llm.provider, cfg.llm.model,
    )

    payload = _build_beats_payload(beats)
    prompt  = _USER_TEMPLATE.format(n=len(beats), beats_json=payload)

    try:
        raw_response = _call_llm(prompt, cfg)
    except Exception as exc:
        logger.error("LLM classification failed: %s — keeping BeatType.UNKNOWN.", exc)
        return list(beats)

    type_map = _parse_response(raw_response, beats)

    enriched = [replace(b, beat_type=type_map.get(b.beat_id, BeatType.UNKNOWN)) for b in beats]

    classified = sum(1 for b in enriched if b.beat_type != BeatType.UNKNOWN)
    logger.info("Beat classification done: %d / %d classified.", classified, len(beats))
    return enriched
