"""Slide Mapping Engine: map classified content blocks to master deck layouts."""
from typing import Dict, List, Optional

from pydantic import BaseModel

from models.content_models import ClassifiedContent, ContentBlock, ContentBlockType
from models.llm_message import LLMSystemMessage, LLMUserMessage
from services.llm_client import LLMClient
from utils.llm_provider import get_model


class SlideMapping(BaseModel):
    content_block_indices: List[int]  # which content blocks go on this slide
    layout_id: str
    layout_name: str
    slide_type: str
    content_summary: str
    attachment_ids: List[str] = []


# Map content block types to preferred layout types (as stored in MasterDeckModel.layouts[].layout_type)
_BLOCK_TO_LAYOUT_TYPE: Dict[ContentBlockType, List[str]] = {
    ContentBlockType.metric: ["metrics", "kpi", "data", "chart", "content"],
    ContentBlockType.quote: ["quote", "testimonial", "content"],
    ContentBlockType.table: ["table", "chart", "data", "content"],
    ContentBlockType.timeline: ["timeline", "process", "content"],
    ContentBlockType.comparison: ["comparison", "two_column", "content"],
    ContentBlockType.list_items: ["content", "bullet", "list"],
    ContentBlockType.narrative: ["content", "text", "description"],
    ContentBlockType.image_reference: ["picture", "image", "content"],
    ContentBlockType.call_to_action: ["content", "title_slide"],
}


class SlideMappingEngine:

    async def map(
        self,
        classified_content: ClassifiedContent,
        layouts: List[dict],
        n_slides: int,
        instructions: Optional[str] = None,
    ) -> List[SlideMapping]:
        """Map classified content blocks to master deck layouts.

        Args:
            classified_content: Output from ContentIntelligenceService.classify()
            layouts: MasterDeckModel.layouts list — each dict has layout_name, layout_type, index, etc.
            n_slides: Target number of slides
            instructions: Optional user instructions

        Returns:
            Ordered list of SlideMapping
        """
        if not layouts:
            return self._fallback_mapping(classified_content, n_slides)

        # Build layout index by type for fast lookup
        layout_by_type: Dict[str, List[dict]] = {}
        for layout in layouts:
            lt = (layout.get("layout_type") or "custom").lower()
            layout_by_type.setdefault(lt, []).append(layout)

        blocks = classified_content.blocks

        # 1. Always start with a title slide
        mappings: List[SlideMapping] = []
        title_layout = self._find_layout(layout_by_type, ["title_slide", "title"], layouts)
        mappings.append(
            SlideMapping(
                content_block_indices=[],
                layout_id=str(title_layout.get("index", 0)),
                layout_name=title_layout.get("layout_name", "Title"),
                slide_type="title_slide",
                content_summary=classified_content.title or "Presentation Title",
            )
        )

        # 2. If many blocks, add agenda/section header
        if len(blocks) > 5:
            section_layout = self._find_layout(
                layout_by_type, ["section_header", "section", "content"], layouts
            )
            sections = list(
                {b.source_section for b in blocks if b.source_section}
            )
            mappings.append(
                SlideMapping(
                    content_block_indices=[],
                    layout_id=str(section_layout.get("index", 0)),
                    layout_name=section_layout.get("layout_name", "Agenda"),
                    slide_type="section_header",
                    content_summary="Agenda: " + ", ".join(sections[:6]),
                )
            )

        # 3. Map each content block to a layout
        remaining_slots = n_slides - len(mappings)
        block_mappings = self._assign_blocks_to_slides(
            blocks, layout_by_type, layouts, remaining_slots
        )
        mappings.extend(block_mappings)

        # 4. If we have more slides than content, add transitional slides
        while len(mappings) < n_slides:
            content_layout = self._find_layout(
                layout_by_type, ["content", "blank"], layouts
            )
            mappings.append(
                SlideMapping(
                    content_block_indices=[],
                    layout_id=str(content_layout.get("index", 0)),
                    layout_name=content_layout.get("layout_name", "Content"),
                    slide_type="content",
                    content_summary="Additional content",
                )
            )

        # 5. Trim if over target
        if len(mappings) > n_slides:
            # Keep title + agenda, trim lowest-priority from the rest
            fixed = mappings[:2] if len(mappings) > 2 else mappings[:1]
            rest = mappings[len(fixed):]
            rest = rest[: n_slides - len(fixed)]
            mappings = fixed + rest

        # 6. Optional LLM refinement for ambiguous mappings
        if instructions:
            mappings = await self._llm_refine(
                mappings, classified_content, layouts, instructions, n_slides
            )

        return mappings

    def _find_layout(
        self,
        layout_by_type: Dict[str, List[dict]],
        preferred_types: List[str],
        all_layouts: List[dict],
    ) -> dict:
        """Find best matching layout by type preference, fallback to first layout."""
        for lt in preferred_types:
            if lt in layout_by_type and layout_by_type[lt]:
                return layout_by_type[lt][0]
        return all_layouts[0] if all_layouts else {"index": 0, "layout_name": "Default", "layout_type": "content"}

    def _assign_blocks_to_slides(
        self,
        blocks: List[ContentBlock],
        layout_by_type: Dict[str, List[dict]],
        all_layouts: List[dict],
        max_slides: int,
    ) -> List[SlideMapping]:
        """Assign content blocks to slides, respecting max_slides constraint."""
        if max_slides <= 0:
            return []

        mappings: List[SlideMapping] = []

        if len(blocks) <= max_slides:
            # One block per slide
            for i, block in enumerate(blocks):
                preferred = _BLOCK_TO_LAYOUT_TYPE.get(block.type, ["content"])
                layout = self._find_layout(layout_by_type, preferred, all_layouts)
                mappings.append(
                    SlideMapping(
                        content_block_indices=[i],
                        layout_id=str(layout.get("index", 0)),
                        layout_name=layout.get("layout_name", "Content"),
                        slide_type=block.type.value,
                        content_summary=block.raw_text[:120],
                    )
                )
        else:
            # More blocks than slides — merge low-priority blocks
            # Sort by priority descending, take top max_slides groups
            sorted_blocks = sorted(
                enumerate(blocks), key=lambda x: -x[1].priority
            )

            # High-priority blocks get their own slide
            high_priority = sorted_blocks[:max_slides]
            overflow = sorted_blocks[max_slides:]

            # Group overflow with nearest high-priority block
            for idx, block in high_priority:
                preferred = _BLOCK_TO_LAYOUT_TYPE.get(block.type, ["content"])
                layout = self._find_layout(layout_by_type, preferred, all_layouts)
                mappings.append(
                    SlideMapping(
                        content_block_indices=[idx],
                        layout_id=str(layout.get("index", 0)),
                        layout_name=layout.get("layout_name", "Content"),
                        slide_type=block.type.value,
                        content_summary=block.raw_text[:120],
                    )
                )

            # Distribute overflow blocks across existing slides
            for i, (idx, block) in enumerate(overflow):
                target = i % len(mappings)
                mappings[target].content_block_indices.append(idx)

            # Re-sort mappings by original block order
            mappings.sort(
                key=lambda m: min(m.content_block_indices) if m.content_block_indices else 999
            )

        return mappings

    async def _llm_refine(
        self,
        mappings: List[SlideMapping],
        content: ClassifiedContent,
        layouts: List[dict],
        instructions: str,
        n_slides: int,
    ) -> List[SlideMapping]:
        """Use LLM to refine layout assignments based on user instructions."""
        client = LLMClient()
        model = get_model()

        layout_info = "\n".join(
            f"- Index {l.get('index')}: {l.get('layout_name')} (type: {l.get('layout_type')})"
            for l in layouts
        )

        current_mapping = "\n".join(
            f"Slide {i + 1}: [{m.slide_type}] {m.content_summary[:80]} → layout '{m.layout_name}'"
            for i, m in enumerate(mappings)
        )

        messages = [
            LLMSystemMessage(
                content="You refine slide-to-layout mappings for presentations. "
                "Given the current mapping and user instructions, suggest layout changes. "
                "Return a JSON with 'changes' array of {slide_index: int, new_layout_index: int} objects. "
                "Only include slides that need changing. Return empty array if no changes needed."
            ),
            LLMUserMessage(
                content=f"Available layouts:\n{layout_info}\n\n"
                f"Current mapping:\n{current_mapping}\n\n"
                f"User instructions: {instructions}"
            ),
        ]

        schema = {
            "type": "object",
            "properties": {
                "changes": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "slide_index": {"type": "integer"},
                            "new_layout_index": {"type": "integer"},
                        },
                        "required": ["slide_index", "new_layout_index"],
                    },
                }
            },
            "required": ["changes"],
        }

        try:
            result = await client.generate_structured(
                model=model, messages=messages, response_format=schema
            )
            for change in result.get("changes", []):
                si = change.get("slide_index", -1)
                li = change.get("new_layout_index", -1)
                if 0 <= si < len(mappings) and 0 <= li < len(layouts):
                    mappings[si].layout_id = str(li)
                    mappings[si].layout_name = layouts[li].get("layout_name", "Content")
        except Exception:
            pass  # Keep original mapping on LLM failure

        return mappings

    def _fallback_mapping(
        self, content: ClassifiedContent, n_slides: int
    ) -> List[SlideMapping]:
        """Fallback when no master deck layouts are available."""
        mappings = [
            SlideMapping(
                content_block_indices=[],
                layout_id="0",
                layout_name="Title",
                slide_type="title_slide",
                content_summary=content.title or "Presentation",
            )
        ]

        for i, block in enumerate(content.blocks[: n_slides - 1]):
            mappings.append(
                SlideMapping(
                    content_block_indices=[i],
                    layout_id="0",
                    layout_name="Content",
                    slide_type=block.type.value,
                    content_summary=block.raw_text[:120],
                )
            )

        return mappings
