from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass

from pydantic import ValidationError
from yaml import YAMLError

from backend.models.events import EventKind
from backend.services.generated_document.generated_document_metadata import build_initial_document_metadata
from backend.services.pptx_artifact_generation.image_assets import (
    build_generation_image_assets_metadata,
    build_image_assets_map,
)
from backend.services.pptx_artifact_generation.models import GeneratedPptxArtifacts, PptxImageAsset
from backend.services.pptx_artifact_generation.source_artifacts import build_pptx_image_assets_user_message

from . import generation_steps, internal_utils, planning, previews, quality_gate, rendering
from .parsing import PptxDslContentError, PptxDslSyntaxError, parse_pptx_spec


class ImageAssetReferenceError(ValueError):
    """Raised when generated deck YAML references source images that are unavailable in the current run."""


_RAW_DECK_RETRYABLE_ERRORS = (
    planning.DeckConstraintFailure,
    ValidationError,
    YAMLError,
    ImageAssetReferenceError,
    PptxDslContentError,
    PptxDslSyntaxError,
)
_TARGETED_PATCH_RETRYABLE_ERRORS = (
    planning.DeckConstraintFailure,
    planning.SlidePatchContentError,
    ValidationError,
    YAMLError,
    PptxDslContentError,
    PptxDslSyntaxError,
)


@dataclass
class _PptxGenerationState:
    project: object
    llm_id: str
    user_prompt: str
    image_assets: list[PptxImageAsset]
    user_message: dict[str, object] | None
    deck_plan: planning.DeckPlan
    deck_plan_yaml: str
    raw_yaml: str
    emit_progress_event: Callable[[EventKind], None] | None
    repair_progress_emitted: bool = False


@dataclass
class _PptxRevisionState:
    project: object
    llm_id: str
    source_deck_yaml: str
    image_assets: list[PptxImageAsset]
    user_message: dict[str, object] | None
    revision_instructions: str
    raw_yaml: str
    emit_progress_event: Callable[[EventKind], None] | None
    repair_progress_emitted: bool = False


def generate_pptx_artifacts(
    project,
    llm_id: str,
    user_prompt: str,
    user_message: dict[str, object] | None = None,
    image_assets: list[PptxImageAsset] | None = None,
    requested_filename: str | None = None,
    emit_progress_event: Callable[[EventKind], None] | None = None,
    conversation_history: list[dict] | None = None,
) -> GeneratedPptxArtifacts:
    """Generate a deck plan, produce deck YAML, repair it if needed, render the PPTX, and package the result."""

    state = _initialize_generation_state(
        project=project,
        llm_id=llm_id,
        user_prompt=user_prompt,
        user_message=user_message,
        image_assets=image_assets or [],
        emit_progress_event=emit_progress_event,
        conversation_history=conversation_history,
    )
    spec = _generate_validated_deck_spec(state)
    if state.emit_progress_event is not None:
        state.emit_progress_event(EventKind.PPTX_RENDERING_FILE)
    return _build_generated_pptx_artifacts(
        spec=spec,
        raw_yaml=state.raw_yaml,
        image_assets=state.image_assets,
        user_prompt=user_prompt,
        requested_filename=requested_filename,
        document_metadata=build_initial_document_metadata(),
    )


def revise_pptx_artifacts(
    project,
    llm_id: str,
    source_deck_yaml: str,
    source_image_assets: list[PptxImageAsset] | None,
    revision_instructions: str,
    user_message: dict[str, object] | None = None,
    requested_filename: str | None = None,
    document_metadata: dict | None = None,
    emit_progress_event: Callable[[EventKind], None] | None = None,
    conversation_history: list[dict] | None = None,
) -> GeneratedPptxArtifacts:
    """Revise an existing Agent Hub deck YAML, render the new PPTX, and package it as a new artifact."""

    source_spec = parse_pptx_spec(source_deck_yaml)
    normalized_source_deck_yaml = planning.format_deck_spec_for_prompt(source_spec)
    image_assets = source_image_assets or []
    revision_user_message = user_message
    if revision_user_message is None and image_assets:
        revision_user_message = build_pptx_image_assets_user_message(revision_instructions, image_assets)
    if emit_progress_event is not None:
        emit_progress_event(EventKind.PPTX_GENERATING_DECK)
    raw_yaml = generation_steps.revise_deck_yaml(
        project=project,
        llm_id=llm_id,
        source_deck_yaml=normalized_source_deck_yaml,
        revision_instructions=revision_instructions,
        user_message=revision_user_message,
        conversation_history=conversation_history,
    )
    state = _PptxRevisionState(
        project=project,
        llm_id=llm_id,
        source_deck_yaml=normalized_source_deck_yaml,
        image_assets=image_assets,
        user_message=revision_user_message,
        revision_instructions=revision_instructions,
        raw_yaml=raw_yaml,
        emit_progress_event=emit_progress_event,
    )
    spec = _generate_validated_revised_deck_spec(state)
    if state.emit_progress_event is not None:
        state.emit_progress_event(EventKind.PPTX_RENDERING_FILE)
    return _build_generated_pptx_artifacts(
        spec=spec,
        raw_yaml=state.raw_yaml,
        image_assets=state.image_assets,
        user_prompt=revision_instructions,
        requested_filename=requested_filename,
        document_metadata=document_metadata,
    )


def _initialize_generation_state(
    *,
    project,
    llm_id: str,
    user_prompt: str,
    user_message: dict[str, object] | None,
    image_assets: list[PptxImageAsset],
    emit_progress_event: Callable[[EventKind], None] | None,
    conversation_history: list[dict] | None = None,
) -> _PptxGenerationState:
    """Generate the approved deck plan and first raw deck YAML, then store both in the orchestration state."""

    if emit_progress_event is not None:
        emit_progress_event(EventKind.PPTX_GENERATING_OUTLINE)
    deck_plan = generation_steps.generate_deck_plan(
        project, llm_id, user_prompt, user_message=user_message, conversation_history=conversation_history
    )
    deck_plan_yaml = planning.format_deck_plan_for_prompt(deck_plan)
    if emit_progress_event is not None:
        emit_progress_event(EventKind.PPTX_GENERATING_DECK)
    raw_yaml = generation_steps.generate_deck_yaml(
        project,
        llm_id,
        user_prompt,
        deck_plan_yaml,
        user_message=user_message,
        conversation_history=conversation_history,
    )
    return _PptxGenerationState(
        project=project,
        llm_id=llm_id,
        user_prompt=user_prompt,
        image_assets=image_assets,
        user_message=user_message,
        deck_plan=deck_plan,
        deck_plan_yaml=deck_plan_yaml,
        raw_yaml=raw_yaml,
        emit_progress_event=emit_progress_event,
    )


def _generate_validated_deck_spec(state: _PptxGenerationState):
    """Iterate over parse/validate/quality passes, mutating `state.raw_yaml` until one deck spec is acceptable."""

    spec = None
    for attempt in range(1, internal_utils.MAX_PPTX_GENERATION_ATTEMPTS + 1):
        try:
            spec = _parse_and_validate_deck_spec(state)
        except Exception as exc:
            if not isinstance(exc, _RAW_DECK_RETRYABLE_ERRORS):
                raise
            _handle_retryable_generation_failure(state, exc, attempt)
            continue

        try:
            _raise_for_quality_issues(spec)
        except quality_gate.DeckQualityFailure as exc:
            _handle_quality_failure(state, spec, exc, attempt)
            continue

        return spec

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


def _generate_validated_revised_deck_spec(state: _PptxRevisionState):
    """Iterate over parse/validate/quality passes for a revised deck YAML."""

    for attempt in range(1, internal_utils.MAX_PPTX_GENERATION_ATTEMPTS + 1):
        try:
            spec = parse_pptx_spec(state.raw_yaml)
            _validate_image_asset_references(spec, state.image_assets)
        except Exception as exc:
            if not isinstance(exc, _RAW_DECK_RETRYABLE_ERRORS):
                raise
            _handle_retryable_revision_failure(state, exc, attempt)
            continue

        try:
            _raise_for_quality_issues(spec)
        except quality_gate.DeckQualityFailure as exc:
            _handle_revision_quality_failure(state, exc, attempt)
            continue

        return spec

    raise ValueError("PPTX revision did not produce a valid deck specification")


def _parse_and_validate_deck_spec(state: _PptxGenerationState):
    """Parse `state.raw_yaml` into `DeckSpec` and verify that slide count and slide types still match the plan."""

    spec = parse_pptx_spec(state.raw_yaml)
    planning.validate_deck_against_plan(spec, state.deck_plan, state.user_prompt)
    _validate_image_asset_references(spec, state.image_assets)
    return spec


def _raise_for_quality_issues(spec) -> None:
    """Convert quality-gate findings into a single exception so repairs stay in the same orchestration loop."""

    quality_issues = quality_gate.collect_deck_quality_issues(spec)
    if quality_issues:
        raise quality_gate.DeckQualityFailure(quality_issues)


def _handle_quality_failure(
    state: _PptxGenerationState,
    spec,
    exc: quality_gate.DeckQualityFailure,
    attempt: int,
) -> None:
    """Decide whether a quality failure can be fixed with slide-local patching or needs a full deck rewrite."""

    if attempt == internal_utils.MAX_PPTX_GENERATION_ATTEMPTS:
        raise exc

    if generation_steps.can_use_targeted_repair(exc.issues):
        _repair_targeted_quality_failure(state, spec, exc, attempt)
        return

    _repair_full_deck(
        state,
        validation_error=str(exc),
        quality_feedback=quality_gate.format_quality_gate_findings(exc.issues),
        attempt=attempt + 1,
    )


def _repair_targeted_quality_failure(
    state: _PptxGenerationState,
    spec,
    exc: quality_gate.DeckQualityFailure,
    attempt: int,
) -> None:
    """Patch only the flagged slides, then replace `state.raw_yaml` with the repaired spec serialized back to YAML."""

    _emit_repair_progress_once(state)
    quality_feedback = quality_gate.format_quality_gate_findings(exc.issues)
    try:
        repaired_spec = generation_steps.repair_flagged_slides(
            project=state.project,
            llm_id=state.llm_id,
            user_prompt=state.user_prompt,
            deck_plan_yaml=state.deck_plan_yaml,
            spec=spec,
            issues=exc.issues,
            attempt=attempt + 1,
            max_attempts=internal_utils.MAX_PPTX_GENERATION_ATTEMPTS,
            user_message=state.user_message,
        )
    except Exception as patch_exc:
        if not isinstance(patch_exc, _TARGETED_PATCH_RETRYABLE_ERRORS):
            raise
        _repair_full_deck(
            state,
            validation_error=(
                f"{exc}\nTargeted repair patch failed: {internal_utils.format_retryable_generation_error(patch_exc)}"
            ),
            quality_feedback=quality_feedback,
            attempt=attempt + 1,
        )
        return

    state.raw_yaml = planning.format_deck_spec_for_prompt(repaired_spec)


def _handle_retryable_generation_failure(
    state: _PptxGenerationState,
    exc: Exception,
    attempt: int,
) -> None:
    """Send retryable parse/validation failures to syntax-only repair when possible, otherwise request a full rewrite."""

    if attempt == internal_utils.MAX_PPTX_GENERATION_ATTEMPTS:
        raise exc

    if isinstance(exc, PptxDslSyntaxError):
        _repair_syntax_failure(state, exc, attempt)
        return

    _repair_full_deck(
        state,
        validation_error=internal_utils.format_retryable_generation_error(exc),
        quality_feedback=None,
        attempt=attempt + 1,
    )


def _handle_retryable_revision_failure(state: _PptxRevisionState, exc: Exception, attempt: int) -> None:
    """Repair retryable parse/validation failures from the revision flow."""

    if attempt == internal_utils.MAX_PPTX_GENERATION_ATTEMPTS:
        raise exc

    if isinstance(exc, PptxDslSyntaxError):
        _repair_revision_syntax_failure(state, exc, attempt)
        return

    _repair_revision_full_deck(
        state,
        validation_error=internal_utils.format_retryable_generation_error(exc),
        quality_feedback=None,
        attempt=attempt + 1,
    )


def _handle_revision_quality_failure(
    state: _PptxRevisionState,
    exc: quality_gate.DeckQualityFailure,
    attempt: int,
) -> None:
    """Ask for a full revised deck repair when a revision fails content-quality checks."""

    if attempt == internal_utils.MAX_PPTX_GENERATION_ATTEMPTS:
        raise exc

    _repair_revision_full_deck(
        state,
        validation_error=str(exc),
        quality_feedback=quality_gate.format_quality_gate_findings(exc.issues),
        attempt=attempt + 1,
    )


def _repair_syntax_failure(state: _PptxGenerationState, exc: PptxDslSyntaxError, attempt: int) -> None:
    """Ask the LLM to fix only the YAML syntax while keeping the current deck content and plan context."""

    _emit_repair_progress_once(state)
    state.raw_yaml = generation_steps.repair_syntax_only(
        project=state.project,
        llm_id=state.llm_id,
        user_prompt=state.user_prompt,
        deck_plan_yaml=state.deck_plan_yaml,
        current_deck_yaml=state.raw_yaml,
        syntax_error=internal_utils.format_retryable_generation_error(exc),
        attempt=attempt + 1,
        max_attempts=internal_utils.MAX_PPTX_GENERATION_ATTEMPTS,
        user_message=state.user_message,
    )


def _repair_revision_syntax_failure(state: _PptxRevisionState, exc: PptxDslSyntaxError, attempt: int) -> None:
    """Ask the LLM to fix only the revised deck YAML syntax."""

    _emit_revision_repair_progress_once(state)
    state.raw_yaml = generation_steps.repair_revision_syntax_only(
        project=state.project,
        llm_id=state.llm_id,
        source_deck_yaml=state.source_deck_yaml,
        revision_instructions=state.revision_instructions,
        current_deck_yaml=state.raw_yaml,
        syntax_error=internal_utils.format_retryable_generation_error(exc),
        attempt=attempt + 1,
        max_attempts=internal_utils.MAX_PPTX_GENERATION_ATTEMPTS,
        user_message=state.user_message,
    )


def _repair_full_deck(
    state: _PptxGenerationState,
    *,
    validation_error: str,
    quality_feedback: str | None,
    attempt: int,
) -> None:
    """Ask the LLM for a new full-deck YAML draft seeded with the latest validation error and optional quality notes."""

    _emit_repair_progress_once(state)
    state.raw_yaml = generation_steps.repair_full_deck_yaml(
        project=state.project,
        llm_id=state.llm_id,
        user_prompt=state.user_prompt,
        deck_plan_yaml=state.deck_plan_yaml,
        current_deck_yaml=state.raw_yaml,
        validation_error=validation_error,
        quality_feedback=quality_feedback,
        attempt=attempt,
        max_attempts=internal_utils.MAX_PPTX_GENERATION_ATTEMPTS,
        user_message=state.user_message,
    )


def _repair_revision_full_deck(
    state: _PptxRevisionState,
    *,
    validation_error: str,
    quality_feedback: str | None,
    attempt: int,
) -> None:
    """Ask the LLM for a repaired full revised deck YAML."""

    _emit_revision_repair_progress_once(state)
    state.raw_yaml = generation_steps.repair_revision_full_deck_yaml(
        project=state.project,
        llm_id=state.llm_id,
        source_deck_yaml=state.source_deck_yaml,
        revision_instructions=state.revision_instructions,
        current_deck_yaml=state.raw_yaml,
        validation_error=validation_error,
        quality_feedback=quality_feedback,
        attempt=attempt,
        max_attempts=internal_utils.MAX_PPTX_GENERATION_ATTEMPTS,
        user_message=state.user_message,
    )


def _emit_repair_progress_once(state: _PptxGenerationState) -> None:
    """Emit the repair milestone once even if several repair attempts happen in the same generation run."""

    if state.repair_progress_emitted:
        return
    if state.emit_progress_event is not None:
        state.emit_progress_event(EventKind.PPTX_REPAIRING_DECK)
    state.repair_progress_emitted = True


def _emit_revision_repair_progress_once(state: _PptxRevisionState) -> None:
    """Emit the shared PPTX repair milestone once for a revision run."""

    if state.repair_progress_emitted:
        return
    if state.emit_progress_event is not None:
        state.emit_progress_event(EventKind.PPTX_REPAIRING_DECK)
    state.repair_progress_emitted = True


def _build_generated_pptx_artifacts(
    *,
    spec,
    raw_yaml: str,
    image_assets: list[PptxImageAsset],
    user_prompt: str,
    requested_filename: str | None,
    document_metadata: dict | None = None,
) -> GeneratedPptxArtifacts:
    """Render the validated spec to PPTX bytes, derive the output filename, and build the inline artifact payload."""

    retained_image_assets = _retain_used_image_assets(spec, image_assets)
    pptx_bytes = rendering.render_deck_to_bytes(spec, build_image_assets_map(retained_image_assets))
    deck_title = previews.resolve_deck_title(spec) or "Presentation"
    filename = internal_utils.build_pptx_filename(
        user_prompt=user_prompt,
        requested_filename=requested_filename,
        deck_title=deck_title,
    )
    artifacts = internal_utils.build_inline_pptx_artifacts(
        title=deck_title,
        filename=filename,
        pptx_bytes=pptx_bytes,
        mime_type=internal_utils.PPTX_MIME_TYPE,
        deck_yaml=raw_yaml,
        image_assets=build_generation_image_assets_metadata(retained_image_assets),
        document_metadata=document_metadata,
        markdown_snippet=previews.build_deck_markdown_preview(spec),
        text_snippet=previews.build_deck_text_preview(spec),
    )
    return GeneratedPptxArtifacts(filename=filename, artifacts=artifacts, raw_yaml=raw_yaml)


def _validate_image_asset_references(spec, image_assets: list[PptxImageAsset]) -> None:
    """Fail early when deck YAML references image assets that the current render/revision context does not provide.

    This keeps asset-resolution errors in the service validation loop, where they can trigger an explicit repair or
    bubble up clearly, instead of failing later inside the low-level PowerPoint renderer.
    """
    available_asset_ids = {asset.asset_id for asset in image_assets}
    for slide_index, slide in enumerate(spec.slides, start=1):
        if slide.type == "image":
            if not available_asset_ids:
                raise ImageAssetReferenceError(
                    f"Slide {slide_index} uses an image slide, but no source image assets are available in this "
                    "request. Convert it to a non-image slide type."
                )
            if slide.image is None or slide.image.asset_id not in available_asset_ids:
                raise ImageAssetReferenceError(f"Slide {slide_index} references an unknown image asset")
        elif slide.type == "image_text":
            image_text = slide.image_text
            if image_text is None:
                continue
            if not available_asset_ids:
                raise ImageAssetReferenceError(
                    f"Slide {slide_index} uses an image_text slide, but no source image assets are available in this "
                    "request. Convert it to a non-image slide type."
                )
            if not image_text.asset_id:
                raise ImageAssetReferenceError(f"Slide {slide_index} must reference an image asset")
            if image_text.asset_id and image_text.asset_id not in available_asset_ids:
                raise ImageAssetReferenceError(f"Slide {slide_index} references an unknown image asset")


def _retain_used_image_assets(spec, image_assets: list[PptxImageAsset]) -> list[PptxImageAsset]:
    """Drop persisted image bindings that are no longer referenced by the final deck spec.

    This keeps `agentHubGenerationMetadata.imageAssets` aligned with the actual deck content so a later revision
    only carries forward visuals that still matter.
    """
    used_asset_ids: set[str] = set()
    for slide in spec.slides:
        if slide.type == "image" and slide.image is not None:
            used_asset_ids.add(slide.image.asset_id)
        elif slide.type == "image_text" and slide.image_text and slide.image_text.asset_id:
            used_asset_ids.add(slide.image_text.asset_id)
    return [asset for asset in image_assets if asset.asset_id in used_asset_ids]
