"""DTCG token file parser.

Parses .tokens.json files into Python dataclasses.
"""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

from tokenmoulds.dtcg.types import (
    BorderValue,
    ColorValue,
    DimensionValue,
    DurationValue,
    GradientStop,
    GradientValue,
    ShadowValue,
    StrokeStyleValue,
    Token,
    TokenExtensions,
    TokenFile,
    TokenGroup,
    TokenType,
    TransitionValue,
    TypographyValue,
)


_DIMENSION_RE = re.compile(r"^(-?[\d.]+)\s*(px|pt|rem|em|%)$")


class DTCGParseError(Exception):
    """Error parsing DTCG token file."""

    pass


def parse_tokens(path: Path) -> TokenFile:
    """Parse a .tokens.json file.

    Args:
        path: Path to the token file

    Returns:
        TokenFile containing all parsed tokens

    Raises:
        DTCGParseError: If the file cannot be parsed
    """
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
    except json.JSONDecodeError as e:
        raise DTCGParseError(f"Invalid JSON in {path}: {e}")
    except FileNotFoundError:
        raise DTCGParseError(f"File not found: {path}")

    token_file = parse_tokens_from_dict(data)
    token_file.source_path = str(path)
    return token_file


def parse_tokens_from_string(content: str) -> TokenFile:
    """Parse a JSON string containing DTCG tokens.

    Args:
        content: JSON string

    Returns:
        TokenFile containing all parsed tokens
    """
    try:
        data = json.loads(content)
    except json.JSONDecodeError as e:
        raise DTCGParseError(f"Invalid JSON: {e}")

    return parse_tokens_from_dict(data)


def parse_tokens_from_dict(data: Dict[str, Any]) -> TokenFile:
    """Parse a dictionary containing DTCG tokens.

    Args:
        data: Dictionary from JSON

    Returns:
        TokenFile containing all parsed tokens
    """
    token_file = TokenFile()

    for key, value in data.items():
        # Skip reserved properties at root level
        if key.startswith("$"):
            continue

        if _is_token(value):
            token = _parse_token(key, value, inherited_type=None)
            token_file.tokens[key] = token
        elif isinstance(value, dict):
            group = _parse_group(key, value, inherited_type=None)
            token_file.groups[key] = group

    return token_file


def _is_token(obj: Any) -> bool:
    """Check if an object represents a token (has $value)."""
    return isinstance(obj, dict) and "$value" in obj


def _parse_group(
    name: str,
    data: Dict[str, Any],
    inherited_type: Optional[TokenType],
) -> TokenGroup:
    """Parse a token group."""
    group = TokenGroup(name=name)

    # Parse group-level properties
    if "$type" in data:
        try:
            group.type = TokenType(data["$type"])
        except ValueError:
            # Unknown type, keep as None
            pass

    group.description = data.get("$description")
    group.deprecated = data.get("$deprecated")
    group.extends = data.get("$extends")

    if "$extensions" in data:
        group.extensions = data["$extensions"]

    # Determine type for children
    child_type = group.type or inherited_type

    # Parse children
    for key, value in data.items():
        # Skip reserved properties
        if key.startswith("$"):
            continue

        if _is_token(value):
            token = _parse_token(key, value, inherited_type=child_type)
            group.tokens[key] = token
        elif isinstance(value, dict):
            child_group = _parse_group(key, value, inherited_type=child_type)
            group.groups[key] = child_group

    return group


def _parse_token(
    name: str,
    data: Dict[str, Any],
    inherited_type: Optional[TokenType],
) -> Token:
    """Parse a single token."""
    # Determine type
    token_type: Optional[TokenType] = None
    if "$type" in data:
        try:
            token_type = TokenType(data["$type"])
        except ValueError:
            pass
    else:
        token_type = inherited_type

    # Parse value based on type
    raw_value = data["$value"]
    parsed_value = _parse_value(raw_value, token_type)

    # Parse extensions
    extensions = None
    if "$extensions" in data:
        extensions = _parse_extensions(data["$extensions"])

    return Token(
        name=name,
        value=parsed_value,
        type=token_type,
        description=data.get("$description"),
        deprecated=data.get("$deprecated"),
        extensions=extensions,
    )


def _parse_value(value: Any, token_type: Optional[TokenType]) -> Any:
    """Parse a token value based on its type."""
    # If it's a reference string, return as-is
    if isinstance(value, str) and value.strip().startswith("{"):
        return value

    # Parse based on type
    if token_type == TokenType.COLOR:
        return _parse_color_value(value)
    elif token_type == TokenType.DIMENSION:
        return _parse_dimension_value(value)
    elif token_type == TokenType.DURATION:
        return _parse_duration_value(value)
    elif token_type == TokenType.FONT_FAMILY:
        return value  # String or array of strings
    elif token_type == TokenType.FONT_WEIGHT:
        return value  # Number or string keyword
    elif token_type == TokenType.CUBIC_BEZIER:
        return value  # Array of 4 numbers
    elif token_type == TokenType.NUMBER:
        return value  # Number
    elif token_type == TokenType.STROKE_STYLE:
        return _parse_stroke_style_value(value)
    elif token_type == TokenType.BORDER:
        return _parse_border_value(value)
    elif token_type == TokenType.TRANSITION:
        return _parse_transition_value(value)
    elif token_type == TokenType.SHADOW:
        return _parse_shadow_value(value)
    elif token_type == TokenType.GRADIENT:
        return _parse_gradient_value(value)
    elif token_type == TokenType.TYPOGRAPHY:
        return _parse_typography_value(value)

    # Unknown type - try to infer or return raw
    return _infer_and_parse_value(value)


def _infer_and_parse_value(value: Any) -> Any:
    """Try to infer the type and parse accordingly."""
    if isinstance(value, str):
        # Could be a reference or a simple string (fontFamily, hex color)
        if value.startswith("{"):
            return value
        if value.startswith("#"):
            return ColorValue.from_hex(value)
        return value

    if isinstance(value, dict):
        # Try to identify by structure
        if "colorSpace" in value or "components" in value:
            return _parse_color_value(value)
        if "value" in value and "unit" in value:
            return _parse_dimension_value(value)
        if "fontFamily" in value and "fontSize" in value:
            return _parse_typography_value(value)
        if "offsetX" in value and "blur" in value:
            return _parse_shadow_value(value)
        if "dashArray" in value:
            return _parse_stroke_style_value(value)
        if "color" in value and "width" in value and "style" in value:
            return _parse_border_value(value)

    if isinstance(value, list):
        # Could be gradient, multiple shadows, fontFamily array, or cubicBezier
        if len(value) == 4 and all(isinstance(v, (int, float)) for v in value):
            return value  # cubicBezier
        if value and isinstance(value[0], dict) and "position" in value[0]:
            return _parse_gradient_value(value)
        if value and isinstance(value[0], dict) and "offsetX" in value[0]:
            return [_parse_shadow_value(v) for v in value]
        return value  # fontFamily array or other

    return value


def _parse_color_value(value: Any) -> ColorValue | str:
    """Parse a color value."""
    if isinstance(value, str):
        if value.startswith("{"):
            return value  # Reference
        return ColorValue.from_hex(value)

    if isinstance(value, dict):
        return ColorValue(
            hex=value.get("hex"),
            color_space=value.get("colorSpace"),
            components=value.get("components"),
            alpha=value.get("alpha"),
        )

    raise DTCGParseError(f"Invalid color value: {value}")


def _parse_dimension_string(raw: str) -> DimensionValue:
    """Parse a shorthand dimension string like '4px', '1.5rem', '16pt'.

    This is the format exported by Figma, Tokens Studio, and Style Dictionary.
    """
    m = _DIMENSION_RE.match(raw)
    if not m:
        raise DTCGParseError(f"Cannot parse dimension string: {raw!r}")
    return DimensionValue(value=float(m.group(1)), unit=m.group(2))


def _parse_dimension_value(value: Any) -> Union[DimensionValue, str]:
    """Parse a dimension value.

    Accepts both the DTCG object form ``{"value": 4, "unit": "px"}``
    and the shorthand string form ``"4px"`` used by Figma / Tokens Studio.
    """
    if isinstance(value, str):
        if value.startswith("{"):
            return value  # Reference
        return _parse_dimension_string(value)

    if isinstance(value, (int, float)):
        # Bare number — assume px (common in Figma exports)
        return DimensionValue(value=float(value), unit="px")

    if isinstance(value, dict):
        return DimensionValue(
            value=float(value["value"]),
            unit=value["unit"],
        )

    raise DTCGParseError(f"Invalid dimension value: {value}")


def _parse_duration_value(value: Any) -> Union[DurationValue, str]:
    """Parse a duration value."""
    if isinstance(value, str) and value.startswith("{"):
        return value  # Reference

    if isinstance(value, dict):
        return DurationValue(
            value=float(value["value"]),
            unit=value["unit"],
        )

    raise DTCGParseError(f"Invalid duration value: {value}")


def _parse_stroke_style_value(value: Any) -> Union[StrokeStyleValue, str]:
    """Parse a stroke style value."""
    if isinstance(value, str):
        if value.startswith("{"):
            return value  # Reference
        return StrokeStyleValue.from_keyword(value)

    if isinstance(value, dict):
        dash_array = None
        if "dashArray" in value:
            dash_array = [_parse_dimension_value(d) for d in value["dashArray"]]

        return StrokeStyleValue(
            dash_array=dash_array,
            line_cap=value.get("lineCap"),
        )

    raise DTCGParseError(f"Invalid stroke style value: {value}")


def _parse_border_value(value: Any) -> Union[BorderValue, str]:
    """Parse a border value."""
    if isinstance(value, str) and value.startswith("{"):
        return value  # Reference

    if isinstance(value, dict):
        return BorderValue(
            color=_parse_color_value(value["color"])
            if not isinstance(value["color"], str) or not value["color"].startswith("{")
            else value["color"],
            width=_parse_dimension_value(value["width"])
            if not isinstance(value["width"], str) or not value["width"].startswith("{")
            else value["width"],
            style=_parse_stroke_style_value(value["style"])
            if not isinstance(value["style"], str) or not value["style"].startswith("{")
            else value["style"],
        )

    raise DTCGParseError(f"Invalid border value: {value}")


def _parse_transition_value(value: Any) -> Union[TransitionValue, str]:
    """Parse a transition value."""
    if isinstance(value, str) and value.startswith("{"):
        return value  # Reference

    if isinstance(value, dict):
        return TransitionValue(
            duration=_parse_duration_value(value["duration"])
            if not isinstance(value["duration"], str)
            or not value["duration"].startswith("{")
            else value["duration"],
            delay=_parse_duration_value(value["delay"])
            if not isinstance(value["delay"], str) or not value["delay"].startswith("{")
            else value["delay"],
            timing_function=value["timingFunction"],
        )

    raise DTCGParseError(f"Invalid transition value: {value}")


def _parse_shadow_value(value: Any) -> Union[ShadowValue, List[ShadowValue | str], str]:
    """Parse a shadow value (single or array)."""
    if isinstance(value, str) and value.startswith("{"):
        return value  # Reference

    if isinstance(value, list):
        return [_parse_single_shadow(v) for v in value]

    return _parse_single_shadow(value)


def _parse_single_shadow(value: Any) -> Union[ShadowValue, str]:
    """Parse a single shadow object."""
    if isinstance(value, str) and value.startswith("{"):
        return value  # Reference

    if isinstance(value, dict):
        return ShadowValue(
            color=_parse_color_value(value["color"])
            if not isinstance(value["color"], str) or not value["color"].startswith("{")
            else value["color"],
            offset_x=_parse_dimension_value(value["offsetX"])
            if not isinstance(value["offsetX"], str)
            or not value["offsetX"].startswith("{")
            else value["offsetX"],
            offset_y=_parse_dimension_value(value["offsetY"])
            if not isinstance(value["offsetY"], str)
            or not value["offsetY"].startswith("{")
            else value["offsetY"],
            blur=_parse_dimension_value(value["blur"])
            if not isinstance(value["blur"], str) or not value["blur"].startswith("{")
            else value["blur"],
            spread=_parse_dimension_value(value["spread"])
            if not isinstance(value["spread"], str)
            or not value["spread"].startswith("{")
            else value["spread"],
            inset=value.get("inset", False),
        )

    raise DTCGParseError(f"Invalid shadow value: {value}")


def _parse_gradient_value(value: Any) -> Union[GradientValue, str]:
    """Parse a gradient value (array of stops)."""
    if isinstance(value, str) and value.startswith("{"):
        return value  # Reference

    if isinstance(value, list):
        stops = []
        for stop in value:
            if isinstance(stop, str) and stop.startswith("{"):
                stops.append(stop)  # Reference to gradient stop
            elif isinstance(stop, dict):
                stops.append(
                    GradientStop(
                        color=_parse_color_value(stop["color"])
                        if not isinstance(stop["color"], str)
                        or not stop["color"].startswith("{")
                        else stop["color"],
                        position=stop["position"],
                    )
                )
        return stops

    raise DTCGParseError(f"Invalid gradient value: {value}")


def _parse_typography_value(value: Any) -> Union[TypographyValue, str]:
    """Parse a typography value.

    Handles both DTCG object-form dimensions and Figma shorthand strings
    (e.g. ``"42px"``, ``"-0.02em"``).
    """
    if isinstance(value, str) and value.startswith("{"):
        return value  # Reference

    if isinstance(value, dict):
        font_family = value.get("fontFamily", "sans-serif")
        raw_size = value.get("fontSize", {"value": 16, "unit": "px"})
        raw_spacing = value.get("letterSpacing", {"value": 0, "unit": "px"})
        raw_line_height = value.get("lineHeight")
        raw_weight = value.get("fontWeight", 400)

        parsed_size = _parse_dimension_value(raw_size)

        # Parse line height: string dim ("24px"), dict dim, number multiplier, or reference
        if isinstance(raw_line_height, str) and raw_line_height.startswith("{"):
            parsed_line_height: float | str = raw_line_height
        elif isinstance(raw_line_height, (str, dict)):
            dim = _parse_dimension_value(raw_line_height)
            # Convert absolute line-height to a multiplier relative to font size
            if (
                isinstance(dim, DimensionValue)
                and isinstance(parsed_size, DimensionValue)
                and parsed_size.to_pt() > 0
            ):
                parsed_line_height = round(dim.to_pt() / parsed_size.to_pt(), 3)
            elif isinstance(dim, DimensionValue):
                parsed_line_height = 1.2
            else:
                parsed_line_height = dim  # reference string
        elif isinstance(raw_line_height, (int, float)):
            parsed_line_height = float(raw_line_height)
        else:
            parsed_line_height = 1.2

        return TypographyValue(
            font_family=font_family,
            font_size=parsed_size,
            font_weight=raw_weight,
            letter_spacing=_parse_dimension_value(raw_spacing),
            line_height=parsed_line_height,
        )

    raise DTCGParseError(f"Invalid typography value: {value}")


def _parse_extensions(data: Dict[str, Any]) -> TokenExtensions:
    """Parse token extensions."""
    extensions = TokenExtensions()

    # Parse TokenMoulds extensions
    tm_ext = data.get("com.tokenmoulds", {})

    ooxml = tm_ext.get("ooxml", {})
    extensions.ooxml_theme_slot = ooxml.get("themeSlot")
    extensions.ooxml_tint = ooxml.get("tint")
    extensions.ooxml_shade = ooxml.get("shade")

    derivation = tm_ext.get("derivation", {})
    extensions.derivation_source = derivation.get("source")
    extensions.derivation_formula = derivation.get("formula")

    accessibility = tm_ext.get("accessibility", {})
    extensions.wcag_contrast_ratio = accessibility.get("contrastRatio")
    extensions.wcag_level = accessibility.get("wcagLevel")

    # SVG asset metadata
    extensions.svg_asset = bool(tm_ext.get("svgAsset", False))
    svg_color_map = tm_ext.get("colorMap")
    if isinstance(svg_color_map, dict):
        extensions.svg_color_map = svg_color_map

    # Preserve other vendor extensions
    for key, value in data.items():
        if key != "com.tokenmoulds":
            extensions.other[key] = value

    return extensions
