"""DTCG token type definitions.

This module defines Python dataclasses that represent the W3C Design Tokens
Community Group (DTCG) format specification.

See: https://tr.designtokens.org/format/
"""

from __future__ import annotations

import copy
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Union

from tokenmoulds.design.units import EMU_PER_POINT


class TokenType(str, Enum):
    """DTCG token types."""

    # Primitive types
    COLOR = "color"
    DIMENSION = "dimension"
    FONT_FAMILY = "fontFamily"
    FONT_WEIGHT = "fontWeight"
    DURATION = "duration"
    CUBIC_BEZIER = "cubicBezier"
    NUMBER = "number"

    # Composite types
    STROKE_STYLE = "strokeStyle"
    BORDER = "border"
    TRANSITION = "transition"
    SHADOW = "shadow"
    GRADIENT = "gradient"
    TYPOGRAPHY = "typography"


# Font weight string aliases as per DTCG spec
FONT_WEIGHT_ALIASES: Dict[str, int] = {
    "thin": 100,
    "hairline": 100,
    "extra-light": 200,
    "ultra-light": 200,
    "light": 300,
    "normal": 400,
    "regular": 400,
    "book": 400,
    "medium": 500,
    "semi-bold": 600,
    "demi-bold": 600,
    "bold": 700,
    "extra-bold": 800,
    "ultra-bold": 800,
    "black": 900,
    "heavy": 900,
    "extra-black": 950,
    "ultra-black": 950,
}


# Dimension units
class DimensionUnit(str, Enum):
    """Supported dimension units."""

    PX = "px"
    REM = "rem"
    # Extended units for Office/print
    PT = "pt"
    EMU = "emu"
    TWIPS = "twips"


# Duration units
class DurationUnit(str, Enum):
    """Supported duration units."""

    MS = "ms"
    S = "s"


# Stroke style keywords
class StrokeStyleKeyword(str, Enum):
    """Pre-defined stroke style keywords."""

    SOLID = "solid"
    DASHED = "dashed"
    DOTTED = "dotted"
    DOUBLE = "double"
    GROOVE = "groove"
    RIDGE = "ridge"
    OUTSET = "outset"
    INSET = "inset"


# Stroke cap keywords (CSS values — distinct from ir.style_primitives.LineCap which uses OOXML values)
class StrokeCapKeyword(str, Enum):
    """CSS stroke-linecap keywords for DTCG stroke style tokens."""

    ROUND = "round"
    BUTT = "butt"
    SQUARE = "square"


@dataclass
class ColorValue:
    """DTCG color value.

    Can be represented as:
    - Hex string: "#2563EB"
    - Object with colorSpace and components
    """

    # Simple hex representation
    hex: Optional[str] = None

    # Full color space representation
    color_space: Optional[str] = None  # "srgb", "display-p3", etc.
    components: Optional[List[float]] = None  # [r, g, b] normalized 0-1
    alpha: Optional[float] = None  # 0-1

    @classmethod
    def from_hex(cls, hex_str: str) -> ColorValue:
        """Create from hex string."""
        normalized = hex_str if hex_str.startswith("#") else f"#{hex_str}"
        return cls(hex=normalized.upper())

    @classmethod
    def from_components(
        cls,
        r: float,
        g: float,
        b: float,
        alpha: float = 1.0,
        color_space: str = "srgb",
    ) -> ColorValue:
        """Create from RGB components (0-1 range)."""
        hex_str = f"#{int(r * 255):02X}{int(g * 255):02X}{int(b * 255):02X}"
        return cls(
            hex=hex_str,
            color_space=color_space,
            components=[r, g, b],
            alpha=alpha,
        )

    def to_hex(self) -> str:
        """Convert to hex string."""
        if self.hex:
            return self.hex
        if self.components:
            r, g, b = self.components[:3]
            return f"#{int(r * 255):02X}{int(g * 255):02X}{int(b * 255):02X}"
        return "#000000"


@dataclass
class DimensionValue:
    """DTCG dimension value (e.g., spacing, size)."""

    value: float
    unit: str  # "px", "rem", "pt", etc.

    def to_px(self, rem_base: float = 16.0) -> float:
        """Convert to pixels."""
        if self.unit == "px":
            return self.value
        elif self.unit == "rem":
            return self.value * rem_base
        elif self.unit == "pt":
            return self.value * 1.333333  # 96/72
        elif self.unit == "emu":
            return self.value / 9525  # EMU to px at 96 DPI
        elif self.unit == "twips":
            return self.value / 15  # twips to px at 96 DPI
        return self.value

    def to_pt(self) -> float:
        """Convert to points."""
        if self.unit == "pt":
            return self.value
        elif self.unit == "px":
            return self.value * 0.75  # 72/96
        elif self.unit == "rem":
            return self.value * 12  # Assuming 16px = 12pt
        elif self.unit == "emu":
            return self.value / EMU_PER_POINT
        elif self.unit == "twips":
            return self.value / 20
        return self.value


@dataclass
class DurationValue:
    """DTCG duration value."""

    value: float
    unit: str  # "ms" or "s"

    def to_ms(self) -> float:
        """Convert to milliseconds."""
        if self.unit == "s":
            return self.value * 1000
        return self.value


# Cubic bezier is just a list of 4 numbers
CubicBezierValue = List[float]  # [P1x, P1y, P2x, P2y]


@dataclass
class StrokeStyleValue:
    """DTCG stroke style value (can be keyword or object)."""

    # Keyword style (solid, dashed, etc.)
    keyword: Optional[str] = None

    # Object style with dash array
    dash_array: Optional[List[DimensionValue | str]] = None
    line_cap: Optional[str] = None  # "round", "butt", "square"

    @classmethod
    def from_keyword(cls, keyword: str) -> StrokeStyleValue:
        """Create from keyword."""
        return cls(keyword=keyword)

    @classmethod
    def from_dash_array(
        cls,
        dash_array: List[DimensionValue | str],
        line_cap: str = "butt",
    ) -> StrokeStyleValue:
        """Create from dash array."""
        return cls(dash_array=dash_array, line_cap=line_cap)


@dataclass
class BorderValue:
    """DTCG border composite value."""

    color: Union[ColorValue, str]  # ColorValue or reference string
    width: Union[DimensionValue, str]  # DimensionValue or reference
    style: Union[StrokeStyleValue, str]  # StrokeStyleValue or reference


@dataclass
class TransitionValue:
    """DTCG transition composite value."""

    duration: Union[DurationValue, str]
    delay: Union[DurationValue, str]
    timing_function: Union[CubicBezierValue, str]


@dataclass
class ShadowValue:
    """DTCG shadow composite value."""

    color: Union[ColorValue, str]
    offset_x: Union[DimensionValue, str]
    offset_y: Union[DimensionValue, str]
    blur: Union[DimensionValue, str]
    spread: Union[DimensionValue, str]
    inset: bool = False


@dataclass
class GradientStop:
    """A single stop in a gradient."""

    color: Union[ColorValue, str]
    position: Union[float, str]  # 0-1 or reference


# Gradient is a list of stops
GradientValue = List[GradientStop]


@dataclass
class TypographyValue:
    """DTCG typography composite value."""

    font_family: Union[str, List[str], str]  # Font name(s) or reference
    font_size: Union[DimensionValue, str]
    font_weight: Union[int, str]  # Number or keyword or reference
    letter_spacing: Union[DimensionValue, str]
    line_height: Union[float, str]  # Multiplier or reference


@dataclass
class SvgAssetValue:
    """Internal representation of an SVG asset with theme color mapping.

    Not a DTCG token type — constructed from tokens that carry
    ``$extensions.com.tokenmoulds.svgAsset`` metadata. The token ``$value``
    is a plain string (the ``src``), keeping the file valid DTCG.
    """

    src: str
    color_map: Dict[str, str] = field(
        default_factory=dict
    )  # e.g. {"6C63FF": "accent1"}


# Union of all possible token values
TokenValue = Union[
    # Primitive values
    ColorValue,
    DimensionValue,
    str,  # fontFamily as string
    List[str],  # fontFamily as array
    int,  # fontWeight as number
    DurationValue,
    CubicBezierValue,
    float,  # number
    # Composite values
    StrokeStyleValue,
    BorderValue,
    TransitionValue,
    ShadowValue,
    List[ShadowValue],  # Multiple shadows
    GradientValue,
    TypographyValue,
    # Reference (alias)
    str,  # "{token.path}"
]


@dataclass
class TokenExtensions:
    """TokenMoulds-specific extensions."""

    # OOXML mapping
    ooxml_theme_slot: Optional[str] = None  # "accent1", "dk1", etc.
    ooxml_tint: Optional[float] = None
    ooxml_shade: Optional[float] = None

    # Derivation info
    derivation_source: Optional[str] = None  # "input", "formula", "inherited"
    derivation_formula: Optional[str] = None  # e.g., "lighten({color.primary}, 0.2)"

    # Accessibility
    wcag_contrast_ratio: Optional[float] = None
    wcag_level: Optional[str] = None  # "AA", "AAA"

    # SVG asset metadata
    svg_asset: bool = False
    svg_color_map: Optional[Dict[str, str]] = None  # e.g. {"6C63FF": "accent1"}

    # Other vendor extensions (preserved on round-trip)
    other: Dict[str, Any] = field(default_factory=dict)


@dataclass
class Token:
    """A single design token."""

    # Required
    name: str
    value: Any  # The actual value or a reference string

    # Optional
    type: Optional[TokenType] = None
    description: Optional[str] = None
    deprecated: Union[bool, str, None] = None  # True, False, or explanation string
    extensions: Optional[TokenExtensions] = None

    # Internal: the full path to this token
    path: str = ""

    def is_reference(self) -> bool:
        """Check if this token's value is a reference to another token."""
        if isinstance(self.value, str):
            v = self.value.strip()
            return v.startswith("{") and v.endswith("}")
        return False

    def get_reference_path(self) -> Optional[str]:
        """Get the path of the referenced token, if this is a reference."""
        if self.is_reference() and isinstance(self.value, str):
            return self.value.strip()[1:-1]  # Remove { }
        return None


@dataclass
class TokenGroup:
    """A group of tokens and/or nested groups."""

    name: str
    tokens: Dict[str, Token] = field(default_factory=dict)
    groups: Dict[str, TokenGroup] = field(default_factory=dict)

    # Group-level properties
    type: Optional[TokenType] = None  # Inherited by child tokens
    description: Optional[str] = None
    deprecated: Union[bool, str, None] = None
    extensions: Optional[Dict[str, Any]] = None

    # For $extends support
    extends: Optional[str] = None  # Reference to another group

    def get_token(self, path: str) -> Optional[Token]:
        """Get a token by relative path (e.g., 'primary' or 'shade.light')."""
        parts = path.split(".")

        if len(parts) == 1:
            return self.tokens.get(parts[0])

        # Navigate through nested groups
        group_name = parts[0]
        if group_name in self.groups:
            return self.groups[group_name].get_token(".".join(parts[1:]))

        return None

    def all_tokens(self, prefix: str = "") -> List[Token]:
        """Get all tokens in this group and nested groups."""
        result = []

        for name, token in self.tokens.items():
            token_copy = copy.copy(token)
            token_copy.path = f"{prefix}.{name}" if prefix else name
            result.append(token_copy)

        for name, group in self.groups.items():
            group_prefix = f"{prefix}.{name}" if prefix else name
            result.extend(group.all_tokens(group_prefix))

        return result


@dataclass
class TokenFile:
    """A complete design token file."""

    # Root-level tokens and groups
    tokens: Dict[str, Token] = field(default_factory=dict)
    groups: Dict[str, TokenGroup] = field(default_factory=dict)

    # File metadata (not in DTCG spec, but useful)
    source_path: Optional[str] = None

    def get_token(self, path: str) -> Optional[Token]:
        """Get a token by full path (e.g., 'color.primary.base')."""
        parts = path.split(".")

        if len(parts) == 1:
            return self.tokens.get(parts[0])

        # Navigate through groups
        group_name = parts[0]
        if group_name in self.groups:
            return self.groups[group_name].get_token(".".join(parts[1:]))

        return None

    def all_tokens(self) -> List[Token]:
        """Get all tokens in the file."""
        result = []

        for name, token in self.tokens.items():
            token.path = name
            result.append(token)

        for name, group in self.groups.items():
            result.extend(group.all_tokens(name))

        return result

    def resolve_reference(self, ref: str) -> Optional[Token]:
        """Resolve a reference string like '{color.primary}' to its token."""
        if ref.startswith("{") and ref.endswith("}"):
            path = ref[1:-1]
            return self.get_token(path)
        return None

    def resolve_value(self, token: Token, max_depth: int = 10) -> Any:
        """Resolve a token's value, following references."""
        if max_depth <= 0:
            raise ValueError(f"Circular reference detected for token: {token.path}")

        if token.is_reference():
            ref_path = token.get_reference_path()
            ref_token = self.get_token(ref_path) if ref_path else None
            if ref_token:
                return self.resolve_value(ref_token, max_depth - 1)
            raise ValueError(f"Unresolved reference: {token.value}")

        return token.value
