from __future__ import annotations

from dataclasses import dataclass
import re
from typing import Any

import yaml

from .deck_models import DeckSpec

YAML_TAGGED_CODE_BLOCK_RE = re.compile(r"```(?:yaml|yml)[ \t]*\n?(.*?)```", re.IGNORECASE | re.DOTALL)
YAML_UNMARKED_CODE_BLOCK_RE = re.compile(r"```[ \t]*\n?(.*?)```", re.DOTALL)
YAML_FENCE_LINE_RE = re.compile(r"(?m)^[ \t]*```(?:yaml|yml)?[ \t]*$|^[ \t]*```[ \t]*$")
DECK_ROOT_RE = re.compile(r"(?m)^[ \t]*deck:")
SLIDES_ROOT_RE = re.compile(r"(?m)^[ \t]*slides:")


@dataclass(frozen=True)
class _YamlCandidate:
    text: str
    order: int
    source: str


@dataclass(frozen=True)
class _ParsedYamlCandidate:
    text: str
    payload: Any
    order: int
    source: str


class PptxDslSyntaxError(ValueError):
    """Raised when the response contains YAML candidates but none can be parsed syntactically."""


class PptxDslContentError(ValueError):
    """Raised when the response content is structurally invalid but still retryable."""


def _normalize_yaml_candidate(text: str) -> str | None:
    normalized = text.strip()
    return normalized or None


def _collect_yaml_candidates(raw_text: str) -> list[_YamlCandidate]:
    if not raw_text or not raw_text.strip():
        raise PptxDslContentError("PPTX DSL response is empty")

    candidates: list[_YamlCandidate] = []
    seen: set[str] = set()

    def add_candidate(text: str, source: str) -> None:
        normalized = _normalize_yaml_candidate(text)
        if not normalized or normalized in seen:
            return
        seen.add(normalized)
        candidates.append(_YamlCandidate(text=normalized, order=len(candidates), source=source))

    for block in YAML_TAGGED_CODE_BLOCK_RE.findall(raw_text):
        add_candidate(block, source="tagged_fence")

    for block in YAML_UNMARKED_CODE_BLOCK_RE.findall(raw_text):
        add_candidate(block, source="unmarked_fence")

    sanitized = YAML_FENCE_LINE_RE.sub("", raw_text).strip()
    add_candidate(sanitized, source="sanitized")

    anchored = _slice_from_first_yaml_root(sanitized)
    if anchored:
        add_candidate(anchored, source="anchored_root")

    return candidates


def _slice_from_first_yaml_root(text: str) -> str | None:
    deck_match = DECK_ROOT_RE.search(text)
    if deck_match:
        return text[deck_match.start() :].strip()

    slides_match = SLIDES_ROOT_RE.search(text)
    if slides_match:
        return text[slides_match.start() :].strip()

    return None


def _parse_yaml_candidate(candidate: _YamlCandidate) -> _ParsedYamlCandidate:
    payload = yaml.safe_load(candidate.text)
    return _ParsedYamlCandidate(
        text=candidate.text,
        payload=payload,
        order=candidate.order,
        source=candidate.source,
    )


def _candidate_root_priority(candidate: _ParsedYamlCandidate) -> int:
    has_deck = "deck" in candidate.payload
    has_slides = "slides" in candidate.payload
    if has_deck and has_slides:
        return 0
    if has_slides:
        return 1
    return 2


def _select_preferred_candidate(parsed_candidates: list[_ParsedYamlCandidate]) -> _ParsedYamlCandidate:
    mapping_candidates = [candidate for candidate in parsed_candidates if isinstance(candidate.payload, dict)]
    if mapping_candidates:
        return min(
            mapping_candidates,
            key=lambda candidate: (_candidate_root_priority(candidate), candidate.order),
        )

    return min(parsed_candidates, key=lambda candidate: candidate.order)


def _select_yaml_candidate(raw_text: str) -> _ParsedYamlCandidate:
    candidates = _collect_yaml_candidates(raw_text)
    syntax_errors: list[yaml.YAMLError] = []
    parsed_candidates: list[_ParsedYamlCandidate] = []

    for candidate in candidates:
        try:
            parsed = _parse_yaml_candidate(candidate)
        except yaml.YAMLError as exc:
            syntax_errors.append(exc)
            continue
        parsed_candidates.append(parsed)

    mapping_candidates = [candidate for candidate in parsed_candidates if isinstance(candidate.payload, dict)]
    if mapping_candidates:
        return _select_preferred_candidate(mapping_candidates)
    if any(candidate.source != "sanitized" for candidate in parsed_candidates):
        return min(parsed_candidates, key=lambda candidate: candidate.order)
    if syntax_errors:
        raise PptxDslSyntaxError(f"PPTX DSL YAML parsing failed: {syntax_errors[0]}") from syntax_errors[0]
    raise PptxDslContentError("PPTX DSL response did not contain exploitable YAML")


def parse_pptx_spec(raw_text: str) -> DeckSpec:
    """Parse one raw LLM response into a validated deck specification.

    The function first extracts the most plausible YAML candidate from the response, tolerating
    markdown fences and extra prose when possible. It then requires the selected YAML payload to be
    a mapping with a top-level `slides` key before delegating the structural validation to
    `DeckSpec.model_validate(...)`.
    """
    candidate = _select_yaml_candidate(raw_text)
    payload = candidate.payload

    if not isinstance(payload, dict):
        raise PptxDslContentError("PPTX DSL root must be a mapping")
    if "slides" not in payload:
        raise PptxDslContentError("PPTX DSL root must contain slides")

    return DeckSpec.model_validate(payload)
