"""DesignAgent — applies composition logic and layout improvements to slide definitions."""

from __future__ import annotations

import re
from typing import Optional

from pydantic import BaseModel, Field

from pptx_mcp.agents.base import BaseAgent
from pptx_mcp.models.brand import BrandConfig
from pptx_mcp.models.slides import (
    ColumnContent,
    IconItem,
    ProcessStep,
    PresentationDefinition,
    SlideDefinition,
    SlideType,
)
from pptx_mcp.brand.config import resolve_brand_config


class DesignerInput(BaseModel):
    """Input for the DesignAgent."""

    definition: PresentationDefinition
    brand_config_path: Optional[str] = None
    brand_preset: Optional[str] = None


class DesignerOutput(BaseModel):
    """Output from the DesignAgent."""

    definition: PresentationDefinition
    changes_made: list[str] = Field(default_factory=list)


class DesignAgent(BaseAgent[DesignerInput, DesignerOutput]):
    """Applies design heuristics and layout optimizations.

    This agent takes a PresentationDefinition (possibly with raw/unoptimized
    content) and applies design rules to improve layout, balance, and
    visual hierarchy. It does not change content semantics — only
    structural and layout properties.
    """

    @property
    def name(self) -> str:
        return "designer"

    @property
    def description(self) -> str:
        return "Applies composition logic and layout improvements to slide definitions."

    def run(self, input_data: DesignerInput) -> DesignerOutput:
        """Optimize the presentation layout.

        Args:
            input_data: Presentation definition and brand config.

        Returns:
            Optimized definition with a list of changes made.
        """
        brand = resolve_brand_config(
            path=input_data.brand_config_path,
            preset=input_data.brand_preset,
        )

        definition = input_data.definition.model_copy(deep=True)
        changes: list[str] = []

        for i, slide in enumerate(definition.slides):
            slide_changes = self._optimize_slide(slide, i, brand)
            changes.extend(slide_changes)

        deck_changes = self._optimize_deck(definition, brand)
        changes.extend(deck_changes)

        self.logger.info("Design optimization complete: %d changes", len(changes))

        return DesignerOutput(
            definition=definition,
            changes_made=changes,
        )

    def _optimize_slide(
        self,
        slide: SlideDefinition,
        index: int,
        brand: BrandConfig,
    ) -> list[str]:
        """Optimize a single slide."""
        changes: list[str] = []

        # Split overloaded bullet slides
        max_bullets = brand.layout_rules.max_bullets_per_slide
        if len(slide.bullets) > max_bullets + 2 and slide.slide_type == SlideType.CONTENT:
            mid = len(slide.bullets) // 2
            slide.columns = [
                ColumnContent(bullets=slide.bullets[:mid]),
                ColumnContent(bullets=slide.bullets[mid:]),
            ]
            slide.bullets = []
            slide.slide_type = SlideType.TWO_COLUMN
            changes.append(
                f"Slide {index}: Split {mid * 2} bullets into two-column layout."
            )

        # Trim excessively long bullets
        max_words = brand.layout_rules.max_words_per_bullet
        for j, bullet in enumerate(slide.bullets):
            words = bullet.split()
            if len(words) > max_words * 2:
                slide.bullets[j] = " ".join(words[:max_words]) + "..."
                changes.append(
                    f"Slide {index}, bullet {j}: Truncated from {len(words)} to {max_words} words."
                )

        # Slide-level promotions (order matters — first match wins)
        # Run before the single-bullet fallback so promotions take priority.
        promoted = self._try_promote_to_big_number(slide, index)
        if promoted:
            changes.append(promoted)
        else:
            promoted = self._try_promote_to_icon_grid(slide, index)
            if promoted:
                changes.append(promoted)
            else:
                promoted = self._try_promote_to_step_cards(slide, index)
                if promoted:
                    changes.append(promoted)

        # Promote single-bullet content to body text (fallback if no promotion matched)
        if (
            slide.slide_type == SlideType.CONTENT
            and len(slide.bullets) == 1
            and not slide.body
        ):
            slide.body = slide.bullets[0]
            slide.bullets = []
            changes.append(f"Slide {index}: Promoted single bullet to body text.")

        # Balance columns
        if slide.columns and len(slide.columns) >= 2:
            changes.extend(self._balance_columns(slide, index))

        return changes

    def _balance_columns(
        self,
        slide: SlideDefinition,
        index: int,
    ) -> list[str]:
        """Attempt to balance column content."""
        changes: list[str] = []
        lengths = [len(col.bullets) for col in slide.columns]

        if not lengths or max(lengths) == 0:
            return changes

        ratio = min(lengths) / max(lengths) if max(lengths) > 0 else 1.0

        if ratio < 0.4 and len(slide.columns) == 2:
            # Redistribute bullets
            all_bullets = []
            for col in slide.columns:
                all_bullets.extend(col.bullets)

            if all_bullets:
                mid = len(all_bullets) // 2
                slide.columns[0].bullets = all_bullets[:mid]
                slide.columns[1].bullets = all_bullets[mid:]
                changes.append(f"Slide {index}: Rebalanced column content ({len(all_bullets)} bullets).")

        return changes

    # ------------------------------------------------------------------
    # Slide-level promotion methods
    # ------------------------------------------------------------------

    _NUMBER_PATTERN = re.compile(
        r"\d[\d,]*\.?\d*\s*[%$]"   # e.g. 47%, $1.2
        r"|\$\s*\d[\d,]*\.?\d*"    # e.g. $ 500
        r"|\d[\d,]*\.?\d*\s*[MBKmbk]\b"  # e.g. 1.2M, 500K
        r"|\b\d[\d,]*\.?\d*\b"     # standalone number
    )

    def _try_promote_to_big_number(
        self, slide: SlideDefinition, index: int
    ) -> Optional[str]:
        """Promote a single-bullet CONTENT slide with a number to BIG_NUMBER."""
        if slide.slide_type != SlideType.CONTENT:
            return None
        if len(slide.bullets) != 1:
            return None
        text = slide.bullets[0]
        if not self._NUMBER_PATTERN.search(text):
            return None

        slide.body = text
        slide.bullets = []
        slide.slide_type = SlideType.BIG_NUMBER
        return f"Slide {index}: Promoted to BIG_NUMBER (number detected in single bullet)."

    def _try_promote_to_icon_grid(
        self, slide: SlideDefinition, index: int
    ) -> Optional[str]:
        """Promote a short-bullet CONTENT slide to ICON_GRID."""
        if slide.slide_type != SlideType.CONTENT:
            return None
        if not (4 <= len(slide.bullets) <= 6):
            return None
        if any(len(b.split()) >= 6 for b in slide.bullets):
            return None

        slide.icons = [
            IconItem(icon="\u2605", title=b) for b in slide.bullets
        ]
        slide.bullets = []
        slide.slide_type = SlideType.ICON_GRID
        return f"Slide {index}: Promoted to ICON_GRID ({len(slide.icons)} short bullets)."

    _STEP_PREFIX = re.compile(r"^\d+[\.\)]\s*")

    def _try_promote_to_step_cards(
        self, slide: SlideDefinition, index: int
    ) -> Optional[str]:
        """Promote a numbered-list CONTENT slide to STEP_CARDS."""
        if slide.slide_type != SlideType.CONTENT:
            return None
        if not (3 <= len(slide.bullets) <= 5):
            return None
        if not all(self._STEP_PREFIX.match(b) for b in slide.bullets):
            return None

        slide.steps = [
            ProcessStep(title=self._STEP_PREFIX.sub("", b).strip())
            for b in slide.bullets
        ]
        slide.bullets = []
        slide.slide_type = SlideType.STEP_CARDS
        return f"Slide {index}: Promoted to STEP_CARDS ({len(slide.steps)} numbered steps)."

    # ------------------------------------------------------------------
    # Deck-level optimization methods
    # ------------------------------------------------------------------

    _VISUAL_TYPES: frozenset[SlideType] = frozenset({
        SlideType.CHART,
        SlideType.DASHBOARD,
        SlideType.BIG_NUMBER,
        SlideType.PROCESS,
        SlideType.TIMELINE,
        SlideType.STEP_CARDS,
        SlideType.ICON_GRID,
        SlideType.IMAGE_TEXT,
        SlideType.FUNNEL,
        SlideType.SWOT,
        SlideType.TEAM,
        SlideType.QUOTE,
    })

    _TEXT_HEAVY_TYPES: frozenset[SlideType] = frozenset({
        SlideType.CONTENT,
        SlideType.TWO_COLUMN,
        SlideType.PROS_CONS,
        SlideType.COMPARISON,
        SlideType.TABLE,
    })

    def _optimize_deck(
        self,
        definition: PresentationDefinition,
        brand: BrandConfig,
    ) -> list[str]:
        """Apply deck-level optimizations."""
        changes: list[str] = []
        changes.extend(self._enforce_visual_rhythm(definition))
        changes.extend(self._ensure_deck_structure(definition))
        return changes

    def _enforce_visual_rhythm(
        self, definition: PresentationDefinition
    ) -> list[str]:
        """Insert section dividers to break up long runs of text-heavy slides."""
        changes: list[str] = []
        consecutive = 0
        inserts: list[int] = []
        for i, slide in enumerate(definition.slides):
            if slide.slide_type in self._TEXT_HEAVY_TYPES:
                consecutive += 1
                if consecutive >= 4:
                    inserts.append(i)
                    consecutive = 0
            else:
                consecutive = 0

        for offset, pos in enumerate(inserts):
            insert_at = pos + offset
            section_slide = SlideDefinition(
                slide_type=SlideType.SECTION,
                title="[Section Break]",
                speaker_notes="Consider adding a section title here for pacing.",
            )
            definition.slides.insert(insert_at, section_slide)
            changes.append(
                f"Inserted section break before slide {insert_at} to improve visual rhythm."
            )
        return changes

    def _ensure_deck_structure(
        self, definition: PresentationDefinition
    ) -> list[str]:
        """Ensure the deck has a TITLE opening and CLOSING ending."""
        changes: list[str] = []

        if definition.slides and definition.slides[0].slide_type != SlideType.TITLE:
            title_slide = SlideDefinition(
                slide_type=SlideType.TITLE,
                title=definition.metadata.title,
            )
            definition.slides.insert(0, title_slide)
            changes.append("Prepended TITLE slide (deck did not start with one).")

        if not definition.slides or definition.slides[-1].slide_type != SlideType.CLOSING:
            closing_slide = SlideDefinition(
                slide_type=SlideType.CLOSING,
                title="Thank You",
            )
            definition.slides.append(closing_slide)
            changes.append("Appended CLOSING slide.")

        return changes
