"""MCP server — exposes PPTX generation and review tools via Model Context Protocol."""

from __future__ import annotations

import json
import threading
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Annotated, Literal, Optional

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.exceptions import ToolError
from pydantic import Field

from pptx_mcp.agents.designer import DesignAgent, DesignerInput
from pptx_mcp.agents.iterator import IterationAgent, IterationInput
from pptx_mcp.agents.planner import PlannerInput, SlidePlannerAgent
from pptx_mcp.agents.reviewer import ReviewAgent, ReviewerInput
from pptx_mcp.agents.archetypes import ARCHETYPES
from pptx_mcp.brand.config import resolve_brand_config
from pptx_mcp.brand.defaults import PRESETS
from pptx_mcp.engine.generator import (
    DeckGenerator,
    _delete_slide,
    _duplicate_slide,
    _move_slide,
)
from pptx_mcp.engine.brand_extractor import extract_brand as _extract_brand_impl
from pptx_mcp.engine.parser import ingest_presentation, parse_presentation
from pptx_mcp.engine.spec_parser import parse_brand_guide as _parse_brand_guide_impl
from pptx_mcp.engine.spec_parser import parse_spec as _parse_spec_impl
from pptx_mcp.models.slides import (
    PresentationDefinition,
    SlideDefinition,
    SlideType,
)
from pptx_mcp.review.engine import ReviewEngine
from pptx_mcp.models.editor import AlignmentType, ElementLocator, ElementModification
from pptx_mcp.utils.errors import PptxMcpError
from pptx_mcp.utils.logging import get_logger
from pptx_mcp.utils.paths import validate_config_path, validate_output_path, validate_pptx_input, validate_save_path

# Type aliases for constrained tool parameters
BrandPreset = Optional[Literal[
    "executive", "technical", "minimal", "creative", "corporate", "consulting",
    "healthcare", "startup", "academic", "government", "finance", "nonprofit",
]]
AudienceType = Literal["executive", "technical", "general"]

logger = get_logger("server")

# ---------------------------------------------------------------------------
# Plan cache — stores planned PresentationDefinitions server-side so
# plan_presentation can return a compact summary instead of 13-16KB JSON.
# ---------------------------------------------------------------------------

@dataclass
class CachedPlan:
    plan_id: str
    definition: PresentationDefinition
    created_at: datetime
    topic: str

_plan_cache: dict[str, CachedPlan] = {}
_plan_cache_lock = threading.Lock()
_PLAN_CACHE_MAX = 50
_PLAN_CACHE_TTL = timedelta(hours=1)


def _cache_plan(definition: PresentationDefinition, topic: str) -> CachedPlan:
    """Cache a plan and return the CachedPlan entry.

    Evicts expired entries first, then oldest if at capacity.
    Thread-safe via _plan_cache_lock.
    """
    now = datetime.now()
    with _plan_cache_lock:
        # Evict expired
        expired = [k for k, v in _plan_cache.items() if now - v.created_at > _PLAN_CACHE_TTL]
        for k in expired:
            del _plan_cache[k]
        # Evict oldest if at capacity
        while len(_plan_cache) >= _PLAN_CACHE_MAX:
            oldest_key = min(_plan_cache, key=lambda k: _plan_cache[k].created_at)
            del _plan_cache[oldest_key]

        plan_id = uuid.uuid4().hex[:12]
        entry = CachedPlan(
            plan_id=plan_id,
            definition=definition,
            created_at=now,
            topic=topic,
        )
        _plan_cache[plan_id] = entry
    return entry


def _get_cached_plan(plan_id: str) -> CachedPlan:
    """Retrieve a cached plan by ID. Raises ValueError if not found or expired."""
    with _plan_cache_lock:
        entry = _plan_cache.get(plan_id)
        if entry is None:
            raise ValueError(f"Plan '{plan_id}' not found. It may have expired or been evicted.")
        if datetime.now() - entry.created_at > _PLAN_CACHE_TTL:
            del _plan_cache[plan_id]
            raise ValueError(f"Plan '{plan_id}' has expired (TTL is 1 hour).")
    return entry

mcp = FastMCP(
    "pptx-mcp",
    instructions="Model Context Protocol server for creating, editing, and reviewing PowerPoint presentations.",
)


def _raise_tool_error(tool_name: str, e: Exception) -> None:
    """Log the error and raise a ToolError with a safe message.

    Raises ToolError so the MCP response includes isError=True,
    letting the LLM client know the call failed.
    """
    logger.error("%s failed: %s", tool_name, e)
    if isinstance(e, PptxMcpError):
        raise ToolError(str(e)) from e
    error_type = type(e).__name__
    # Include message for common safe exception types so LLMs can recover
    if isinstance(e, (ValueError, TypeError, KeyError, IndexError)):
        raise ToolError(f"{error_type}: {e}") from e
    raise ToolError(f"Operation failed ({error_type}). Check server logs for details.") from e


def _resolve_brand(
    brand_config_path: Optional[str] = None,
    brand_preset: Optional[str] = None,
):
    """Resolve brand config with path validation."""
    if brand_config_path:
        validated = validate_config_path(brand_config_path)
        return resolve_brand_config(path=validated)
    return resolve_brand_config(preset=brand_preset)


# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------


@mcp.tool()
def create_presentation(
    definition: PresentationDefinition,
    output_path: str,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
) -> str:
    """Render a PresentationDefinition JSON to PPTX. LOW-LEVEL — avoid if possible.

    WARNING: Passing a full PresentationDefinition as input costs 4-25K tokens.
    Prefer plan_presentation + create_from_plan which keeps the definition
    server-side and costs <1K tokens total.

    Only use this when you already have a PresentationDefinition from
    parse_spec or read_presentation. Do NOT manually construct definitions.

    Args:
        definition: Presentation definition with metadata and slides.
        output_path: File path where the .pptx will be saved (must end in .pptx).
        brand_config_path: Optional path to a brand config YAML/JSON file.
        brand_preset: Optional preset name. Valid values: executive, technical,
            minimal, creative, corporate, consulting, healthcare, startup,
            academic, government, finance, nonprofit.

    Returns:
        JSON with status and output path.
    """
    try:
        validate_output_path(output_path)
        brand = _resolve_brand(brand_config_path, brand_preset)
        generator = DeckGenerator(brand=brand)
        result_path = generator.generate(definition, output_path)
        return json.dumps({
            "status": "success",
            "output_path": str(result_path),
            "slide_count": len(definition.slides),
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("create_presentation", e)
        return ""  # unreachable, satisfies type checker


@mcp.tool()
def add_slide(
    file_path: str,
    slide: SlideDefinition,
    position: Optional[Annotated[int, Field(ge=0)]] = None,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
) -> str:
    """Add a slide to an existing PPTX presentation.

    Args:
        file_path: Path to the existing .pptx file.
        slide: Slide definition with slide_type, title, content, etc.
        position: 0-based position to insert. Appends if omitted.
        brand_config_path: Optional brand config path.
        brand_preset: Optional brand preset name.

    Returns:
        JSON with status.
    """
    try:
        validate_pptx_input(file_path)
        brand = _resolve_brand(brand_config_path, brand_preset)
        generator = DeckGenerator(brand=brand)
        generator.add_slide(file_path, slide, position=position)
        return json.dumps({"status": "success", "file_path": file_path})
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("add_slide", e)
        return ""


@mcp.tool()
def update_slide(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    slide: SlideDefinition,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
) -> str:
    """Replace a specific slide in an existing presentation.

    Args:
        file_path: Path to the existing .pptx file.
        slide_index: 0-based index of the slide to replace.
        slide: New slide definition to replace the existing slide.
        brand_config_path: Optional brand config path.
        brand_preset: Optional brand preset name.

    Returns:
        JSON with status.
    """
    try:
        validate_pptx_input(file_path)
        brand = _resolve_brand(brand_config_path, brand_preset)
        generator = DeckGenerator(brand=brand)
        generator.update_slide(file_path, slide_index, slide)
        return json.dumps({"status": "success", "file_path": file_path, "slide_index": slide_index})
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("update_slide", e)
        return ""


@mcp.tool()
def delete_slide(file_path: str, slide_index: Annotated[int, Field(ge=0)]) -> str:
    """Delete a slide from a presentation.

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based index of the slide to delete.

    Returns:
        JSON with status.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        prs = Presentation(file_path)
        _delete_slide(prs, slide_index)
        prs.save(file_path)

        return json.dumps({"status": "success", "file_path": file_path})
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("delete_slide", e)
        return ""


@mcp.tool()
def move_slide(
    file_path: str,
    from_index: Annotated[int, Field(ge=0)],
    to_index: Annotated[int, Field(ge=0)],
) -> str:
    """Move a slide from one position to another within a presentation.

    Args:
        file_path: Path to the .pptx file.
        from_index: Current 0-based index of the slide to move.
        to_index: Target 0-based index where the slide should end up.

    Returns:
        JSON with status.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        prs = Presentation(file_path)
        _move_slide(prs, from_index, to_index)
        prs.save(file_path)

        return json.dumps({
            "status": "success",
            "file_path": file_path,
            "moved": {"from": from_index, "to": to_index},
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("move_slide", e)
        return ""


@mcp.tool()
def duplicate_slide(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
) -> str:
    """Duplicate a slide, placing the copy immediately after the original.

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based index of the slide to duplicate.

    Returns:
        JSON with status and the index of the new slide.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        prs = Presentation(file_path)
        new_index = _duplicate_slide(prs, slide_index)
        prs.save(file_path)

        return json.dumps({
            "status": "success",
            "file_path": file_path,
            "new_slide_index": new_index,
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("duplicate_slide", e)
        return ""


@mcp.tool()
def add_speaker_notes(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    notes: str,
) -> str:
    """Add or update speaker notes for a specific slide.

    Args:
        file_path: Path to existing .pptx file.
        slide_index: Zero-based slide index.
        notes: Speaker notes text to set.

    Returns:
        JSON with status.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]
        notes_slide = slide.notes_slide
        notes_slide.notes_text_frame.text = notes
        prs.save(file_path)

        return json.dumps({
            "status": "success",
            "file_path": file_path,
            "slide_index": slide_index,
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("add_speaker_notes", e)
        return ""


@mcp.tool()
def read_presentation(file_path: str, summary: bool = True) -> str:
    """Read and parse an existing PPTX file into structured JSON.

    Args:
        file_path: Path to the .pptx file.
        summary: If True (default), returns a compact outline. Set to False for full detail.

    Returns:
        JSON representation — compact summary or full PresentationDefinition.
    """
    try:
        validate_pptx_input(file_path)
        definition = parse_presentation(file_path)
        if summary:
            slides_summary = []
            for i, s in enumerate(definition.slides):
                entry: dict = {
                    "index": i,
                    "type": s.slide_type.value if s.slide_type else "unknown",
                    "title": s.title or "(untitled)",
                }
                if s.bullets:
                    entry["bullet_count"] = len(s.bullets)
                if s.columns:
                    entry["column_count"] = len(s.columns)
                if s.elements:
                    entry["element_count"] = len(s.elements)
                if s.speaker_notes:
                    entry["has_notes"] = True
                slides_summary.append(entry)
            return json.dumps({
                "title": definition.metadata.title,
                "author": definition.metadata.author,
                "slide_count": len(definition.slides),
                "slides": slides_summary,
            }, indent=2)
        return definition.model_dump_json(indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("read_presentation", e)
        return ""


@mcp.tool()
def analyze_presentation(
    file_path: str,
    brand_preset: BrandPreset = None,
) -> str:
    """Analyze a presentation and return natural-language descriptions, content inventory, and recommendations.

    Combines structural parsing, slide descriptions, content analysis, and
    design review into a single comprehensive analysis. Use this to understand
    what's in a deck before editing or restyling.

    Args:
        file_path: Path to the .pptx file.
        brand_preset: Optional brand preset for review scoring.

    Returns:
        JSON with title, slide_count, overall_score, slide_descriptions,
        content_inventory, type_distribution, narrative_assessment, and recommendations.
    """
    try:
        validate_pptx_input(file_path)
        definition = parse_presentation(file_path)

        # Natural-language analysis
        from pptx_mcp.engine.describer import describe_presentation
        analysis = describe_presentation(definition.slides)

        # Review scoring
        brand = _resolve_brand(brand_preset=brand_preset)
        engine = ReviewEngine(brand=brand)
        review = engine.review_definition(definition)

        return json.dumps({
            "title": definition.metadata.title,
            "slide_count": len(definition.slides),
            "overall_score": review.overall_score,
            **analysis,
        }, indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("analyze_presentation", e)
        return ""


@mcp.tool()
def review_presentation(
    file_path: str,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
    summary_only: bool = False,
) -> str:
    """Review a presentation and provide structured design feedback.

    Args:
        file_path: Path to the .pptx file.
        brand_config_path: Optional brand config path.
        brand_preset: Optional brand preset name.
        summary_only: If True, return only the overall score, total issues,
            and per-slide issue counts (~200 tokens) instead of the full
            review (~2-8K tokens). Use this for a quick health check.

    Returns:
        JSON review result with scores, issues, and suggestions.
    """
    try:
        validate_pptx_input(file_path)
        brand = _resolve_brand(brand_config_path, brand_preset)
        engine = ReviewEngine(brand=brand)
        result = engine.review_file(file_path)
        if summary_only:
            return json.dumps({
                "overall_score": result.overall_score,
                "total_issues": result.total_issues,
                "slides": [
                    {
                        "slide_index": sr.slide_index,
                        "issue_count": len(sr.issues),
                        "error_count": sum(1 for i in sr.issues if i.severity.value == "error"),
                        "warning_count": sum(1 for i in sr.issues if i.severity.value == "warning"),
                    }
                    for sr in result.slide_reviews
                    if sr.issues
                ],
            }, indent=2)
        return result.model_dump_json(indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("review_presentation", e)
        return ""


@mcp.tool()
def apply_brand_guidelines(
    file_path: str,
    output_path: Optional[str] = None,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
) -> str:
    """Re-render a presentation with brand guidelines applied.

    Parses the existing deck, then re-generates it with the specified brand config.

    Args:
        file_path: Path to the existing .pptx file.
        output_path: Where to save the branded version. Overwrites original if omitted.
        brand_config_path: Brand config file path.
        brand_preset: Brand preset name.

    Returns:
        JSON with status and output path.
    """
    try:
        validate_pptx_input(file_path)
        definition = parse_presentation(file_path)
        brand = _resolve_brand(brand_config_path, brand_preset)
        generator = DeckGenerator(brand=brand)
        out = output_path or file_path
        result_path = generator.generate(definition, out)
        return json.dumps({"status": "success", "output_path": str(result_path)})
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("apply_brand_guidelines", e)
        return ""


@mcp.tool()
def restyle_presentation(
    input_path: str,
    output_path: str,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
) -> str:
    """Restyle an existing presentation by ingesting it and re-generating with a new brand.

    Deep-reads the source deck, detects slide types via heuristics (title,
    section, content, two-column, big-number, quote, chart, table, etc.),
    extracts all content (titles, bullets, body, table data, chart data,
    speaker notes), then re-renders every slide through the layout engine
    with the requested brand config.

    This is the primary tool for consulting-style "feed a deck and restyle
    it better" workflows.

    Args:
        input_path: Path to the existing .pptx file to restyle.
        output_path: Where to save the restyled .pptx (must end in .pptx).
        brand_config_path: Optional path to a brand config YAML/JSON file.
        brand_preset: Optional preset name. Valid values: executive, technical,
            minimal, creative, corporate, consulting, healthcare, startup,
            academic, government, finance, nonprofit.

    Returns:
        JSON with status, slide count, detected slide types, and output path.
    """
    try:
        validate_pptx_input(input_path)
        validate_output_path(output_path)

        definition = ingest_presentation(input_path)
        brand = _resolve_brand(brand_config_path, brand_preset)
        generator = DeckGenerator(brand=brand)
        result_path = generator.generate(definition, output_path)

        detected_types = [s.slide_type.value for s in definition.slides]

        return json.dumps({
            "status": "success",
            "output_path": str(result_path),
            "slide_count": len(definition.slides),
            "detected_types": detected_types,
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("restyle_presentation", e)
        return ""


@mcp.tool()
def plan_presentation(
    topic: str,
    audience: AudienceType = "executive",
    num_slides: Annotated[int, Field(ge=3, le=50)] = 10,
    key_points: Optional[list[str]] = None,
    context: str = "",
    author: Optional[str] = None,
    company: Optional[str] = None,
    deck_type: Optional[str] = None,
    return_full_json: bool = False,
) -> str:
    """Plan a presentation and cache it server-side. RECOMMENDED for 9+ slides.

    Returns a compact summary (~500 tokens) with a plan_id. The full
    definition is cached server-side for 1 hour — no large JSON is
    returned. Use create_from_plan with the plan_id to render.

    This is the most token-efficient workflow for any deck with 9+ slides.
    For small decks (3-8 slides), generate_presentation is simpler.

    Workflow:
    1. plan_presentation -> returns plan_id + slide summary (~500 tokens)
    2. Optionally get_plan_slide to inspect individual slides (~200 tokens each)
    3. Optionally update_plan_slide to modify slides before rendering
    4. create_from_plan with plan_id -> renders to .pptx (~80 tokens)

    Total token cost: ~600-800 tokens (vs 8-20K for manual JSON construction).

    Args:
        topic: Main topic or title.
        audience: Target audience.
        num_slides: Target number of slides (3-50).
        key_points: List of key points to cover.
        context: Additional context or instructions.
        author: Author name.
        company: Company name.
        deck_type: Deck archetype name (e.g. 'pitch_deck', 'board_deck',
            'strategy_deck', 'sales_deck'). Call get_deck_types for the full
            list of 27 archetypes.
        return_full_json: If True, return the full PresentationDefinition JSON
            instead of a compact summary. AVOID — costs 8-20K tokens.

    Returns:
        JSON — compact summary with plan_id (default) or full PresentationDefinition.
    """
    try:
        planner = SlidePlannerAgent()
        plan_input = PlannerInput(
            topic=topic,
            audience=audience,
            num_slides=num_slides,
            key_points=key_points or [],
            context=context,
            author=author,
            company=company,
            deck_type=deck_type,
        )
        result = planner.run(plan_input)

        if return_full_json:
            return result.model_dump_json(indent=2)

        # Cache and return compact summary
        entry = _cache_plan(result, topic)
        slides_summary = []
        for i, s in enumerate(result.slides):
            item: dict = {
                "index": i,
                "slide_type": s.slide_type.value if s.slide_type else "unknown",
                "title": s.title or "(untitled)",
            }
            if s.kpis:
                item["kpi_count"] = len(s.kpis)
            if s.bullets:
                item["bullet_count"] = len(s.bullets)
            if s.columns:
                item["column_count"] = len(s.columns)
            if s.cards:
                item["card_count"] = len(s.cards)
            if s.steps:
                item["step_count"] = len(s.steps)
            if s.icons:
                item["icon_count"] = len(s.icons)
            if s.team_members:
                item["member_count"] = len(s.team_members)
            if s.funnel_stages:
                item["stage_count"] = len(s.funnel_stages)
            if s.agenda_items:
                item["agenda_count"] = len(s.agenda_items)
            slides_summary.append(item)

        return json.dumps({
            "plan_id": entry.plan_id,
            "topic": topic,
            "slide_count": len(result.slides),
            "slides": slides_summary,
            "instructions": "Call create_from_plan with this plan_id to render. Use get_plan_slide to inspect individual slides.",
        }, indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("plan_presentation", e)
        return ""


@mcp.tool()
def generate_presentation(
    topic: str,
    output_path: str,
    audience: AudienceType = "executive",
    num_slides: Annotated[int, Field(ge=3, le=8)] = 6,
    key_points: Optional[list[str]] = None,
    context: str = "",
    author: Optional[str] = None,
    company: Optional[str] = None,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
    deck_type: Optional[str] = None,
) -> str:
    """One-shot deck creation for SMALL decks only (3-8 slides).

    Plans, optimizes, and renders in a single call. Hard-capped at 8 slides
    to stay within output token limits.

    For 9+ slides, you MUST use the two-step workflow instead:
    plan_presentation -> create_from_plan. This keeps the full definition
    server-side and uses ~80 tokens per call instead of 8-20K.

    Args:
        topic: Main topic or title.
        output_path: File path where the .pptx will be saved (must end in .pptx).
        audience: Target audience.
        num_slides: Target number of slides (3-8). Default 6.
        key_points: List of key points to cover.
        context: Additional context or instructions.
        author: Author name.
        company: Company name.
        brand_config_path: Optional path to a brand config YAML/JSON file.
        brand_preset: Optional preset name. Valid values: executive, technical,
            minimal, creative, corporate, consulting, healthcare, startup,
            academic, government, finance, nonprofit.
        deck_type: Deck archetype name (e.g. 'pitch_deck', 'board_deck',
            'strategy_deck', 'sales_deck'). Call get_deck_types for the full
            list of 27 archetypes.

    Returns:
        JSON with status, output path, and slide count.
    """
    try:
        validate_output_path(output_path)
        # Step 1: Plan
        planner = SlidePlannerAgent()
        plan_input = PlannerInput(
            topic=topic,
            audience=audience,
            num_slides=num_slides,
            key_points=key_points or [],
            context=context,
            author=author,
            company=company,
            deck_type=deck_type,
        )
        definition = planner.run(plan_input)

        # Step 2: Design optimize
        designer = DesignAgent()
        design_result = designer.run(DesignerInput(
            definition=definition,
            brand_config_path=brand_config_path,
            brand_preset=brand_preset,
        ))

        # Step 3: Generate PPTX
        brand = _resolve_brand(brand_config_path, brand_preset)
        generator = DeckGenerator(brand=brand)
        result_path = generator.generate(design_result.definition, output_path)

        return json.dumps({
            "status": "success",
            "output_path": str(result_path),
            "slide_count": len(design_result.definition.slides),
            "design_changes": design_result.changes_made,
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("generate_presentation", e)
        return ""


@mcp.tool()
def optimize_design(
    definition: PresentationDefinition,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
) -> str:
    """Optimize design/layout of a PresentationDefinition. LOW-LEVEL — avoid if possible.

    WARNING: This tool accepts AND returns a full PresentationDefinition,
    costing 8-50K tokens round-trip. For cached plans, use optimize_plan
    instead (operates server-side, costs ~100 tokens).

    Uses the DesignAgent to apply composition rules, balance columns,
    split overloaded slides, and improve visual hierarchy.

    Args:
        definition: Presentation definition to optimize.
        brand_config_path: Optional brand config path.
        brand_preset: Optional brand preset name.

    Returns:
        JSON with optimized definition and list of changes made.
    """
    try:
        designer = DesignAgent()
        result = designer.run(DesignerInput(
            definition=definition,
            brand_config_path=brand_config_path,
            brand_preset=brand_preset,
        ))
        return json.dumps({
            "definition": json.loads(result.definition.model_dump_json()),
            "changes_made": result.changes_made,
        }, indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("optimize_design", e)
        return ""


@mcp.tool()
def improve_presentation(
    file_path: str,
    output_path: Optional[str] = None,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
) -> str:
    """Review and automatically improve a presentation.

    Runs the full pipeline: parse -> review -> iterate -> re-generate.

    Args:
        file_path: Path to the existing .pptx file.
        output_path: Where to save the improved version.
        brand_config_path: Optional brand config path.
        brand_preset: Optional brand preset name.

    Returns:
        JSON with review results and improvement details.
    """
    try:
        validate_pptx_input(file_path)
        brand = _resolve_brand(brand_config_path, brand_preset)

        # Parse
        definition = parse_presentation(file_path)

        # Review
        review_engine = ReviewEngine(brand=brand)
        review = review_engine.review_definition(definition)

        # Iterate
        iterator = IterationAgent()
        iteration_result = iterator.run(IterationInput(
            definition=definition,
            review=review,
            brand=brand,
        ))

        # Design optimize
        designer = DesignAgent()
        design_result = designer.run(DesignerInput(
            definition=iteration_result.definition,
            brand_config_path=brand_config_path,
            brand_preset=brand_preset,
        ))

        # Re-generate
        out = output_path or file_path
        generator = DeckGenerator(brand=brand)
        result_path = generator.generate(design_result.definition, out)

        # Re-review to get improved score
        improved_review = review_engine.review_definition(design_result.definition)

        return json.dumps({
            "status": "success",
            "output_path": str(result_path),
            "original_score": review.overall_score,
            "improved_score": improved_review.overall_score,
            "fixes_applied": iteration_result.fixes_applied + design_result.changes_made,
            "remaining_issues": len(iteration_result.remaining_issues),
        }, indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("improve_presentation", e)
        return ""


@mcp.tool()
def parse_spec(
    markdown: str,
    output_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
) -> str:
    """Parse a markdown presentation spec into slides and optionally generate a PPTX.

    Accepts structured markdown specs (from any AI or human author) that describe
    slide content, layout hints, and design tokens. Extracts slides, infers slide
    types, and maps design tokens to brand config.

    If output_path is provided, generates the PPTX directly.
    Otherwise, returns the parsed PresentationDefinition JSON for review/editing.

    Args:
        markdown: Raw markdown text of the presentation spec.
        output_path: Optional .pptx path — if provided, generates the file.
        brand_preset: Optional brand preset to override spec-extracted brand.

    Returns:
        JSON with parsed definition, detected brand, and optionally the output path.
    """
    try:
        definition, spec_brand = _parse_spec_impl(markdown)

        result: dict = {
            "slide_count": len(definition.slides),
            "detected_types": [s.slide_type.value for s in definition.slides],
            "spec_brand_detected": spec_brand is not None,
        }

        if output_path:
            validate_output_path(output_path)
            # Use spec brand unless overridden by preset
            if brand_preset:
                brand = _resolve_brand(brand_preset=brand_preset)
            elif spec_brand:
                brand = spec_brand
            else:
                brand = _resolve_brand()
            generator = DeckGenerator(brand=brand)
            path = generator.generate(definition, output_path)
            result["status"] = "success"
            result["output_path"] = str(path)
        else:
            result["definition"] = json.loads(definition.model_dump_json())
            if spec_brand:
                result["spec_brand"] = json.loads(spec_brand.model_dump_json())

        return json.dumps(result, indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("parse_spec", e)
        return ""


@mcp.tool()
def create_from_plan(
    plan_id: str,
    output_path: str,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
    optimize: bool = True,
) -> str:
    """Render a cached plan to a PPTX file.

    Takes a plan_id returned by plan_presentation and generates the
    PowerPoint file. Optionally runs the DesignAgent optimizer first.

    Args:
        plan_id: The plan_id from a previous plan_presentation call.
        output_path: File path where the .pptx will be saved (must end in .pptx).
        brand_config_path: Optional path to a brand config YAML/JSON file.
        brand_preset: Optional preset name. Valid values: executive, technical,
            minimal, creative, corporate, consulting, healthcare, startup,
            academic, government, finance, nonprofit.
        optimize: Run DesignAgent layout optimization before rendering (default True).

    Returns:
        JSON with status, output_path, and slide_count.
    """
    try:
        validate_output_path(output_path)
        entry = _get_cached_plan(plan_id)
        definition = entry.definition

        if optimize:
            designer = DesignAgent()
            design_result = designer.run(DesignerInput(
                definition=definition,
                brand_config_path=brand_config_path,
                brand_preset=brand_preset,
            ))
            definition = design_result.definition

        brand = _resolve_brand(brand_config_path, brand_preset)
        generator = DeckGenerator(brand=brand)
        result_path = generator.generate(definition, output_path)

        return json.dumps({
            "status": "success",
            "output_path": str(result_path),
            "slide_count": len(definition.slides),
        })
    except ValueError as e:
        raise ToolError(str(e)) from e
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("create_from_plan", e)
        return ""


@mcp.tool()
def optimize_plan(
    plan_id: str,
    brand_config_path: Optional[str] = None,
    brand_preset: BrandPreset = None,
) -> str:
    """Run DesignAgent optimization on a cached plan (server-side).

    This is the token-efficient alternative to optimize_design. Instead of
    passing a full PresentationDefinition through the wire (8-50K tokens),
    this operates on the server-side cached plan (~100 tokens total).

    The optimized definition replaces the cached plan in-place — subsequent
    calls to create_from_plan or get_plan_slide will use the optimized version.

    Args:
        plan_id: The plan_id from a previous plan_presentation call.
        brand_config_path: Optional path to a brand config YAML/JSON file.
        brand_preset: Optional preset name.

    Returns:
        JSON with optimization status and list of changes made.
    """
    try:
        entry = _get_cached_plan(plan_id)
        designer = DesignAgent()
        result = designer.run(DesignerInput(
            definition=entry.definition,
            brand_config_path=brand_config_path,
            brand_preset=brand_preset,
        ))
        # Update the cached plan in-place
        with _plan_cache_lock:
            if plan_id in _plan_cache:
                _plan_cache[plan_id].definition = result.definition
        return json.dumps({
            "status": "success",
            "plan_id": plan_id,
            "changes_made": result.changes_made,
            "slide_count": len(result.definition.slides),
        }, indent=2)
    except ValueError as e:
        raise ToolError(str(e)) from e
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("optimize_plan", e)
        return ""


@mcp.tool()
def get_plan_slide(
    plan_id: str,
    slide_index: Annotated[int, Field(ge=0)],
) -> str:
    """Get the full definition of a single slide from a cached plan.

    Returns the complete SlideDefinition JSON for one slide (~0.5-1.5KB),
    letting you inspect individual slides without pulling the entire plan.

    Args:
        plan_id: The plan_id from a previous plan_presentation call.
        slide_index: 0-based index of the slide to retrieve.

    Returns:
        JSON with the full SlideDefinition for the requested slide.
    """
    try:
        entry = _get_cached_plan(plan_id)
        slides = entry.definition.slides
        if slide_index < 0 or slide_index >= len(slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(slides) - 1})"
            )
        slide = slides[slide_index]
        return json.dumps({
            "plan_id": plan_id,
            "slide_index": slide_index,
            "slide": json.loads(slide.model_dump_json()),
        }, indent=2)
    except ValueError as e:
        raise ToolError(str(e)) from e
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("get_plan_slide", e)
        return ""


@mcp.tool()
def get_slide_templates() -> str:
    """List all available slide types with their schemas and descriptions.

    Returns:
        JSON with slide types, required fields, and example structures.
    """
    templates = {
        "title": {
            "description": "Title slide with centered title and optional subtitle",
            "fields": ["title", "subtitle"],
            "example": {"slide_type": "title", "title": "Quarterly Review", "subtitle": "Q4 2025"},
        },
        "section": {
            "description": "Section divider with accent bar",
            "fields": ["title", "subtitle"],
            "example": {"slide_type": "section", "title": "Market Analysis"},
        },
        "content": {
            "description": "Standard content with title, body text, and/or bullets",
            "fields": ["title", "body", "bullets"],
            "example": {
                "slide_type": "content",
                "title": "Key Findings",
                "bullets": ["Revenue grew 15%", "Customer retention at 92%", "NPS improved to 67"],
            },
        },
        "two_column": {
            "description": "Two-column layout with independent content",
            "fields": ["title", "columns"],
            "example": {
                "slide_type": "two_column",
                "title": "Before vs After",
                "columns": [
                    {"title": "Before", "bullets": ["Manual process", "5-day turnaround"]},
                    {"title": "After", "bullets": ["Automated", "Same-day delivery"]},
                ],
            },
        },
        "pros_cons": {
            "description": "Pros and cons comparison with color-coded lists",
            "fields": ["title", "pros", "cons"],
            "example": {
                "slide_type": "pros_cons",
                "title": "Migration Assessment",
                "pros": ["Improved performance", "Lower cost"],
                "cons": ["Migration risk", "Learning curve"],
            },
        },
        "chart": {
            "description": "Chart slide (bar, line, pie, column, area)",
            "fields": ["title", "elements (ChartElement)"],
            "example": {
                "slide_type": "chart",
                "title": "Revenue by Quarter",
                "elements": [{
                    "type": "chart",
                    "chart_type": "bar",
                    "data": {
                        "categories": ["Q1", "Q2", "Q3", "Q4"],
                        "series": [{"name": "Revenue", "values": [100, 120, 115, 140]}],
                    },
                }],
            },
        },
        "comparison": {
            "description": "Multi-column comparison with header bars",
            "fields": ["title", "columns"],
            "example": {
                "slide_type": "comparison",
                "title": "Platform Comparison",
                "columns": [
                    {"title": "AWS", "bullets": ["Mature", "Broadest services"]},
                    {"title": "Azure", "bullets": ["Enterprise focus", "AD integration"]},
                    {"title": "GCP", "bullets": ["ML/AI strength", "Competitive pricing"]},
                ],
            },
        },
        "diagram": {
            "description": "Shapes and connectors for visual diagrams",
            "fields": ["title", "elements (ShapeElement, ConnectorElement)"],
        },
        "table": {
            "description": "Table with header row and data rows",
            "fields": ["title", "elements (TableElement)"],
        },
        "blank": {
            "description": "Empty slide for freeform element placement",
            "fields": ["elements"],
        },
        "quote": {
            "description": "Quote slide with large quote text, attribution, and decorative elements",
            "fields": ["body (the quote)", "quote_attribution", "quote_attribution_role"],
            "example": {
                "slide_type": "quote",
                "body": "The best way to predict the future is to create it.",
                "quote_attribution": "Peter Drucker",
                "quote_attribution_role": "Management Consultant",
            },
        },
        "big_number": {
            "description": "Hero metric slide with a large number, label, and optional context",
            "fields": ["body (the number)", "title (label)", "subtitle (context)", "bullets (comparison)"],
            "example": {
                "slide_type": "big_number",
                "body": "47%",
                "title": "Customer Satisfaction Score",
                "subtitle": "Q4 2025 Results",
                "bullets": ["Up from 32% last quarter"],
            },
        },
        "process": {
            "description": "Horizontal chevron process flow (2-5 steps)",
            "fields": ["title", "steps", "active_step"],
            "example": {
                "slide_type": "process",
                "title": "Implementation Plan",
                "active_step": 1,
                "steps": [
                    {"title": "Discovery", "description": "Gather requirements", "status": "completed"},
                    {"title": "Design", "description": "Create architecture", "status": "active"},
                    {"title": "Build", "description": "Implement solution", "status": "future"},
                ],
            },
        },
        "timeline": {
            "description": "Vertical timeline with milestone dots and content",
            "fields": ["title", "steps (with date field)", "active_step"],
            "example": {
                "slide_type": "timeline",
                "title": "Project Milestones",
                "steps": [
                    {"title": "Kickoff", "date": "Jan 2025", "status": "completed"},
                    {"title": "Beta Launch", "date": "Mar 2025", "status": "active"},
                    {"title": "GA Release", "date": "Jun 2025", "status": "future"},
                ],
            },
        },
        "step_cards": {
            "description": "Numbered step cards in a horizontal row (2-5 cards)",
            "fields": ["title", "steps"],
            "example": {
                "slide_type": "step_cards",
                "title": "How It Works",
                "steps": [
                    {"title": "Sign Up", "description": "Create your account in minutes"},
                    {"title": "Configure", "description": "Set your preferences"},
                    {"title": "Launch", "description": "Go live with one click"},
                ],
            },
        },
        "dashboard": {
            "description": "KPI dashboard with metric cards in an auto-grid (2-12 cards)",
            "fields": ["title", "kpis"],
            "example": {
                "slide_type": "dashboard",
                "title": "Key Metrics — Q4 2025",
                "kpis": [
                    {"value": "$1.2M", "label": "Revenue", "change": "+15%", "trend": "up"},
                    {"value": "92%", "label": "Retention", "change": "+3%", "trend": "up"},
                    {"value": "67", "label": "NPS Score", "change": "-2", "trend": "down"},
                    {"value": "4.2s", "label": "Avg Response", "trend": "neutral"},
                ],
            },
        },
        "image_text": {
            "description": "Split layout with image on one side and text/bullets on the other",
            "fields": ["title", "image_path", "image_position", "body", "bullets"],
            "example": {
                "slide_type": "image_text",
                "title": "Our Product",
                "image_path": "/path/to/product.png",
                "image_position": "left",
                "body": "Next-generation platform for enterprise teams.",
                "bullets": ["Fast deployment", "Built-in analytics", "24/7 support"],
            },
        },
        "icon_grid": {
            "description": "Grid of icon + title + description cards (2-12 items)",
            "fields": ["title", "icons"],
            "example": {
                "slide_type": "icon_grid",
                "title": "Our Values",
                "icons": [
                    {"icon": "\U0001F680", "title": "Speed", "description": "Ship fast, iterate often"},
                    {"icon": "\U0001F91D", "title": "Trust", "description": "Transparent by default"},
                    {"icon": "\U0001F4A1", "title": "Innovation", "description": "Question everything"},
                    {"icon": "\U0001F4AA", "title": "Resilience", "description": "Embrace failure"},
                ],
            },
        },
        "team": {
            "description": "Team bio cards with avatar circles, names, roles, and optional bios",
            "fields": ["title", "team_members"],
            "example": {
                "slide_type": "team",
                "title": "Leadership Team",
                "team_members": [
                    {"name": "Jane Smith", "role": "CEO", "bio": "20 years in enterprise SaaS"},
                    {"name": "John Doe", "role": "CTO", "bio": "Former VP Engineering at Acme"},
                    {"name": "Alice Chen", "role": "CFO", "bio": "IPO experience across 3 companies"},
                ],
            },
        },
        "closing": {
            "description": "Closing / thank-you slide with optional CTA button and contact info",
            "fields": ["title", "subtitle", "cta_text", "contact_info"],
            "example": {
                "slide_type": "closing",
                "title": "Thank You",
                "subtitle": "Questions? Let's connect.",
                "cta_text": "Schedule a Demo",
                "contact_info": ["hello@company.com", "+1 (555) 123-4567"],
            },
        },
        "swot": {
            "description": "SWOT analysis 2x2 quadrant matrix with color-coded sections",
            "fields": ["title", "swot"],
            "example": {
                "slide_type": "swot",
                "title": "SWOT Analysis",
                "swot": {
                    "strengths": ["Strong brand", "Loyal customers"],
                    "weaknesses": ["High costs", "Limited reach"],
                    "opportunities": ["New markets", "Partnerships"],
                    "threats": ["Competition", "Regulation"],
                },
            },
        },
        "funnel": {
            "description": "Vertical funnel with 3-6 stages of decreasing width",
            "fields": ["title", "funnel_stages"],
            "example": {
                "slide_type": "funnel",
                "title": "Sales Pipeline",
                "funnel_stages": [
                    {"label": "Prospects", "value": "10,000", "description": "Website visitors"},
                    {"label": "Leads", "value": "2,500", "description": "Form submissions"},
                    {"label": "Qualified", "value": "800", "description": "Sales-qualified leads"},
                    {"label": "Closed", "value": "120", "description": "Won deals"},
                ],
            },
        },
        "card_grid": {
            "description": "Rich card grid with icon, title, body, bullets, and optional badge (2-12 cards)",
            "fields": ["title", "cards"],
            "example": {
                "slide_type": "card_grid",
                "title": "Our Services",
                "cards": [
                    {
                        "title": "Strategy",
                        "icon": "\U0001F3AF",
                        "body": "End-to-end strategic advisory",
                        "bullets": ["Market analysis", "Growth planning"],
                        "badge": {"text": "NEW"},
                    },
                    {
                        "title": "Implementation",
                        "icon": "\U0001F527",
                        "body": "Hands-on delivery and execution",
                        "bullets": ["Agile delivery", "Change management"],
                    },
                    {
                        "title": "Analytics",
                        "icon": "\U0001F4CA",
                        "body": "Data-driven insights and reporting",
                        "bullets": ["Dashboards", "Predictive models"],
                    },
                ],
            },
        },
        "agenda": {
            "description": "Visual agenda with numbered circles and status indicators",
            "fields": ["title", "agenda_items", "active_agenda_item"],
            "example": {
                "slide_type": "agenda",
                "title": "Today's Agenda",
                "active_agenda_item": 1,
                "agenda_items": [
                    {"title": "Opening Remarks", "status": "completed"},
                    {"title": "Q4 Financial Review", "description": "Revenue, margins, and forecast", "status": "active"},
                    {"title": "2026 Strategic Priorities", "description": "Growth plan and investments", "status": "upcoming"},
                    {"title": "Q&A", "status": "upcoming"},
                ],
            },
        },
        "org_chart": {
            "description": "Organizational hierarchy chart with boxes and connector lines",
            "fields": ["title", "org_chart (OrgNode tree)"],
            "example": {
                "slide_type": "org_chart",
                "title": "Organization Structure",
                "org_chart": {
                    "id": "ceo",
                    "name": "CEO",
                    "role": "Chief Executive Officer",
                    "children": [
                        {"id": "vp-eng", "name": "VP Engineering", "role": "Technology"},
                        {"id": "vp-sales", "name": "VP Sales", "role": "Revenue"},
                    ],
                },
            },
        },
        "matrix": {
            "description": "2x2 quadrant matrix with labeled axes and quadrant objects",
            "fields": ["title", "matrix_data (MatrixData with top_left/top_right/bottom_left/bottom_right)"],
            "example": {
                "slide_type": "matrix",
                "title": "Priority Matrix",
                "matrix_data": {
                    "x_axis_label": "Effort",
                    "y_axis_label": "Impact",
                    "top_left": {"label": "Quick Wins", "items": ["SSO Integration"]},
                    "top_right": {"label": "Strategic Bets", "items": ["Cloud Migration"]},
                    "bottom_left": {"label": "Fill-Ins", "items": ["Doc Updates"]},
                    "bottom_right": {"label": "Avoid", "items": ["Legacy Rewrite"]},
                },
            },
        },
        "pyramid": {
            "description": "Layered pyramid diagram (top = narrow, bottom = wide)",
            "fields": ["title", "pyramid_layers"],
            "example": {
                "slide_type": "pyramid",
                "title": "Strategy Framework",
                "pyramid_layers": [
                    {"label": "Vision", "description": "Where we're headed"},
                    {"label": "Strategy", "description": "How we'll get there"},
                    {"label": "Execution", "description": "What we do daily"},
                ],
            },
        },
        "venn": {
            "description": "Venn diagram with 2-3 overlapping circles and optional intersection label",
            "fields": ["title", "venn_data (VennData)"],
            "example": {
                "slide_type": "venn",
                "title": "Digital Maturity",
                "venn_data": {
                    "circles": [
                        {"label": "Technology"},
                        {"label": "People"},
                        {"label": "Process"},
                    ],
                    "intersection_label": "Innovation",
                },
            },
        },
        "waterfall": {
            "description": "Waterfall chart showing cumulative positive/negative deltas",
            "fields": ["title", "waterfall_bars"],
            "example": {
                "slide_type": "waterfall",
                "title": "Profit Bridge Q3 → Q4",
                "waterfall_bars": [
                    {"label": "Q3 Profit", "value": 12.0, "bar_type": "total"},
                    {"label": "New Revenue", "value": 4.2},
                    {"label": "Cost Savings", "value": 1.8},
                    {"label": "Restructuring", "value": -2.1},
                    {"label": "Q4 Profit", "value": 15.9, "bar_type": "total"},
                ],
            },
        },
        "hub_spoke": {
            "description": "Central hub with radiating spoke nodes in a circle",
            "fields": ["title", "hub_spoke_center", "spokes"],
            "example": {
                "slide_type": "hub_spoke",
                "title": "Data Platform",
                "hub_spoke_center": "Core Platform",
                "spokes": [
                    {"label": "Ingestion", "description": "Real-time data feeds"},
                    {"label": "Storage", "description": "Distributed data lake"},
                    {"label": "Analytics", "description": "Self-serve dashboards"},
                ],
            },
        },
        "cycle": {
            "description": "Circular process flow with arrows connecting steps",
            "fields": ["title", "cycle_steps"],
            "example": {
                "slide_type": "cycle",
                "title": "DevOps Lifecycle",
                "cycle_steps": [
                    {"label": "Plan", "description": "Define requirements"},
                    {"label": "Build", "description": "Write code"},
                    {"label": "Test", "description": "Validate quality"},
                    {"label": "Deploy", "description": "Release to production"},
                    {"label": "Monitor", "description": "Observe performance"},
                ],
            },
        },
        "gantt": {
            "description": "Gantt chart timeline with task bars and progress indicators. Start and duration are fractions 0.0-1.0 of the timeline width. Progress is a fraction 0.0-1.0.",
            "fields": ["title", "gantt_tasks", "gantt_phases (optional list of phase labels)"],
            "example": {
                "slide_type": "gantt",
                "title": "Project Timeline",
                "gantt_tasks": [
                    {"label": "Discovery", "start": 0.0, "duration": 0.2, "progress": 1.0},
                    {"label": "Design", "start": 0.15, "duration": 0.3, "progress": 0.6},
                    {"label": "Build", "start": 0.35, "duration": 0.4, "progress": 0.1},
                ],
            },
        },
    }
    return json.dumps(templates, indent=2)


@mcp.tool()
def get_brand_presets() -> str:
    """List available brand configuration presets.

    Returns:
        JSON with preset names and their key properties.
    """
    result = {}
    for name, config in PRESETS.items():
        result[name] = {
            "name": config.name,
            "tone": config.tone.value,
            "primary_color": config.colors.primary,
            "secondary_color": config.colors.secondary,
            "accent_color": config.colors.accent,
            "heading_font": config.fonts.heading_font,
            "body_font": config.fonts.body_font,
        }
    return json.dumps(result, indent=2)


@mcp.tool()
def get_deck_types() -> str:
    """List all available deck archetypes with descriptions and slot sequences.

    Each archetype defines a recommended slide structure for a specific
    presentation type (pitch deck, board update, sales deck, etc.).
    Pass the archetype name as the deck_type parameter to plan_presentation
    or generate_presentation for intelligent slide selection.

    Returns:
        JSON with archetype names, descriptions, slide sequences, and limits.
    """
    result = {}
    for name, arch in ARCHETYPES.items():
        result[name] = {
            "display_name": arch.display_name,
            "description": arch.description,
            "min_slides": arch.min_slides,
            "max_slides": arch.max_slides,
            "slot_sequence": [
                {"slide_type": slot.slide_type.value, "title_template": slot.title_template, "required": slot.required}
                for slot in arch.slots
            ],
        }
    return json.dumps(result, indent=2)


# ---------------------------------------------------------------------------
# Element editing tools
# ---------------------------------------------------------------------------


@mcp.tool()
def list_slide_elements(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
) -> str:
    """List all shapes on a slide with their positions, sizes, text, and styling.

    Use this to discover what elements exist on a slide before modifying them.
    Each element includes an index that can be used with modify_element,
    align_elements, and group_elements.

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based slide index.

    Returns:
        JSON array of element info objects.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import list_elements

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]
        elements = list_elements(slide)
        return json.dumps(
            [e.model_dump(exclude_none=True) for e in elements],
            indent=2,
        )
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("list_slide_elements", e)
        return ""


@mcp.tool()
def modify_element(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    locator: ElementLocator,
    modification: ElementModification,
) -> str:
    """Modify a single shape on a slide — change position, size, text, font, fill, or border.

    Only fields you provide in the modification are changed; everything
    else stays the same.

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based slide index.
        locator: How to find the target shape. Provide exactly one of:
            - index: 0-based shape index from list_slide_elements (fastest)
            - name: PowerPoint shape name (if set)
            - text_match: case-insensitive substring search in shape text
            Priority order if multiple provided: index > name > text_match.
        modification: Properties to change (only non-null fields are applied).

    Returns:
        JSON with the updated element info.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import modify_element as _modify

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]
        result = _modify(slide, locator, modification)
        prs.save(file_path)
        return json.dumps(result.model_dump(exclude_none=True), indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("modify_element", e)
        return ""


@mcp.tool()
def add_element(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    element_type: Literal["textbox", "shape", "image"],
    left: float,
    top: float,
    width: Optional[float] = None,
    height: Optional[float] = None,
    text: Optional[str] = None,
    shape_type: Optional[str] = None,
    image_path: Optional[str] = None,
    font_name: Optional[str] = None,
    font_size: Optional[float] = None,
    font_bold: Optional[bool] = None,
    font_color: Optional[str] = None,
    fill_color: Optional[str] = None,
    border_color: Optional[str] = None,
    border_width: Optional[float] = None,
    text_alignment: Optional[str] = None,
) -> str:
    """Add a new element (textbox, shape, or image) to a slide.

    For textbox: provide text and optional font/fill styling.
    For shape: provide shape_type (rectangle, circle, etc.) and optional text/fill/border.
    For image: provide image_path. Width/height are optional (auto-sized if omitted).

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based slide index.
        element_type: Type of element to add — "textbox", "shape", or "image".
        left: Left edge position in inches.
        top: Top edge position in inches.
        width: Width in inches (required for textbox/shape, optional for image).
        height: Height in inches (required for textbox/shape, optional for image).
        text: Text content (textbox/shape).
        shape_type: Shape type for "shape" elements (e.g. "rectangle", "circle").
        image_path: File path for "image" elements.
        font_name: Font family name.
        font_size: Font size in points.
        font_bold: Bold flag.
        font_color: Font hex color.
        fill_color: Shape fill hex color.
        border_color: Border hex color.
        border_width: Border width in points.
        text_alignment: Text alignment (left, center, right, justify).

    Returns:
        JSON with the new element info.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import add_image as _add_image
        from pptx_mcp.engine.editor import add_shape as _add_shape
        from pptx_mcp.engine.editor import add_textbox as _add_textbox

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]

        if element_type == "textbox":
            if width is None or height is None:
                raise ValueError("textbox requires width and height")
            result = _add_textbox(
                slide, left, top, width, height,
                text=text or "",
                font_name=font_name,
                font_size=font_size,
                font_bold=font_bold,
                font_color=font_color,
                fill_color=fill_color,
                text_alignment=text_alignment,
            )
        elif element_type == "shape":
            if width is None or height is None:
                raise ValueError("shape requires width and height")
            if not shape_type:
                raise ValueError("shape requires shape_type")
            result = _add_shape(
                slide, shape_type, left, top, width, height,
                text=text,
                fill_color=fill_color,
                border_color=border_color,
                border_width=border_width,
            )
        elif element_type == "image":
            if not image_path:
                raise ValueError("image requires image_path")
            result = _add_image(slide, image_path, left, top, width, height)
        else:
            raise ValueError(f"Invalid element_type '{element_type}'. Use: textbox, shape, image")

        prs.save(file_path)
        return json.dumps(result.model_dump(exclude_none=True), indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("add_element", e)
        return ""


@mcp.tool()
def align_elements(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    locators: Annotated[list[ElementLocator], Field(max_length=100)],
    alignment: AlignmentType,
) -> str:
    """Align or evenly distribute multiple elements on a slide.

    Provide 2+ element locators and an alignment type. Elements can be
    aligned to edges (left, right, top, bottom), centers (center, middle),
    or evenly distributed (distribute_h, distribute_v).

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based slide index.
        locators: List of element locators (at least 2).
        alignment: Alignment type (left, center, right, top, middle, bottom, distribute_h, distribute_v).

    Returns:
        JSON array of updated element info objects.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import align_elements as _align

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]
        results = _align(slide, locators, alignment)
        prs.save(file_path)
        return json.dumps(
            [e.model_dump(exclude_none=True) for e in results],
            indent=2,
        )
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("align_elements", e)
        return ""


@mcp.tool()
def group_elements(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    locators: Annotated[list[ElementLocator], Field(max_length=100)],
) -> str:
    """Group multiple shapes into a single group shape.

    After grouping, shape indices change. Call list_slide_elements again
    to see the updated layout.

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based slide index.
        locators: List of element locators (at least 2).

    Returns:
        JSON with the group element info.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import group_elements as _group

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]
        result = _group(slide, locators)
        prs.save(file_path)
        return json.dumps(result.model_dump(exclude_none=True), indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("group_elements", e)
        return ""


@mcp.tool()
def validate_slide_layout(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    brand_preset: BrandPreset = None,
) -> str:
    """Run spatial validation on a single slide to check for layout issues.

    Checks for: element overlaps, out-of-bounds elements, tight spacing,
    and text overflow. Use after adding or modifying elements to verify
    the layout is clean.

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based slide index.
        brand_preset: Optional brand preset for context.

    Returns:
        JSON with list of spatial issues (empty array = clean layout).
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.review.spatial import SPATIAL_RULES

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]
        brand = _resolve_brand(brand_preset=brand_preset)

        slide_w = round(prs.slide_width / 914400, 3) if prs.slide_width else 13.333
        slide_h = round(prs.slide_height / 914400, 3) if prs.slide_height else 7.5

        all_issues = []
        for rule in SPATIAL_RULES:
            kwargs = {}
            if rule.__name__ == "check_element_bounds":
                kwargs = {"slide_width": slide_w, "slide_height": slide_h}
            all_issues.extend(rule(slide, slide_index, brand, **kwargs))

        return json.dumps({
            "slide_index": slide_index,
            "issue_count": len(all_issues),
            "issues": [issue.model_dump(exclude_none=True) for issue in all_issues],
        }, indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("validate_slide_layout", e)
        return ""


@mcp.tool()
def generate_brand_guide(
    output_path: str,
    brand_preset: BrandPreset = None,
    brand_config_path: Optional[str] = None,
) -> str:
    """Generate a brand guide PPTX showcasing colors, fonts, and layout rules.

    Creates a 13-slide deck that demonstrates the brand's visual identity:
    color palette, typography hierarchy, spacing rules, and sample slides
    (dashboard, cards, chart, table, process) rendered with the brand styling.

    Args:
        output_path: File path where the .pptx will be saved (must end in .pptx).
        brand_preset: Optional brand preset name.
        brand_config_path: Optional path to a brand config YAML/JSON file.

    Returns:
        JSON with status, output path, and slide count.
    """
    try:
        validate_output_path(output_path)
        brand = _resolve_brand(brand_config_path, brand_preset)

        from pathlib import Path

        from pptx_mcp.engine.brand_guide import generate_brand_guide as _generate

        result_path = _generate(brand, Path(output_path))
        return json.dumps({
            "status": "success",
            "output_path": str(result_path),
            "slide_count": 13,
            "brand_name": brand.name,
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("generate_brand_guide", e)
        return ""


@mcp.tool()
def parse_brand_guide(
    markdown: str,
    save_path: Optional[str] = None,
) -> str:
    """Parse a markdown or YAML brand guide into a BrandConfig.

    Accepts structured markdown with sections like ``## Colors``,
    ``## Typography``, ``## Shape Style``, ``## Spacing``,
    ``## Header/Footer``, and ``## Logo``. Also auto-detects YAML input
    when the text does not start with ``#``.

    Missing sections use sensible defaults. Use this to convert a
    human-authored brand guide into a machine-readable config that can
    be passed to other tools.

    Args:
        markdown: Raw markdown or YAML text describing the brand.
        save_path: Optional file path (.yaml or .json) to save the
            parsed config for reuse.

    Returns:
        JSON representation of the parsed BrandConfig.
    """
    try:
        brand = _parse_brand_guide_impl(markdown)

        if save_path:
            p = validate_save_path(save_path)
            data = json.loads(brand.model_dump_json())
            if p.suffix in (".yaml", ".yml"):
                import yaml

                p.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False), encoding="utf-8")
            else:
                p.write_text(json.dumps(data, indent=2), encoding="utf-8")

            return json.dumps({
                "status": "success",
                "save_path": str(p),
                "brand": data,
            }, indent=2)

        return brand.model_dump_json(indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("parse_brand_guide", e)
        return ""


@mcp.tool()
def extract_brand(
    file_path: str,
    save_path: Optional[str] = None,
) -> str:
    """Extract a BrandConfig from an existing PPTX by analyzing its visual properties.

    Scans all slides and shapes to collect font families, sizes, fill colors,
    text colors, and border colors. Aggregates by frequency to determine the
    brand's primary/secondary/accent colors and heading/body fonts.

    Use this to reverse-engineer a brand configuration from any deck, then
    apply it to new presentations via brand_config_path or brand_preset.

    Args:
        file_path: Path to the .pptx file to analyze.
        save_path: Optional file path (.yaml or .json) to save the extracted config.

    Returns:
        JSON representation of the extracted BrandConfig.
    """
    try:
        validate_pptx_input(file_path)
        brand = _extract_brand_impl(file_path)

        if save_path:
            p = validate_save_path(save_path)
            data = json.loads(brand.model_dump_json())
            if p.suffix in (".yaml", ".yml"):
                import yaml

                p.write_text(
                    yaml.dump(data, default_flow_style=False, sort_keys=False),
                    encoding="utf-8",
                )
            else:
                p.write_text(json.dumps(data, indent=2), encoding="utf-8")

            return json.dumps({
                "status": "success",
                "save_path": str(p),
                "brand": data,
            }, indent=2)

        return brand.model_dump_json(indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("extract_brand", e)
        return ""


# ---------------------------------------------------------------------------
# Phase 2 editing power tools (v0.8.0)
# ---------------------------------------------------------------------------


@mcp.tool()
def find_and_replace(
    file_path: str,
    find_text: str,
    replace_text: str,
    case_sensitive: bool = True,
    slide_indices: Optional[list[int]] = None,
) -> str:
    """Find and replace text across all slides (or specific slides) in a presentation.

    Searches through all text in all shapes. Returns a summary of changes made.

    Args:
        file_path: Path to the .pptx file.
        find_text: Text string to search for.
        replace_text: Replacement text string.
        case_sensitive: Whether the search is case-sensitive (default True).
        slide_indices: Optional list of 0-based slide indices to limit the search.

    Returns:
        JSON with list of replacements made per shape.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import find_and_replace as _find_replace

        prs = Presentation(file_path)
        results = _find_replace(prs, find_text, replace_text, case_sensitive, slide_indices)
        prs.save(file_path)
        return json.dumps({
            "status": "success",
            "total_replacements": sum(r.replacements for r in results),
            "results": [r.model_dump() for r in results],
        }, indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("find_and_replace", e)
        return ""


@mcp.tool()
def batch_modify_elements(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    modifications: list[dict],
) -> str:
    """Apply multiple modifications to different elements on a single slide in one call.

    Each modification is a dict with 'locator' (ElementLocator) and 'modification'
    (ElementModification) keys.

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based slide index.
        modifications: List of {locator: ..., modification: ...} dicts.

    Returns:
        JSON array of updated element info objects.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import batch_modify as _batch
        from pptx_mcp.models.editor import BatchModification

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]

        batch_mods = [BatchModification(**m) for m in modifications]
        results = _batch(slide, batch_mods)
        prs.save(file_path)
        return json.dumps(
            [e.model_dump(exclude_none=True) for e in results],
            indent=2,
        )
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("batch_modify_elements", e)
        return ""


@mcp.tool()
def delete_element(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    locator: ElementLocator,
) -> str:
    """Delete a single element from a slide.

    After deletion, shape indices change. Call list_slide_elements to
    see the updated layout.

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based slide index.
        locator: Identifies the element to delete (by index, name, or text_match).

    Returns:
        JSON with status confirmation.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import delete_element as _delete

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]
        _delete(slide, locator)
        prs.save(file_path)
        return json.dumps({"status": "success", "message": "Element deleted"})
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("delete_element", e)
        return ""


@mcp.tool()
def copy_element(
    file_path: str,
    source_slide_index: Annotated[int, Field(ge=0)],
    locator: ElementLocator,
    target_slide_index: Annotated[int, Field(ge=0)],
    offset_x: float = 0.3,
    offset_y: float = 0.3,
) -> str:
    """Copy an element from one slide to another (or the same slide).

    Creates a deep copy of the shape with a position offset to avoid
    exact overlap.

    Args:
        file_path: Path to the .pptx file.
        source_slide_index: 0-based index of the source slide.
        locator: Identifies the element to copy.
        target_slide_index: 0-based index of the destination slide.
        offset_x: Horizontal offset in inches (default 0.3).
        offset_y: Vertical offset in inches (default 0.3).

    Returns:
        JSON with the copied element info.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import copy_element as _copy

        prs = Presentation(file_path)
        result = _copy(prs, source_slide_index, locator, target_slide_index, offset_x, offset_y)
        prs.save(file_path)
        return json.dumps(result.model_dump(exclude_none=True), indent=2)
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("copy_element", e)
        return ""


@mcp.tool()
def reorder_slides(
    file_path: str,
    new_order: list[int],
) -> str:
    """Rearrange slides in a presentation to a new order.

    Provide a complete permutation of slide indices. For example, to reverse
    a 3-slide deck: new_order=[2, 1, 0].

    Args:
        file_path: Path to the .pptx file.
        new_order: List of 0-based slide indices in the desired order.
            Must include every index exactly once.

    Returns:
        JSON with status and new slide count.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import reorder_slides as _reorder

        prs = Presentation(file_path)
        _reorder(prs, new_order)
        prs.save(file_path)
        return json.dumps({
            "status": "success",
            "slide_count": len(prs.slides),
            "new_order": new_order,
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("reorder_slides", e)
        return ""


@mcp.tool()
def ungroup_elements(
    file_path: str,
    slide_index: Annotated[int, Field(ge=0)],
    locator: ElementLocator,
) -> str:
    """Dissolve a group shape, moving its children to the slide level.

    After ungrouping, shape indices change. Call list_slide_elements to
    see the updated layout.

    Args:
        file_path: Path to the .pptx file.
        slide_index: 0-based slide index.
        locator: Identifies the group element to dissolve.

    Returns:
        JSON array of the ungrouped child element info objects.
    """
    try:
        validate_pptx_input(file_path)
        from pptx import Presentation

        from pptx_mcp.engine.editor import ungroup_elements as _ungroup

        prs = Presentation(file_path)
        if slide_index < 0 or slide_index >= len(prs.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(prs.slides) - 1})"
            )
        slide = prs.slides[slide_index]
        results = _ungroup(slide, locator)
        prs.save(file_path)
        return json.dumps(
            [e.model_dump(exclude_none=True) for e in results],
            indent=2,
        )
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("ungroup_elements", e)
        return ""


@mcp.tool()
def update_plan_slide(
    plan_id: str,
    slide_index: Annotated[int, Field(ge=0)],
    title: Optional[str] = None,
    subtitle: Optional[str] = None,
    body: Optional[str] = None,
    bullets: Optional[list[str]] = None,
    speaker_notes: Optional[str] = None,
) -> str:
    """Update a single slide in a cached plan before rendering.

    Allows partial updates to a planned slide without re-planning.
    Only non-None fields are applied.

    Args:
        plan_id: The plan_id returned by plan_presentation.
        slide_index: 0-based index of the slide to update.
        title: New title text.
        subtitle: New subtitle text.
        body: New body text.
        bullets: New bullet list.
        speaker_notes: New speaker notes.

    Returns:
        JSON with the updated slide summary.
    """
    try:
        entry = _get_cached_plan(plan_id)
        defn = entry.definition
        if slide_index < 0 or slide_index >= len(defn.slides):
            raise ValueError(
                f"slide_index {slide_index} out of range (0-{len(defn.slides) - 1})"
            )
        slide_def = defn.slides[slide_index]
        if title is not None:
            slide_def.title = title
        if subtitle is not None:
            slide_def.subtitle = subtitle
        if body is not None:
            slide_def.body = body
        if bullets is not None:
            slide_def.bullets = bullets
        if speaker_notes is not None:
            slide_def.speaker_notes = speaker_notes

        return json.dumps({
            "status": "success",
            "plan_id": plan_id,
            "slide_index": slide_index,
            "slide_type": slide_def.slide_type.value,
            "title": slide_def.title,
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("update_plan_slide", e)
        return ""


@mcp.tool()
def create_checkpoint(
    file_path: str,
) -> str:
    """Create a checkpoint copy of a PPTX file for easy rollback.

    Saves a copy as ``<file_path>.checkpoint.pptx`` next to the original.
    Use restore_checkpoint to revert.

    Args:
        file_path: Path to the .pptx file to checkpoint.

    Returns:
        JSON with the checkpoint file path.
    """
    try:
        validate_pptx_input(file_path)
        import shutil
        from pathlib import Path

        src = Path(file_path)
        checkpoint_path = src.with_suffix(".checkpoint.pptx")
        shutil.copy2(str(src), str(checkpoint_path))
        return json.dumps({
            "status": "success",
            "checkpoint_path": str(checkpoint_path),
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("create_checkpoint", e)
        return ""


@mcp.tool()
def restore_checkpoint(
    file_path: str,
) -> str:
    """Restore a PPTX file from its checkpoint copy.

    Copies ``<file_path>.checkpoint.pptx`` back over the original file.
    The checkpoint file must exist (created by create_checkpoint).

    Args:
        file_path: Path to the .pptx file to restore.

    Returns:
        JSON with status confirmation.
    """
    try:
        import shutil
        from pathlib import Path

        src = Path(file_path)
        checkpoint_path = src.with_suffix(".checkpoint.pptx")
        if not checkpoint_path.exists():
            raise ValueError(f"No checkpoint found at {checkpoint_path}")
        shutil.copy2(str(checkpoint_path), str(src))
        return json.dumps({
            "status": "success",
            "restored_from": str(checkpoint_path),
        })
    except ToolError:
        raise
    except Exception as e:
        _raise_tool_error("restore_checkpoint", e)
        return ""


# ---------------------------------------------------------------------------
# Prompts — pre-built templates for common workflows
# ---------------------------------------------------------------------------


@mcp.prompt()
def choose_workflow(num_slides: int = 10) -> str:
    """Recommend the best workflow based on deck size.

    Returns natural-language guidance on which tools to use for the
    given number of slides to minimize token usage.
    """
    if num_slides <= 8:
        return (
            f"For a {num_slides}-slide deck, use generate_presentation.\n"
            "This single-step workflow plans, optimizes, and renders in one call.\n"
            "Token cost: ~200 tokens total (output_path + status).\n"
            "Hard cap: 8 slides max. For 9+ slides, use the plan workflow."
        )
    elif num_slides <= 20:
        return (
            f"For a {num_slides}-slide deck, use the plan workflow:\n"
            "1. plan_presentation -> returns plan_id + slide summary (~500 tokens)\n"
            "2. Optionally get_plan_slide to inspect slides (~200 tokens each)\n"
            "3. Optionally update_plan_slide to modify before rendering\n"
            "4. create_from_plan with plan_id -> renders .pptx (~80 tokens)\n\n"
            "Total: ~600-800 tokens. The full definition stays server-side.\n"
            "Do NOT use generate_presentation — it is hard-capped at 8 slides."
        )
    else:
        return (
            f"For a {num_slides}-slide deck, use the plan workflow with targeted edits:\n"
            "1. plan_presentation -> returns plan_id + slide summary (~500 tokens)\n"
            "2. create_from_plan with plan_id -> renders .pptx (~80 tokens)\n"
            "3. Use update_slide / add_slide / delete_slide for targeted edits\n\n"
            "Total: ~600-800 tokens + ~100 per targeted edit.\n"
            "The plan cache keeps the full definition server-side — never\n"
            "transfer a full PresentationDefinition through the conversation."
        )


@mcp.prompt()
def create_pitch_deck(topic: str, company: str = "", audience: str = "executive") -> str:
    """Generate a complete pitch deck for a topic.

    Creates a polished pitch presentation with problem, solution, traction,
    team, and ask slides using the pitch_deck archetype.
    """
    return (
        f"Create a professional pitch deck about: {topic}\n"
        f"Company: {company or '(not specified)'}\n"
        f"Target audience: {audience}\n\n"
        "Steps:\n"
        f'1. Call plan_presentation with topic="{topic}", '
        f'audience="{audience}", num_slides=10, deck_type="pitch_deck"'
        f'{f", company={company!r}" if company else ""}\n'
        "2. Review the plan summary and slide list\n"
        '3. Call create_from_plan with the plan_id and your desired output_path\n'
        "4. Confirm the output path and slide count\n\n"
        "The deck uses the pitch_deck archetype: title, problem, solution, "
        "traction, product, metrics, differentiators, market, team, and closing."
    )


@mcp.prompt()
def review_and_improve(file_path: str) -> str:
    """Review an existing presentation and apply automatic improvements.

    Runs the full review-iterate-optimize pipeline on a deck.
    """
    return (
        f"Review and improve the presentation at: {file_path}\n\n"
        "Steps:\n"
        f'1. Call read_presentation with file_path="{file_path}" to see the current structure\n'
        f'2. Call review_presentation with file_path="{file_path}" to get design feedback\n'
        f'3. Call improve_presentation with file_path="{file_path}" to apply fixes\n'
        "4. Report the before/after scores and changes made"
    )


@mcp.prompt()
def add_process_flow(file_path: str, num_steps: int = 4) -> str:
    """Add a process flow slide to an existing presentation.

    Creates a chevron process flow or step cards slide.
    """
    return (
        f"Add a process flow slide to: {file_path}\n\n"
        f"Create a slide with {num_steps} steps using the 'process' or 'step_cards' slide type.\n"
        "Each step needs a title and brief description.\n\n"
        "Use add_slide with a slide definition containing:\n"
        '- slide_type: "process" (for chevron flow) or "step_cards" (for numbered cards)\n'
        "- title: descriptive title for the slide\n"
        f'- steps: array of {num_steps} objects with title, description, and status fields\n'
        '- active_step: 0-based index of the current step (optional)\n'
    )


@mcp.prompt()
def create_board_update(topic: str, company: str = "", quarter: str = "") -> str:
    """Generate a board update deck with financials, strategy, and operational review."""
    return (
        f"Create a board update presentation about: {topic}\n"
        f"Company: {company or '(not specified)'}\n"
        f"Quarter: {quarter or '(current)'}\n\n"
        "Steps:\n"
        f'1. Call plan_presentation with topic="{topic}", '
        f'audience="executive", num_slides=12, deck_type="board_deck"'
        f'{f", company={company!r}" if company else ""}\n'
        "2. Review the plan summary and slide list\n"
        '3. Call create_from_plan with the plan_id and your desired output_path\n'
        "4. Confirm the output path and slide count\n\n"
        "The deck uses the board_deck archetype: title, agenda, financial dashboard, "
        "revenue charts, strategic initiatives, SWOT analysis, operational highlights, "
        "risks, and next steps."
    )


@mcp.prompt()
def create_sales_deck(topic: str, company: str = "") -> str:
    """Generate a customer-facing sales presentation with value proposition and social proof."""
    return (
        f"Create a sales presentation about: {topic}\n"
        f"Company: {company or '(not specified)'}\n\n"
        "Steps:\n"
        f'1. Call plan_presentation with topic="{topic}", '
        f'audience="general", num_slides=10, deck_type="sales_deck"'
        f'{f", company={company!r}" if company else ""}\n'
        "2. Review the plan summary and slide list\n"
        '3. Call create_from_plan with the plan_id and your desired output_path\n'
        "4. Confirm the output path and slide count\n\n"
        "The deck uses the sales_deck archetype: title, challenge, approach, "
        "comparison, results dashboard, customer quote, pricing, and CTA."
    )


@mcp.prompt()
def design_guidance() -> str:
    """Professional slide design principles for creating polished presentations.

    Covers layout, typography, color, content density, whitespace, and the
    recommended verify-and-adjust workflow using element editing tools.
    """
    return (
        "# Slide Design Principles\n\n"
        "## Layout\n"
        "- Maintain 0.5\" margins on all sides\n"
        "- Use a grid system — align elements to consistent vertical and horizontal lines\n"
        "- Apply the rule of thirds: place key content at intersection points\n"
        "- Minimum 0.15\" spacing between adjacent elements\n"
        "- Group related elements with proximity; separate unrelated groups with whitespace\n\n"
        "## Typography\n"
        "- Title: 28-36pt, bold, brand primary color\n"
        "- Body: 14-18pt, regular weight, dark gray or black\n"
        "- Caption/footnote: 10-12pt, lighter color\n"
        "- Use at most 2 font families (one for headings, one for body)\n"
        "- Avoid all-caps for body text; reserve for short labels only\n\n"
        "## Color\n"
        "- Use brand primary color for titles and key accents\n"
        "- Limit to 1-2 accent colors beyond the primary palette\n"
        "- Ensure sufficient contrast (dark text on light backgrounds, or vice versa)\n"
        "- Use color consistently — same meaning across all slides\n\n"
        "## Content Density\n"
        "- One key message per slide\n"
        "- 6×6 rule: max 6 bullets, max 6 words per bullet\n"
        "- Target 75 words or fewer per slide\n"
        "- If a slide feels crowded, split it into two\n\n"
        "## Whitespace\n"
        "- Whitespace is a design element, not wasted space\n"
        "- More whitespace around important elements draws attention\n"
        "- Don't fill every inch of the slide — let content breathe\n\n"
        "## Verify-and-Adjust Workflow\n"
        "After generating or editing slides:\n"
        "1. `list_slide_elements` — see all shapes with positions and sizes\n"
        "2. `validate_slide_layout` — check for overlaps, bounds, and spacing issues\n"
        "3. `modify_element` — fix any flagged issues (reposition, resize)\n"
        "4. `align_elements` — align related elements for visual consistency\n"
        "5. `validate_slide_layout` — confirm the layout is clean\n"
    )


@mcp.prompt()
def create_all_hands(topic: str, company: str = "") -> str:
    """Generate an all-hands meeting deck with achievements, metrics, and team recognition."""
    return (
        f"Create an all-hands meeting presentation about: {topic}\n"
        f"Company: {company or '(not specified)'}\n\n"
        "Steps:\n"
        f'1. Call plan_presentation with topic="{topic}", '
        f'audience="general", num_slides=10, deck_type="all_hands"'
        f'{f", company={company!r}" if company else ""}\n'
        "2. Review the plan summary and slide list\n"
        '3. Call create_from_plan with the plan_id and your desired output_path\n'
        "4. Confirm the output path and slide count\n\n"
        "The deck uses the all_hands archetype: title, agenda, highlights, "
        "key metrics, roadmap, new team members, and closing."
    )


@mcp.prompt()
def create_consulting_deck(
    deck_type: str,
    topic: str,
    company: str = "",
    audience: str = "executive",
) -> str:
    """Generate a consulting-grade deck from an archetype.

    Lists available consulting archetypes and generates a professional
    presentation using the consulting brand preset with slide numbers
    and confidential marking.
    """
    consulting_types = [
        "assessment", "opportunity_assessment", "strategy_training",
        "template_guide", "executive_overview", "analysis_deck",
        "cost_breakdown", "architecture_review", "analytics_dashboard",
        "strategy_deck", "due_diligence", "market_analysis",
        "org_design", "competitive_assessment", "transformation_roadmap",
    ]
    type_list = ", ".join(consulting_types)
    return (
        f"Create a consulting-grade presentation about: {topic}\n"
        f"Deck type: {deck_type}\n"
        f"Company: {company or '(not specified)'}\n"
        f"Target audience: {audience}\n\n"
        f"Available consulting archetypes: {type_list}\n\n"
        "Steps:\n"
        f'1. Call plan_presentation with topic="{topic}", '
        f'audience="{audience}", num_slides=12, deck_type="{deck_type}"'
        f'{f", company={company!r}" if company else ""}\n'
        "2. Review the plan summary and slide list\n"
        f'3. Call create_from_plan with the plan_id, your desired output_path, '
        f'and brand_preset="consulting"\n'
        "4. Confirm the output path and slide count\n\n"
        "The consulting preset includes slide numbers, date footers, "
        "confidential marking, and professional card styling with subtle shadows."
    )


# ---------------------------------------------------------------------------
# Resources
# ---------------------------------------------------------------------------


@mcp.resource("pptx://schema/slide-definition")
def slide_definition_schema() -> str:
    """JSON schema for SlideDefinition."""
    return json.dumps(SlideDefinition.model_json_schema(), indent=2)


@mcp.resource("pptx://schema/presentation-definition")
def presentation_definition_schema() -> str:
    """JSON schema for PresentationDefinition."""
    return json.dumps(PresentationDefinition.model_json_schema(), indent=2)


# ---------------------------------------------------------------------------
# Entrypoint
# ---------------------------------------------------------------------------


def run_server() -> None:
    """Start the MCP server."""
    logger.info("Starting PPTX MCP server")
    mcp.run()


if __name__ == "__main__":
    run_server()
