from __future__ import annotations

import json

import yaml
from yaml import YAMLError

from backend.utils.logging_utils import get_logger

from . import internal_utils, planning, prompts
from .parsing import _select_yaml_candidate
from .quality_gate import DeckQualityIssue, format_quality_gate_findings

logger = get_logger(__name__)

MAX_PPTX_PLAN_ATTEMPTS = 3
MAX_TARGETED_REPAIR_SLIDES = 3


def generate_deck_plan(
    project,
    llm_id: str,
    user_prompt: str,
    *,
    user_message: dict[str, object] | None = None,
    conversation_history: list[dict[str, object]] | None = None,
) -> planning.DeckPlan:
    """Generate and validate the compact deck plan that constrains the later full deck YAML phase.

    The function first asks the LLM for a lightweight YAML plan containing slide count, slide order,
    and allowed slide types. That output is parsed with `parse_deck_plan(...)` and then checked again
    with `validate_deck_plan(...)` for business rules tied to the original user prompt.

    If parsing or validation fails, the invalid plan plus the concrete error are fed back into a
    dedicated plan-repair prompt. This loop is bounded by `MAX_PPTX_PLAN_ATTEMPTS` and returns only
    once a structurally valid, business-valid `DeckPlan` is available for the downstream deck
    generation phase.
    """
    raw_plan = _run_llm_phase(
        project=project,
        llm_id=llm_id,
        phase="plan",
        messages=[
            {"role": "system", "content": prompts.build_pptx_plan_system_prompt()},
            *(conversation_history or []),
            _build_user_message(text=prompts.build_pptx_plan_user_prompt(user_prompt), source_message=user_message),
        ],
    )

    for attempt in range(1, MAX_PPTX_PLAN_ATTEMPTS + 1):
        try:
            deck_plan = planning.parse_deck_plan(raw_plan)
            planning.validate_deck_plan(deck_plan, user_prompt)
            return deck_plan
        except (ValueError, TypeError, YAMLError) as exc:
            if attempt == MAX_PPTX_PLAN_ATTEMPTS:
                raise
            logger.warning(
                "PPTX plan validation failed for attempt %s/%s; requesting corrected plan: %s",
                attempt,
                MAX_PPTX_PLAN_ATTEMPTS,
                exc,
            )
            raw_plan = _run_llm_phase(
                project=project,
                llm_id=llm_id,
                phase="plan_repair",
                messages=[
                    {"role": "system", "content": prompts.build_pptx_plan_system_prompt()},
                    _build_user_message(
                        text=prompts.build_pptx_plan_repair_prompt(
                            user_prompt=user_prompt,
                            previous_plan_yaml=raw_plan,
                            validation_error=internal_utils.format_retryable_generation_error(exc),
                            attempt=attempt + 1,
                            max_attempts=MAX_PPTX_PLAN_ATTEMPTS,
                        ),
                        source_message=user_message,
                    ),
                ],
            )

    raise ValueError("PPTX generation did not produce a valid deck plan")


def generate_deck_yaml(
    project,
    llm_id: str,
    user_prompt: str,
    deck_plan_yaml: str,
    *,
    user_message: dict[str, object] | None = None,
    conversation_history: list[dict[str, object]] | None = None,
) -> str:
    """Generate the full deck YAML from the original request plus an already validated deck plan."""

    return _run_llm_phase(
        project=project,
        llm_id=llm_id,
        phase="deck",
        messages=[
            {"role": "system", "content": prompts.build_pptx_system_prompt()},
            *(conversation_history or []),
            _build_user_message(
                text=prompts.build_pptx_deck_user_prompt(
                    user_prompt=user_prompt,
                    deck_plan_yaml=deck_plan_yaml,
                ),
                source_message=user_message,
            ),
        ],
    )


def revise_deck_yaml(
    *,
    project,
    llm_id: str,
    source_deck_yaml: str,
    revision_instructions: str,
    user_message: dict[str, object] | None = None,
    conversation_history: list[dict[str, object]] | None = None,
) -> str:
    """Generate a full revised deck YAML from an existing deck YAML and natural-language change request."""

    return _run_llm_phase(
        project=project,
        llm_id=llm_id,
        phase="revision",
        messages=[
            ("system", prompts.build_pptx_system_prompt()),
            *(conversation_history or []),
            _build_user_message(
                text=prompts.build_pptx_revision_user_prompt(
                    source_deck_yaml=source_deck_yaml,
                    revision_instructions=revision_instructions,
                ),
                source_message=user_message,
            ),
        ],
    )


def repair_full_deck_yaml(
    *,
    project,
    llm_id: str,
    user_prompt: str,
    deck_plan_yaml: str,
    current_deck_yaml: str,
    validation_error: str,
    quality_feedback: str | None,
    attempt: int,
    max_attempts: int,
    user_message: dict[str, object] | None = None,
) -> str:
    """Request one full-deck YAML rewrite when validation, planning, or quality checks fail broadly."""

    logger.warning(
        "PPTX full deck repair requested for attempt %s/%s: %s",
        attempt - 1,
        max_attempts,
        validation_error,
    )
    return _run_llm_phase(
        project=project,
        llm_id=llm_id,
        phase="repair",
        messages=[
            ("system", prompts.build_pptx_system_prompt()),
            _build_user_message(
                text=prompts.build_pptx_full_repair_prompt(
                    user_prompt=user_prompt,
                    deck_plan_yaml=deck_plan_yaml,
                    current_deck_yaml=current_deck_yaml,
                    validation_error=validation_error,
                    quality_feedback=quality_feedback,
                    attempt=attempt,
                    max_attempts=max_attempts,
                ),
                source_message=user_message,
            ),
        ],
    )


def repair_syntax_only(
    *,
    project,
    llm_id: str,
    user_prompt: str,
    deck_plan_yaml: str,
    current_deck_yaml: str,
    syntax_error: str,
    attempt: int,
    max_attempts: int,
    user_message: dict[str, object] | None = None,
) -> str:
    """Request a minimal YAML correction pass when the current deck fails on syntax before semantics."""

    logger.warning(
        "PPTX syntax repair requested for attempt %s/%s: %s",
        attempt - 1,
        max_attempts,
        syntax_error,
    )
    return _run_llm_phase(
        project=project,
        llm_id=llm_id,
        phase="syntax_repair",
        messages=[
            ("system", prompts.build_pptx_system_prompt()),
            _build_user_message(
                text=prompts.build_pptx_syntax_repair_prompt(
                    user_prompt=user_prompt,
                    deck_plan_yaml=deck_plan_yaml,
                    current_deck_yaml=current_deck_yaml,
                    syntax_error=syntax_error,
                    attempt=attempt,
                    max_attempts=max_attempts,
                ),
                source_message=user_message,
            ),
        ],
    )


def repair_revision_full_deck_yaml(
    *,
    project,
    llm_id: str,
    source_deck_yaml: str,
    revision_instructions: str,
    current_deck_yaml: str,
    validation_error: str,
    quality_feedback: str | None,
    attempt: int,
    max_attempts: int,
    user_message: dict[str, object] | None = None,
) -> str:
    """Request a full revised-deck YAML rewrite when a revision fails validation or quality checks."""

    logger.warning(
        "PPTX revision full deck repair requested for attempt %s/%s: %s",
        attempt - 1,
        max_attempts,
        validation_error,
    )
    return _run_llm_phase(
        project=project,
        llm_id=llm_id,
        phase="revision_repair",
        messages=[
            ("system", prompts.build_pptx_system_prompt()),
            _build_user_message(
                text=prompts.build_pptx_revision_repair_prompt(
                    source_deck_yaml=source_deck_yaml,
                    revision_instructions=revision_instructions,
                    current_deck_yaml=current_deck_yaml,
                    validation_error=validation_error,
                    quality_feedback=quality_feedback,
                    attempt=attempt,
                    max_attempts=max_attempts,
                ),
                source_message=user_message,
            ),
        ],
    )


def repair_revision_syntax_only(
    *,
    project,
    llm_id: str,
    source_deck_yaml: str,
    revision_instructions: str,
    current_deck_yaml: str,
    syntax_error: str,
    attempt: int,
    max_attempts: int,
    user_message: dict[str, object] | None = None,
) -> str:
    """Request a minimal YAML syntax correction for a revised deck."""

    logger.warning(
        "PPTX revision syntax repair requested for attempt %s/%s: %s",
        attempt - 1,
        max_attempts,
        syntax_error,
    )
    return _run_llm_phase(
        project=project,
        llm_id=llm_id,
        phase="revision_syntax_repair",
        messages=[
            ("system", prompts.build_pptx_system_prompt()),
            _build_user_message(
                text=prompts.build_pptx_revision_syntax_repair_prompt(
                    source_deck_yaml=source_deck_yaml,
                    revision_instructions=revision_instructions,
                    current_deck_yaml=current_deck_yaml,
                    syntax_error=syntax_error,
                    attempt=attempt,
                    max_attempts=max_attempts,
                ),
                source_message=user_message,
            ),
        ],
    )


def repair_flagged_slides(
    *,
    project,
    llm_id: str,
    user_prompt: str,
    deck_plan_yaml: str,
    spec,
    issues: list[DeckQualityIssue],
    attempt: int,
    max_attempts: int,
    user_message: dict[str, object] | None = None,
):
    """Repair only the subset of slides referenced by localized quality issues and reapply them."""

    slide_indices = targeted_repair_slide_indices(issues)
    quality_feedback = format_quality_gate_findings([issue for issue in issues if issue.slide_index in slide_indices])
    flagged_slides_yaml = planning.format_targeted_slides_for_prompt(spec, slide_indices)

    logger.warning(
        "PPTX targeted repair requested for attempt %s/%s on slides=%s\nfindings=\n%s",
        attempt - 1,
        max_attempts,
        slide_indices,
        quality_feedback,
    )

    raw_patch = _run_llm_phase(
        project=project,
        llm_id=llm_id,
        phase="repair_targeted",
        messages=[
            ("system", prompts.build_pptx_targeted_repair_system_prompt()),
            _build_user_message(
                text=prompts.build_pptx_targeted_repair_prompt(
                    user_prompt=user_prompt,
                    deck_plan_yaml=deck_plan_yaml,
                    flagged_slides_yaml=flagged_slides_yaml,
                    quality_feedback=quality_feedback,
                    attempt=attempt,
                    max_attempts=max_attempts,
                ),
                source_message=user_message,
            ),
        ],
    )

    patch_set = planning.parse_slide_patch_set(raw_patch)
    return planning.apply_slide_patches(spec, patch_set)


def can_use_targeted_repair(issues: list[DeckQualityIssue]) -> bool:
    """Return whether the current quality findings are localized enough for targeted slide repair."""

    slide_indices = targeted_repair_slide_indices(issues)
    return (
        bool(slide_indices)
        and len(slide_indices) <= MAX_TARGETED_REPAIR_SLIDES
        and all(issue.slide_index is not None for issue in issues)
    )


def targeted_repair_slide_indices(issues: list[DeckQualityIssue]) -> list[int]:
    """Return stable unique slide indices mentioned by the current quality issues."""

    return sorted({issue.slide_index for issue in issues if issue.slide_index is not None})


def _format_messages_for_log(messages: list[dict[str, object] | tuple[str, str]]) -> str:
    serializable_messages: list[dict[str, object]] = []
    for message in messages:
        normalized = _normalize_llm_message(message)
        parts = normalized.get("parts")
        if isinstance(parts, list):
            normalized["parts"] = [
                {
                    **part,
                    **({"inlineImage": "<redacted>"} if part.get("type") == "IMAGE_INLINE" else {}),
                }
                for part in parts
                if isinstance(part, dict)
            ]
        serializable_messages.append(normalized)
    return json.dumps(serializable_messages, ensure_ascii=False, indent=2)


def _format_response_payload_for_log(raw_payload) -> str:
    if raw_payload is None:
        return "null"
    if isinstance(raw_payload, (dict, list)):
        return json.dumps(raw_payload, ensure_ascii=False, indent=2, sort_keys=True)
    return repr(raw_payload)


def _format_response_text_for_log(text: str | None) -> str:
    if text is None:
        return "null"

    stripped = text.strip()
    if not stripped:
        return '""'

    try:
        parsed = _select_yaml_candidate(stripped).payload
    except Exception:
        return stripped

    if isinstance(parsed, (dict, list)):
        return yaml.safe_dump(
            parsed,
            allow_unicode=True,
            sort_keys=False,
            default_flow_style=False,
        ).strip()

    return stripped


def _run_llm_phase(
    *,
    project,
    llm_id: str,
    phase: str,
    messages: list[dict[str, object] | tuple[str, str]],
) -> str:
    """Execute one named LLM phase with structured logging and normalize the response down to text."""

    normalized_messages = [_normalize_llm_message(message) for message in messages]
    logger.info(f"PPTX_GEN {phase}_request llm_id={llm_id} messages=\n{_format_messages_for_log(normalized_messages)}")
    response = internal_utils.execute_llm_completion(project, llm_id, normalized_messages)
    logger.info(
        f"PPTX_GEN {phase}_response "
        f"success={getattr(response, 'success', None)} "
        f"raw=\n{_format_response_payload_for_log(getattr(response, '_raw', None))}\n"
        f"text=\n{_format_response_text_for_log(getattr(response, 'text', None))}"
    )
    return internal_utils.extract_llm_text(response)


def _build_user_message(*, text: str, source_message: dict[str, object] | None) -> dict[str, object]:
    if not source_message:
        return {"role": "user", "content": text}
    parts = source_message.get("parts")
    if not isinstance(parts, list):
        return {"role": "user", "content": text}
    source_text = ""
    first_part = parts[0] if parts else None
    if isinstance(first_part, dict) and first_part.get("type") == "TEXT" and isinstance(first_part.get("text"), str):
        source_text = first_part["text"].strip()
    combined_text = text if not source_text else f"{text}\n\n{source_text}"
    return {
        "role": "user",
        "parts": [{"type": "TEXT", "text": combined_text}, *parts[1:]],
    }


def _normalize_llm_message(message: dict[str, object] | tuple[str, str]) -> dict[str, object]:
    if isinstance(message, dict):
        return dict(message)
    if isinstance(message, tuple) and len(message) == 2:
        role, content = message
        return {"role": role, "content": content}
    raise TypeError(f"Unsupported LLM message shape: {type(message)!r}")
