"""unDraw illustration service — fetch and cache MIT-licensed SVG illustrations.

Fetches illustrations from the unDraw project and converts them to native
DrawingML vector shapes via svg2ooxml. Falls back gracefully when svg2ooxml
is not installed.
"""

from __future__ import annotations

import hashlib
import logging
import re
from dataclasses import dataclass, replace
from pathlib import Path
from typing import Any

from tokenmoulds._compat import HAS_HTTPX, HAS_SVG2OOXML, httpx
from tokenmoulds.settings import get_settings


logger = logging.getLogger(__name__)


class IllustrationUnavailableError(RuntimeError):
    """Raised when illustration cannot be fetched or rendered."""


def is_available() -> bool:
    """Check if the illustration service can be used (httpx + svg2ooxml installed)."""
    return HAS_HTTPX and HAS_SVG2OOXML


_SLUG_RE = re.compile(r"^[a-z][a-z0-9-]*$")
_VALID_CATEGORIES = frozenset(
    {"business", "technology", "data", "people", "creative", "abstract"}
)


@dataclass(frozen=True, slots=True)
class IllustrationConfig:
    """Registry entry for a curated illustration."""

    slug: str
    category: str
    description: str

    def __post_init__(self) -> None:
        if not _SLUG_RE.match(self.slug):
            msg = f"Invalid slug format: {self.slug!r} (must be lowercase kebab-case)"
            raise ValueError(msg)
        if self.category not in _VALID_CATEGORIES:
            msg = f"Invalid category: {self.category!r} (valid: {', '.join(sorted(_VALID_CATEGORIES))})"
            raise ValueError(msg)


# Curated catalog — 30 illustrations covering common business/tech scenarios
ILLUSTRATION_CATALOG: dict[str, IllustrationConfig] = {
    # Business
    "teamwork": IllustrationConfig(
        "co-workers", "business", "People collaborating at a desk"
    ),
    "presentation": IllustrationConfig(
        "presentation", "business", "Person giving a presentation"
    ),
    "meeting": IllustrationConfig("meeting", "business", "Group meeting discussion"),
    "agreement": IllustrationConfig("agreement", "business", "Handshake or deal"),
    "business-plan": IllustrationConfig(
        "business-plan", "business", "Planning and strategy"
    ),
    "goals": IllustrationConfig("goals", "business", "Achievement and targets"),
    # Technology
    "programming": IllustrationConfig("programming", "technology", "Developer coding"),
    "server": IllustrationConfig("server", "technology", "Server infrastructure"),
    "cloud": IllustrationConfig("cloud-hosting", "technology", "Cloud computing"),
    "security": IllustrationConfig(
        "security", "technology", "Cybersecurity lock and shield"
    ),
    "mobile-app": IllustrationConfig("mobile-app", "technology", "Mobile application"),
    "website": IllustrationConfig(
        "website-builder", "technology", "Website development"
    ),
    # Data
    "data-analysis": IllustrationConfig(
        "data-trends", "data", "Charts and data visualization"
    ),
    "dashboard": IllustrationConfig("dashboard", "data", "Analytics dashboard"),
    "report": IllustrationConfig("report", "data", "Report document"),
    "spreadsheet": IllustrationConfig("spreadsheets", "data", "Spreadsheet data"),
    "metrics": IllustrationConfig("metrics", "data", "KPI metrics display"),
    # People
    "team": IllustrationConfig("team-spirit", "people", "Team working together"),
    "remote-work": IllustrationConfig(
        "working-remotely", "people", "Remote worker at home"
    ),
    "brainstorming": IllustrationConfig(
        "brainstorming", "people", "Creative thinking session"
    ),
    "interview": IllustrationConfig("interview", "people", "Job interview"),
    "onboarding": IllustrationConfig("welcome", "people", "New member onboarding"),
    # Creative
    "creative": IllustrationConfig("designer", "creative", "Designer at work"),
    "design": IllustrationConfig(
        "design-tools", "creative", "Design tools and palette"
    ),
    "content": IllustrationConfig("content-creator", "creative", "Content creation"),
    # Abstract
    "growth": IllustrationConfig("growth-analytics", "abstract", "Upward growth trend"),
    "success": IllustrationConfig(
        "success-factors", "abstract", "Achievement celebration"
    ),
    "innovation": IllustrationConfig("innovative", "abstract", "Light bulb innovation"),
    "connection": IllustrationConfig(
        "connected-world", "abstract", "Global connectivity"
    ),
    "solution": IllustrationConfig("solution-mindset", "abstract", "Problem solving"),
}


def _cache_dir() -> Path:
    return get_settings().cache_subdir("illustrations")


# Default unDraw color that gets mapped to theme slots
_UNDRAW_DEFAULT_COLOR = "6C63FF"


ColorToThemeMap = dict[str, str]  # e.g. {"6C63FF": "accent1"}

DEFAULT_COLOR_MAP: ColorToThemeMap = {
    _UNDRAW_DEFAULT_COLOR: "accent1",
}


def _inject_theme_colors(
    elements: list[Any],
    color_map: ColorToThemeMap,
) -> None:
    """Walk IR scene elements and set theme_color on matching paints."""
    from svg2ooxml.ir.paint import SolidPaint

    for i, el in enumerate(elements):
        # Handle groups recursively
        if hasattr(el, "children"):
            _inject_theme_colors(el.children, color_map)

        # Replace fill paint
        if hasattr(el, "fill") and isinstance(el.fill, SolidPaint):
            rgb = el.fill.rgb.upper()
            if rgb in color_map:
                elements[i] = replace(
                    el, fill=replace(el.fill, theme_color=color_map[rgb])
                )

        # Replace stroke paint
        if hasattr(el, "stroke") and el.stroke is not None:
            paint = el.stroke.paint if hasattr(el.stroke, "paint") else None
            if isinstance(paint, SolidPaint) and paint.rgb.upper() in color_map:
                new_paint = replace(paint, theme_color=color_map[paint.rgb.upper()])
                new_stroke = replace(el.stroke, paint=new_paint)
                elements[i] = replace(elements[i], stroke=new_stroke)


def get_illustration(name: str) -> IllustrationConfig:
    """Look up an illustration by semantic name.

    Raises:
        KeyError: If name is not in the catalog.
    """
    if name not in ILLUSTRATION_CATALOG:
        msg = f"Unknown illustration: {name!r}. Available: {', '.join(sorted(ILLUSTRATION_CATALOG))}"
        raise KeyError(msg)
    return ILLUSTRATION_CATALOG[name]


def fetch_illustration(
    name: str,
    *,
    theme_color: str = "accent1",
    color_map: ColorToThemeMap | None = None,
) -> str:
    """Fetch an illustration and return DrawingML shape XML via svg2ooxml.

    The default unDraw color (#6C63FF) is mapped to a theme slot so
    illustrations automatically follow Office theme changes.

    Args:
        name: Semantic illustration name from ``ILLUSTRATION_CATALOG``.
        theme_color: Theme slot for the primary unDraw color (default ``accent1``).
        color_map: Optional full color-to-theme mapping. Overrides ``theme_color``
            if provided.

    Returns:
        DrawingML shape XML string (one or more ``<p:sp>`` elements).

    Raises:
        IllustrationUnavailableError: If dependencies are missing or fetch fails.
        KeyError: If illustration name is unknown.
    """
    if not HAS_HTTPX:
        msg = "httpx is required for illustration fetching"
        raise IllustrationUnavailableError(msg)
    if not HAS_SVG2OOXML:
        msg = "svg2ooxml is required for SVG-to-DrawingML conversion"
        raise IllustrationUnavailableError(msg)

    config = get_illustration(name)
    cmap = color_map or {_UNDRAW_DEFAULT_COLOR: theme_color}

    # Check cache
    map_key = "|".join(f"{k}={v}" for k, v in sorted(cmap.items()))
    cache_key = hashlib.sha256(f"{config.slug}|{map_key}".encode()).hexdigest()[:16]
    cache_file = _cache_dir() / f"{cache_key}.xml"
    try:
        return cache_file.read_text()
    except FileNotFoundError:
        pass

    # Fetch SVG from unDraw
    url = f"https://undraw.co/api/illustrations/{config.slug}"
    try:
        resp = httpx.get(url, timeout=15.0, follow_redirects=True)
        resp.raise_for_status()
    except Exception as exc:
        msg = f"Illustration fetch failed: {exc}"
        raise IllustrationUnavailableError(msg) from exc

    # Parse SVG → IR → inject theme colors → render DrawingML shapes
    try:
        from svg2ooxml.public import (
            DrawingMLWriter,
            ParserConfig,
            SVGParser,
            convert_parser_output,
        )

        parser = SVGParser(ParserConfig())
        scene = convert_parser_output(parser.parse(resp.text))
        _inject_theme_colors(scene.elements, cmap)
        shapes = DrawingMLWriter().render_shapes_from_ir(scene)
    except Exception as exc:
        msg = f"SVG-to-DrawingML conversion failed: {exc}"
        raise IllustrationUnavailableError(msg) from exc

    shape_xml = "\n".join(shapes)
    cache_file.write_text(shape_xml)
    return shape_xml


def list_illustrations(category: str | None = None) -> list[str]:
    """List available illustration names, optionally filtered by category."""
    if category is None:
        return sorted(ILLUSTRATION_CATALOG.keys())
    return sorted(
        name for name, cfg in ILLUSTRATION_CATALOG.items() if cfg.category == category
    )
