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

import plotly.graph_objects as go

from .axis import AxesSpec
from .legend import LegendSpec
from .traces import TraceSpec
from .palette import PaletteSpec
from .typograpyhy import TypographySpec
from .surface import SurfaceSpec

LayoutDensity = Literal["comfortable", "compact"]


@dataclass(frozen=True)
class ThemeSpec:
    name: str
    palette: PaletteSpec = field(default_factory=PaletteSpec)
    typography: TypographySpec = field(default_factory=TypographySpec)
    surface: SurfaceSpec = field(default_factory=SurfaceSpec)
    axes: AxesSpec = field(default_factory=AxesSpec)
    legend: LegendSpec = field(default_factory=LegendSpec)
    traces: TraceSpec = field(default_factory=TraceSpec)

    layout_density: LayoutDensity = "comfortable"
    cards: bool = False  # used by ThemeManager.apply(..., as_card=True)

    # Layout knobs
    margin_t: Optional[int] = None
    margin_l: Optional[int] = None
    margin_r: Optional[int] = None
    margin_b: Optional[int] = None

    # Title positioning
    title_x: float = 0.02
    title_xanchor: str = "left"

    # Extra layout overrides if you want raw control
    layout_overrides: Dict[str, Any] = field(default_factory=dict)

    def build_template(self) -> go.layout.Template:
        """
        Build a Plotly template from the composed specs.

        Key safety rule:
        - Never pass dataclass __dict__ (or any dict containing unknown keys) into Plotly layout nodes.
        - Plotly validates properties strictly; only use supported keys.
        """

        # Base layout (background + font) from surface spec
        # Expected shape (example):
        # {
        #   "paper_bgcolor": "...",
        #   "plot_bgcolor": "...",
        #   "font": {"family": "...", "size": 14, "color": "..."}
        # }
        surface_layout = self.surface.resolved(self.typography)

        # Axes
        xaxis = self.axes.build_axis(self.typography, self.surface)
        yaxis = self.axes.build_axis(self.typography, self.surface)

        # Legend
        legend_block = self.legend.build()

        # Density
        if self.layout_density == "compact":
            margin = dict(t=50, l=35, r=25, b=45)
            font_size = max(12, self.typography.size - 1)
            tick_size = (
                self.typography.tick_size
                if self.typography.tick_size is not None
                else max(10, font_size - 2)
            )
            xaxis["tickfont"] = dict(size=tick_size)
            yaxis["tickfont"] = dict(size=tick_size)
        else:
            margin = dict(t=70, l=50, r=40, b=60)
            font_size = self.typography.size

        # Allow explicit margin overrides
        if self.margin_t is not None:
            margin["t"] = self.margin_t
        if self.margin_l is not None:
            margin["l"] = self.margin_l
        if self.margin_r is not None:
            margin["r"] = self.margin_r
        if self.margin_b is not None:
            margin["b"] = self.margin_b

        # Palette
        colorway = self.palette.resolve_colorway()

        # Safe font mapping (ONLY allowed keys)
        base_font = surface_layout.get("font") or {}
        font_color = base_font.get("color", self.typography.color)

        safe_font = dict(
            family=base_font.get("family", self.typography.family),
            size=font_size,
            color=font_color,
        )

        # Title styling (title_size is used HERE, not in layout.font)
        title_block = dict(
            x=self.title_x,
            xanchor=self.title_xanchor,
            font=dict(
                size=self.typography.title_size,
                color=font_color,
                family=safe_font["family"],
            ),
        )

        # Build final layout (avoid merging unknown font keys)
        layout: Dict[str, Any] = {}
        layout.update(surface_layout)
        layout.update(
            colorway=colorway,
            margin=margin,
            font=safe_font,
            title=title_block,
            xaxis=xaxis,
            yaxis=yaxis,
        )
        layout.update(legend_block)

        # Apply raw overrides last (you can still break things here if you pass invalid keys)
        layout.update(self.layout_overrides)

        # Trace defaults
        data: Dict[str, List[Any]] = {}

        # Bars
        bar_kwargs: Dict[str, Any] = {"opacity": self.traces.bar.opacity}
        if self.traces.bar.remove_marker_line:
            bar_kwargs["marker"] = dict(line=dict(width=self.traces.bar.marker_line_width))
        data["bar"] = [go.Bar(**bar_kwargs)]

        # Scatter/line
        data["scatter"] = [
            go.Scatter(
                mode=self.traces.line.mode,
                line=dict(width=self.traces.line.width),
                marker=dict(size=self.traces.line.marker_size),
            )
        ]

        # Pie
        data["pie"] = [
            go.Pie(
                hole=self.traces.pie.donut_hole,
                textinfo=self.traces.pie.textinfo,
                pull=self.traces.pie.pull,
            )
        ]

        # KPI / Indicator
        # FIX: Plotly expects delta.font.size (NOT title_size)
        data["indicator"] = [
            go.Indicator(
                number=dict(
                    font=dict(
                        size=self.traces.kpi.number_size,
                        family=safe_font["family"],
                        color=font_color,
                    )
                ),
                delta=dict(
                    font=dict(
                        size=self.traces.kpi.delta_size,
                        family=safe_font["family"],
                        color=font_color,
                    ),
                    increasing=dict(color=self.traces.kpi.inc_color),
                    decreasing=dict(color=self.traces.kpi.dec_color),
                ),
            )
        ]

        return go.layout.Template(layout=layout, data=data)
