#!/usr/bin/env python3
"""
PPT Master - Template Preview PPTX Exporter

Export every SVG prototype in a template workspace as one structured review deck.

Usage:
    python3 scripts/template_preview_pptx.py <template_workspace> [-o output.pptx]

Examples:
    python3 scripts/template_preview_pptx.py projects/my_template
    python3 scripts/template_preview_pptx.py templates/decks/my_template -o review.pptx
    python3 scripts/template_preview_pptx.py templates/decks/legacy --visual-only

Dependencies:
    python-pptx
"""

from __future__ import annotations

import argparse
import math
import re
import statistics
import sys
from pathlib import Path
from xml.etree import ElementTree as ET

from console_encoding import configure_utf8_stdio


configure_utf8_stdio()

from pptx import Presentation  # noqa: E402

from svg_to_pptx.drawingml.theme_fonts import (  # noqa: E402
    MasterTextStyleSpec,
)
from svg_to_pptx.drawingml.utils import font_px_to_hpt  # noqa: E402
from svg_to_pptx.pptx_package.builder import (  # noqa: E402
    create_pptx_with_native_svg,
)


_FRONTMATTER_ID_RE = re.compile(
    r"^(?:template_id|deck_id|layout_id)\s*:\s*(.+?)\s*$",
    re.MULTILINE,
)
_REPLICATION_MODE_RE = re.compile(
    r"^replication_mode\s*:\s*(standard|fidelity|mirror)\s*$",
    re.MULTILINE,
)
_FONT_SIZE_RE = re.compile(r"^([0-9]+(?:\.[0-9]+)?)(?:px)?$")
_FILENAME_UNSAFE_RE = re.compile(r"[\\/:*?\"<>|\x00-\x1f]+")
_TITLE_PLACEHOLDERS = frozenset({"title", "subtitle"})
_BODY_PLACEHOLDERS = frozenset({
    "body",
    "date",
    "footer",
    "slide-number",
})
_DEFAULT_TITLE_PX = 40.0
_DEFAULT_BODY_PX = 24.0


def _resolve_workspace(path: Path) -> tuple[Path, Path]:
    """Resolve one workspace root and its canonical template-source directory."""
    candidate = path.expanduser().resolve()
    nested_spec = candidate / "templates" / "design_spec.md"
    if nested_spec.is_file():
        return candidate, candidate / "templates"

    direct_spec = candidate / "design_spec.md"
    if direct_spec.is_file():
        if candidate.name == "templates" and (candidate.parent / "exports").is_dir():
            return candidate.parent, candidate
        return candidate, candidate

    raise ValueError(
        "template workspace must contain templates/design_spec.md "
        "(current structure) or design_spec.md (legacy flat package)"
    )


def _template_id(spec_path: Path, workspace: Path) -> str:
    """Read a portable template id, falling back to the workspace directory name."""
    text = spec_path.read_text(encoding="utf-8")
    match = _FRONTMATTER_ID_RE.search(text)
    raw = match.group(1).strip().strip("'\"") if match else workspace.name
    safe = _FILENAME_UNSAFE_RE.sub("_", raw).strip(" ._")
    return safe or "template"


def _replication_mode(spec_path: Path) -> str:
    """Read the template replication mode, defaulting legacy packages to standard."""
    text = spec_path.read_text(encoding="utf-8")
    match = _REPLICATION_MODE_RE.search(text)
    return match.group(1) if match else "standard"


def _style_property(style: str, name: str) -> str | None:
    """Return one inline CSS declaration value."""
    for declaration in style.split(";"):
        key, separator, value = declaration.partition(":")
        if separator and key.strip().lower() == name:
            return value.strip()
    return None


def _font_size_px(element: ET.Element) -> float | None:
    """Read one finite positive SVG font size in px."""
    raw = element.get("font-size")
    if raw is None:
        raw = _style_property(element.get("style", ""), "font-size")
    if raw is None:
        return None
    match = _FONT_SIZE_RE.fullmatch(raw.strip())
    if match is None:
        return None
    value = float(match.group(1))
    return value if math.isfinite(value) and value > 0 else None


def _carrier_sizes(svg_files: list[Path]) -> tuple[list[float], list[float]]:
    """Collect authored title/body sizes from semantic placeholder carriers."""
    title_sizes: list[float] = []
    body_sizes: list[float] = []
    for svg_path in svg_files:
        root = ET.parse(svg_path).getroot()
        for slot in root.iter():
            placeholder = slot.get("data-pptx-placeholder")
            if placeholder not in _TITLE_PLACEHOLDERS | _BODY_PLACEHOLDERS:
                continue
            for carrier in slot.iter():
                if carrier.get("data-pptx-placeholder-carrier") != "true":
                    continue
                size = _font_size_px(carrier)
                if size is None:
                    continue
                target = title_sizes if placeholder in _TITLE_PLACEHOLDERS else body_sizes
                target.append(size)
    return title_sizes, body_sizes


def _master_text_style(svg_files: list[Path]) -> tuple[MasterTextStyleSpec, float, float]:
    """Build review-only Master text defaults without requiring a project lock."""
    title_sizes, body_sizes = _carrier_sizes(svg_files)
    title_px = float(statistics.median(title_sizes)) if title_sizes else _DEFAULT_TITLE_PX
    body_px = float(statistics.median(body_sizes)) if body_sizes else _DEFAULT_BODY_PX
    return (
        MasterTextStyleSpec(
            title_hpt=font_px_to_hpt(title_px),
            body_hpt=font_px_to_hpt(body_px),
        ),
        title_px,
        body_px,
    )


def _verify_output(
    output_path: Path,
    *,
    require_full_placeholder_frames: bool,
) -> tuple[int, int, int, int]:
    """Reopen the review deck and verify counts plus authored placeholder frames."""
    presentation = Presentation(str(output_path))
    master_count = len(presentation.slide_masters)
    layout_count = sum(len(master.slide_layouts) for master in presentation.slide_masters)
    placeholder_count = 0
    if require_full_placeholder_frames:
        for slide_number, slide in enumerate(presentation.slides, 1):
            layout_placeholders = {
                shape.placeholder_format.idx: shape
                for shape in slide.slide_layout.placeholders
            }
            slide_placeholders = {
                shape.placeholder_format.idx: shape
                for shape in slide.placeholders
            }
            if set(slide_placeholders) != set(layout_placeholders):
                raise ValueError(
                    f"review slide {slide_number} placeholder indexes do not match "
                    f"its Layout: {sorted(slide_placeholders)} != "
                    f"{sorted(layout_placeholders)}"
                )
            for placeholder_idx, slide_shape in slide_placeholders.items():
                layout_shape = layout_placeholders[placeholder_idx]
                if (
                    slide_shape.placeholder_format.type
                    != layout_shape.placeholder_format.type
                ):
                    raise ValueError(
                        f"review slide {slide_number} placeholder {placeholder_idx} "
                        "type does not match its Layout"
                    )
                slide_frame = (
                    slide_shape.left,
                    slide_shape.top,
                    slide_shape.width,
                    slide_shape.height,
                )
                layout_frame = (
                    layout_shape.left,
                    layout_shape.top,
                    layout_shape.width,
                    layout_shape.height,
                )
                if slide_frame != layout_frame:
                    raise ValueError(
                        f"review slide {slide_number} placeholder {placeholder_idx} "
                        f"uses a tight/local frame {slide_frame}; expected full "
                        f"Layout frame {layout_frame}"
                    )
                placeholder_count += 1
    return len(presentation.slides), master_count, layout_count, placeholder_count


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description=(
            "Export a complete template workspace as a structured PPTX review deck."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "template_workspace",
        help=(
            "Workspace containing templates/design_spec.md; legacy flat template "
            "directories are also accepted."
        ),
    )
    parser.add_argument(
        "-o",
        "--output",
        help=(
            "Output PPTX path. Default: "
            "<template_workspace>/exports/<template_id>_template_preview.pptx"
        ),
    )
    parser.add_argument(
        "--force",
        action="store_true",
        help="Replace an existing review PPTX after an intentional re-export.",
    )
    parser.add_argument(
        "--visual-only",
        action="store_true",
        help=(
            "Export a legacy SVG roster as slide-local DrawingML for visual review. "
            "This does not validate or claim a reusable Master/Layout contract."
        ),
    )
    return parser


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    try:
        workspace, template_dir = _resolve_workspace(Path(args.template_workspace))
        svg_files = sorted(template_dir.glob("*.svg"))
        if not svg_files:
            raise ValueError(f"template directory has no SVG prototypes: {template_dir}")

        spec_path = template_dir / "design_spec.md"
        template_id = _template_id(spec_path, workspace)
        replication_mode = _replication_mode(spec_path)
        use_full_placeholder_frames = (
            not args.visual_only and replication_mode != "mirror"
        )
        output_path = (
            Path(args.output).expanduser().resolve()
            if args.output
            else workspace / "exports" / f"{template_id}_template_preview.pptx"
        )
        if output_path.suffix.lower() != ".pptx":
            raise ValueError(f"output must use a .pptx extension: {output_path}")
        if output_path.exists() and not args.force:
            raise ValueError(
                f"output already exists: {output_path}; use --force to replace it"
            )
        output_path.parent.mkdir(parents=True, exist_ok=True)
        text_style: MasterTextStyleSpec | None = None
        if not args.visual_only:
            text_style, title_px, body_px = _master_text_style(svg_files)

        print("PPT Master - Template Preview PPTX Exporter")
        print(f"  Workspace: {workspace}")
        print(f"  Template source: {template_dir}")
        print(f"  SVG prototypes: {len(svg_files)}")
        if args.visual_only:
            print("  Review mode: visual-only legacy compatibility")
        elif replication_mode == "mirror":
            print("  Review placeholder frames: preserved source Slide geometry")
        else:
            print(f"  Review Master defaults: title {title_px:g}px, body {body_px:g}px")
            print("  Review placeholder frames: full Layout bounds")
        print(f"  Output: {output_path}")

        success = create_pptx_with_native_svg(
            svg_files=svg_files,
            output_path=output_path,
            canvas_format=None,
            verbose=True,
            transition=None,
            enable_notes=False,
            animation=None,
            image_optimize=False,
            native_objects=True,
            pptx_structure="flat" if args.visual_only else "structured",
            use_layout_placeholder_frames=use_full_placeholder_frames,
            master_text_style_spec=text_style,
            structure_name=template_id,
        )
        if not success or not output_path.is_file():
            print("Error: template preview export did not produce a PPTX", file=sys.stderr)
            return 1

        slide_count, master_count, layout_count, placeholder_count = _verify_output(
            output_path,
            require_full_placeholder_frames=use_full_placeholder_frames,
        )
        if slide_count != len(svg_files):
            print(
                "Error: review PPTX slide count does not match the template SVG roster "
                f"({slide_count} != {len(svg_files)})",
                file=sys.stderr,
            )
            return 1

        label = "Visual-only template preview" if args.visual_only else "Template preview"
        placeholder_status = (
            f", {placeholder_count} full-frame placeholder(s)"
            if use_full_placeholder_frames
            else ""
        )
        print(
            f"[OK] {label} verified: "
            f"{slide_count} slides, {master_count} master(s), "
            f"{layout_count} layout(s){placeholder_status}"
        )
        print(output_path)
        return 0
    except (OSError, ET.ParseError, RuntimeError, ValueError) as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
