"""
duckviz_theme_manager.py

A composable Plotly Theme Manager that can:
- Build & register Plotly templates (themes)
- Provide presets (pro-light, pro-dark, minimal, etc.)
- Let you compose theme pieces: palette, typography, surfaces, axes, legend, traces
- Apply theme to figures (optionally card-style)
- Support KPI/Indicator defaults, donut pies, modern axes, compact layouts, etc.

Install deps:
    pip install plotly

Usage:
    from duckviz_theme_manager import (
        ThemeManager, ThemeSpec, PaletteSpec, TypographySpec, SurfaceSpec,
        AxesSpec, LegendSpec, TraceSpec, KpiSpec, PieSpec, BarSpec, LineSpec
    )

    tm = ThemeManager()

    # Register a built-in preset
    tm.register_preset("duckviz_pro_light")

    # Use as default
    tm.set_default("duckviz_pro_light")

    # Build a custom theme with composition
    spec = ThemeSpec(
        name="my_brand",
        palette=PaletteSpec(style="brand", colors=["#0EA5E9", "#22C55E", "#F97316", "#EF4444", "#A855F7"]),
        typography=TypographySpec(family="Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif", size=14),
        surface=SurfaceSpec(mode="light", paper_bg="#F6F7FB", plot_bg="#FFFFFF", card_border="#E5E7EB"),
        axes=AxesSpec(style="modern", grid=True, grid_color="#E5E7EB"),
        legend=LegendSpec(style="compact_bottom"),
        traces=TraceSpec(
            bar=BarSpec(opacity=0.9, remove_marker_line=True),
            line=LineSpec(width=3, marker_size=6),
            pie=PieSpec(donut_hole=0.45, textinfo="percent+label"),
            kpi=KpiSpec(number_size=44, delta_size=16)
        ),
        layout_density="comfortable",  # or "compact"
        cards=True,  # applies a subtle card panel feel
    )
    tm.register(spec)

    # Apply to a figure
    fig = ...
    tm.apply(fig, "my_brand", as_card=True)
"""

from __future__ import annotations

from dataclasses import dataclass, field, replace
from typing import Dict, List, Optional, Literal, Any, Sequence, Union

import plotly.graph_objects as go
import plotly.io as pio

from .specs import (
    PaletteSpec, 
    TypographySpec, 
    SurfaceSpec, 
    AxesSpec, 
    LegendSpec, 
    TraceSpec, 
    BarSpec, 
    LineSpec, 
    PieSpec, 
    KpiSpec, 
    ThemeSpec
)   

class ThemeManager:
    """
    ThemeManager builds & registers Plotly templates (themes) using composable specs.

    Key features:
    - register(spec): builds a plotly template and registers it under spec.name
    - register_preset(name): registers a built-in preset
    - set_default(name): sets plotly's default template
    - apply(fig, theme_name, as_card=False, card_mode="soft"): apply theme and optional card framing

    Notes:
    - Plotly templates don't support real CSS cards/shadows; we simulate cards using a rectangle shape.
    - For full "SaaS UI" cards with shadows, export HTML and wrap with CSS.
    """

    def __init__(self):
        self._specs: Dict[str, ThemeSpec] = {}

    # ---------- presets ----------
    def preset(self, name: str) -> ThemeSpec:
        if name == "duckviz_pro_light":
            return ThemeSpec(
                name="duckviz_pro_light",
                palette=PaletteSpec(style="modern"),
                typography=TypographySpec(),
                surface=SurfaceSpec(mode="light", paper_bg="#F5F7FA", plot_bg="#FFFFFF", card_border="#E5E7EB"),
                axes=AxesSpec(style="modern", grid=True),
                legend=LegendSpec(style="compact_bottom"),
                traces=TraceSpec(
                    bar=BarSpec(opacity=0.9, remove_marker_line=True),
                    line=LineSpec(width=3, marker_size=6, mode="lines"),
                    pie=PieSpec(donut_hole=0.45, textinfo="percent+label", pull=0.0),
                    kpi=KpiSpec(number_size=46, delta_size=16, inc_color="#059669", dec_color="#DC2626"),
                ),
                layout_density="comfortable",
                cards=True,
            )

        if name == "duckviz_pro_dark":
            return ThemeSpec(
                name="duckviz_pro_dark",
                palette=PaletteSpec(style="vibrant"),
                typography=TypographySpec(color="#E5E7EB"),
                surface=SurfaceSpec(mode="dark"),
                axes=AxesSpec(style="modern", grid=True),
                legend=LegendSpec(style="compact_bottom"),
                traces=TraceSpec(
                    bar=BarSpec(opacity=0.92, remove_marker_line=True),
                    line=LineSpec(width=3, marker_size=6),
                    pie=PieSpec(donut_hole=0.45, textinfo="percent+label"),
                    kpi=KpiSpec(number_size=46, delta_size=16, inc_color="#22C55E", dec_color="#EF4444"),
                ),
                layout_density="comfortable",
                cards=True,
            )

        if name == "duckviz_minimal":
            return ThemeSpec(
                name="duckviz_minimal",
                palette=PaletteSpec(style="minimal"),
                typography=TypographySpec(size=13, title_size=18),
                surface=SurfaceSpec(mode="light", paper_bg="#FFFFFF", plot_bg="#FFFFFF"),
                axes=AxesSpec(style="minimal", grid=True, grid_color="#F1F5F9"),
                legend=LegendSpec(style="none"),
                layout_density="compact",
                cards=False,
            )

        raise ValueError(f"Unknown preset '{name}'. Available: duckviz_pro_light, duckviz_pro_dark, duckviz_minimal")

    def register_preset(self, name: str) -> ThemeSpec:
        spec = self.preset(name)
        self.register(spec)
        return spec

    # ---------- core registration ----------
    def register(self, spec: ThemeSpec) -> None:
        template = spec.build_template()
        pio.templates[spec.name] = template
        self._specs[spec.name] = spec

    def get_spec(self, name: str) -> ThemeSpec:
        if name not in self._specs:
            # allow access to a template that exists but wasn't registered through this manager
            raise KeyError(f"Theme '{name}' not found in ThemeManager. Did you call register(...) or register_preset(...)?")
        return self._specs[name]

    def set_default(self, name: str) -> None:
        if name not in pio.templates:
            raise KeyError(f"Theme '{name}' not registered in plotly.io.templates")
        pio.templates.default = name

    # ---------- applying theme & cards ----------
    CardMode = Literal["soft", "border", "none"]

    def apply(
        self,
        fig: go.Figure,
        theme_name: str,
        *,
        as_card: bool = False,
        card_mode: CardMode = "soft",
        card_padding: int = 14,
        card_border_width: int = 1,
    ) -> go.Figure:
        """
        Apply a registered theme to a figure. Optionally add a "card" frame.
        """
        if theme_name not in pio.templates:
            raise KeyError(f"Theme '{theme_name}' not registered in plotly.io.templates")

        fig.update_layout(template=theme_name)

        # If theme spec wants cards by default, treat as_card True unless explicitly False
        if theme_name in self._specs and self._specs[theme_name].cards:
            as_card = True if as_card is False else as_card

        if as_card and card_mode != "none":
            self._apply_card_frame(
                fig,
                theme_name=theme_name,
                card_mode=card_mode,
                padding=card_padding,
                border_width=card_border_width,
            )

        return fig

    def _apply_card_frame(
        self,
        fig: go.Figure,
        *,
        theme_name: str,
        card_mode: CardMode,
        padding: int,
        border_width: int,
    ) -> None:
        """
        Simulate a card using a rectangle shape in paper coordinates.

        NOTE: Plotly shapes do not support border radius or true shadows; we keep it subtle and clean.
        """
        # Resolve surface colors from spec if available
        border_color = "#E5E7EB"
        fill_color = "#FFFFFF"

        if theme_name in self._specs:
            spec = self._specs[theme_name]
            fill_color = spec.surface.card_bg
            border_color = spec.surface.card_border

        if card_mode == "soft":
            # soft card: subtle border, white fill
            line = dict(color=border_color, width=border_width)
        else:  # "border"
            line = dict(color=border_color, width=max(1, border_width))

        # Tighten margins to feel like a card
        fig.update_layout(
            paper_bgcolor=fill_color,
            plot_bgcolor=fig.layout.plot_bgcolor,
            margin=dict(
                t=max(20, (fig.layout.margin.t or 0) - padding),
                l=max(20, (fig.layout.margin.l or 0) - padding),
                r=max(20, (fig.layout.margin.r or 0) - padding),
                b=max(20, (fig.layout.margin.b or 0) - padding),
            ),
        )

        shapes = list(fig.layout.shapes) if fig.layout.shapes else []
        shapes.append(
            dict(
                type="rect",
                xref="paper",
                yref="paper",
                x0=0,
                y0=0,
                x1=1,
                y1=1,
                line=line,
                fillcolor=fill_color,
                layer="below",
            )
        )
        fig.update_layout(shapes=shapes)

    # ---------- convenience builders ----------
    def clone(self, base_theme: str, new_name: str, **patches) -> ThemeSpec:
        """
        Clone an existing registered ThemeSpec and patch fields (composition-friendly).

        Example:
            spec2 = tm.clone("duckviz_pro_light", "my_brand",
                            palette=PaletteSpec(colors=[...]),
                            surface=replace(tm.get_spec("duckviz_pro_light").surface, paper_bg="#F2F4F8"))
            tm.register(spec2)
        """
        base = self.get_spec(base_theme)
        spec = replace(base, name=new_name, **patches)
        return spec

def register_defaults(tm: Optional[ThemeManager] = None) -> ThemeManager:
    """
    Registers the common presets and returns the ThemeManager.
    """
    tm = tm or ThemeManager()
    tm.register_preset("duckviz_pro_light")
    tm.register_preset("duckviz_pro_dark")
    tm.register_preset("duckviz_minimal")
    return tm
