"""Formula evaluation for token resolution."""

from __future__ import annotations

import ast
import re
from collections.abc import Mapping
from typing import Any, Callable, Dict

from tokenmoulds.colors.oklab import oklch_to_rgb, rgb_to_oklch
from tokenmoulds.colors.transforms import (
    contrast_color,
    darken,
    desaturate,
    hex_to_hsl,
    hsl_rotate,
    hsl_to_hex,
    lighten,
)
from tokenmoulds.design.units import EMU_PER_POINT
from tokenmoulds.dtcg.errors import FormulaError, FormulaReferenceError

FormulaFunction = Callable[..., Any]

_NUMERIC_ATTR_RE = re.compile(r"(\]|\b[A-Za-z_][A-Za-z0-9_]*)\.(\d+)")


def _normalize_numeric_attrs(expression: str) -> str:
    """Allow dotted numeric keys by translating to bracket syntax."""
    return _NUMERIC_ATTR_RE.sub(r'\1["\2"]', expression)


def is_formula(value: object) -> bool:
    if not isinstance(value, str):
        return False
    stripped = value.strip()
    return (stripped.startswith("${") and stripped.endswith("}")) or (
        stripped.startswith("{{") and stripped.endswith("}}")
    )


def strip_formula(value: str) -> str:
    stripped = value.strip()
    if stripped.startswith("${") and stripped.endswith("}"):
        return stripped[2:-1].strip()
    if stripped.startswith("{{") and stripped.endswith("}}"):
        return stripped[2:-2].strip()
    return stripped


def pt_to_twips(value: float) -> int:
    """Convert points to twips (1pt = 20 twips)."""
    return int(round(value * 20))


def twips_to_emu(value: int) -> int:
    """Convert twips to EMU (1 twip = 635 EMU)."""
    return int(round(value * 635))


def pt_to_emu(value: float) -> int:
    """Convert points to EMU (1pt = 12700 EMU)."""
    return int(round(value * EMU_PER_POINT))


def pt_to_hundredths(value: float) -> int:
    """Convert points to hundredths of a point (PowerPoint font sizes)."""
    return int(round(value * 100))


def inches_to_emu(value: float) -> int:
    """Convert inches to EMU (1 inch = 914400 EMU)."""
    return int(round(value * 914400))


def scale_by_ratio(base: float, ratio: float, step: int) -> float:
    """Calculate modular scale value: base * (ratio ^ step)."""
    return base * (ratio**step)


# =============================================================================
# STYLE PRIMITIVE CONSTRUCTORS
# =============================================================================


def mod_stack(*modifiers: Dict[str, Any]) -> list:
    """Create an ordered color modifier stack.

    Order matters! Each modifier is applied sequentially.

    Args:
        *modifiers: Dicts with "type" and optional "value" keys

    Example:
        >>> mod_stack(
        ...     {"type": "tint", "value": 50},
        ...     {"type": "satMod", "value": 150},
        ...     {"type": "lumMod", "value": 90},
        ... )

    In token JSON:
        "$value": "${mod_stack({'type': 'tint', 'value': 50}, {'type': 'satMod', 'value': 150})}"
    """
    return list(modifiers)


def gradient_linear(angle: float = 0, rotate: bool = True) -> Dict[str, Any]:
    """Create linear gradient extension at any angle.

    Args:
        angle: Gradient angle in degrees (0-360, any value!)
        rotate: Whether gradient rotates with shape

    Example:
        >>> gradient_linear(67.5)  # 67.5° - not in GUI!
    """
    return {
        "type": "linear",
        "angle": angle,
        "rotateWithShape": rotate,
    }


def gradient_radial(
    path_shape: str = "circle",
    focus_x: float = 50,
    focus_y: float = 50,
) -> Dict[str, Any]:
    """Create radial gradient with offset center.

    Args:
        path_shape: "circle", "rect", or "shape"
        focus_x: Horizontal center position (50 = centered, can be negative!)
        focus_y: Vertical center position (50 = centered, can be negative!)

    Example:
        >>> gradient_radial("circle", 50, -80)  # Light from above
    """
    return {
        "type": "path",
        "pathShape": path_shape,
        "fillToRect": {
            "l": focus_x,
            "t": focus_y,
            "r": 100 - focus_x + 50,
            "b": 100 - focus_y + 50,
        },
    }


def dash_pattern(*segments: tuple) -> list:
    """Create custom dash pattern.

    Args:
        *segments: Tuples of (dash, space) ratios

    Example:
        >>> dash_pattern((3, 1))  # 3:1 dash-to-space
        >>> dash_pattern((3, 1), (1, 1))  # Alternating long-short
    """
    return [{"dash": seg[0], "space": seg[1]} for seg in segments]


def scene_3d(
    camera_preset: str = "orthographicFront",
    camera_tilt: float = 0,
    camera_pan: float = 0,
    camera_roll: float = 0,
    light_preset: str = "threePt",
    light_dir: str = "t",
    light_roll: float = 0,
) -> Dict[str, Any]:
    """Create 3D scene with custom camera and lighting.

    Args:
        camera_preset: Camera preset name
        camera_tilt: Latitude rotation (degrees)
        camera_pan: Longitude rotation (degrees)
        camera_roll: Revolution rotation (degrees)
        light_preset: Lighting preset name
        light_dir: Light direction (t, b, l, r, tl, tr, bl, br)
        light_roll: Light rotation (degrees)

    Example:
        >>> scene_3d(camera_tilt=20, light_roll=30)  # Custom angles!
    """
    return {
        "camera": {
            "preset": camera_preset,
            "rotation": {"lat": camera_tilt, "lon": camera_pan, "rev": camera_roll},
        },
        "lightRig": {
            "preset": light_preset,
            "direction": light_dir,
            "rotation": {"lat": 0, "lon": 0, "rev": light_roll},
        },
    }


def shadow(
    direction: float,
    distance: float,
    blur: float,
    alpha: float,
    inner: bool = False,
) -> Dict[str, Any]:
    """Create shadow effect at any angle.

    All values must be provided from design tokens - no defaults.

    Args:
        direction: Shadow direction in degrees (0-360, any angle not just GUI presets)
        distance: Shadow offset in points (>= 0)
        blur: Blur radius in points (>= 0)
        alpha: Shadow opacity (0-100)
        inner: True for inner shadow

    Example:
        >>> shadow(60, 3, 4, 40)  # 60° angle - not a GUI preset!
    """
    return {
        "direction": direction,
        "distance": distance,
        "blur": blur,
        "alpha": alpha,
        "inner": inner,
    }


def reflection(
    blur: float,
    distance: float,
    direction: float,
    start_alpha: float,
    end_alpha: float,
    fade_direction: float,
    scale_x: float,
    scale_y: float,
    align: str,
    rotate_with_shape: bool = True,
) -> Dict[str, Any]:
    """Create reflection effect with full control.

    This is a hidden feature - GUI only offers presets, but XML allows
    full control over all parameters.

    All values must be provided from design tokens - no defaults.

    Args:
        blur: Blur radius in points (>= 0)
        distance: Gap between shape and reflection in points (>= 0)
        direction: Direction in degrees (0-360, typically 90 = below)
        start_alpha: Start opacity (0-100)
        end_alpha: End opacity (0-100)
        fade_direction: Fade direction in degrees (0-360)
        scale_x: Horizontal scale (-100 to 100, negative = flip)
        scale_y: Vertical scale (-100 to 100, negative = flip)
        align: Alignment ("b", "t", "l", "r", "ctr")
        rotate_with_shape: Whether reflection rotates with shape

    Example:
        >>> reflection(0.5, 0, 90, 50, 0, 90, 100, -100, "b")  # Floor reflection
    """
    return {
        "blur": blur,
        "distance": distance,
        "direction": direction,
        "startAlpha": start_alpha,
        "endAlpha": end_alpha,
        "fadeDirection": fade_direction,
        "scaleX": scale_x,
        "scaleY": scale_y,
        "align": align,
        "rotateWithShape": rotate_with_shape,
    }


def soft_edge(radius: float) -> Dict[str, Any]:
    """Create soft edge (feather) effect.

    Args:
        radius: Blur radius in points (>= 0)

    Example:
        >>> soft_edge(2)  # 2pt feather
    """
    return {"radius": radius}


def bevel(
    preset: str,
    width: float,
    height: float,
    position: str = "top",
) -> Dict[str, Any]:
    """Create 3D bevel effect.

    Presets: relaxedInset, circle, slope, cross, angle,
             softRound, convex, coolSlant, divot, riblet,
             hardEdge, artDeco

    Args:
        preset: Bevel preset name
        width: Bevel width in points (>= 0)
        height: Bevel height in points (>= 0)
        position: "top" or "bottom"

    Example:
        >>> bevel("relaxedInset", 6, 6, "top")
    """
    return {
        "preset": preset,
        "width": width,
        "height": height,
        "position": position,
    }


def tint(percent: float) -> Dict[str, Any]:
    """Create a tint modifier step."""
    return {"type": "tint", "value": percent}


def shade(percent: float) -> Dict[str, Any]:
    """Create a shade modifier step."""
    return {"type": "shade", "value": percent}


def sat_mod(percent: float) -> Dict[str, Any]:
    """Create a saturation modifier step (100 = no change)."""
    return {"type": "satMod", "value": percent}


def lum_mod(percent: float) -> Dict[str, Any]:
    """Create a luminance modifier step (100 = no change)."""
    return {"type": "lumMod", "value": percent}


def alpha_mod(percent: float) -> Dict[str, Any]:
    """Create an alpha modifier step."""
    return {"type": "alpha", "value": percent}


def complement() -> Dict[str, Any]:
    """Create a complement modifier (180° hue flip)."""
    return {"type": "comp"}


def grayscale() -> Dict[str, Any]:
    """Create a grayscale modifier."""
    return {"type": "gray"}


# =============================================================================
# AI-EDITABLE DESIGN TOKEN FUNCTIONS
# =============================================================================


def hue_rotate(color: str, degrees: float) -> str:
    """Rotate hue of a hex color by degrees in OKLCh perceptual color space.

    Unlike HSL rotation, OKLCh preserves perceptual lightness and chroma,
    producing visually balanced accent colors regardless of input hue.
    """
    color = color.lstrip("#")
    r = int(color[0:2], 16) / 255.0
    g = int(color[2:4], 16) / 255.0
    b = int(color[4:6], 16) / 255.0

    l, c, h = rgb_to_oklch(r, g, b)
    h = (h + degrees) % 360.0
    r_out, g_out, b_out = oklch_to_rgb(l, c, h)

    ri = max(0, min(255, round(r_out * 255)))
    gi = max(0, min(255, round(g_out * 255)))
    bi = max(0, min(255, round(b_out * 255)))
    return f"#{ri:02X}{gi:02X}{bi:02X}"


_DOCUMENT_PRESETS: Dict[str, Dict[str, Any]] = {
    "report": {
        "typeScale": 1.250,
        "bodySize": 12,
        "margins": {"top": 1.0, "bottom": 1.0, "left": 1.25, "right": 1.25},
        "columns": 1,
    },
    "presentation": {
        "typeScale": 1.414,
        "bodySize": 24,
        "margins": {"top": 0.5, "bottom": 0.5, "left": 0.75, "right": 0.75},
        "columns": 1,
    },
    "letter": {
        "typeScale": 1.125,
        "bodySize": 12,
        "margins": {"top": 1.0, "bottom": 1.0, "left": 1.0, "right": 1.0},
        "columns": 1,
    },
    "memo": {
        "typeScale": 1.125,
        "bodySize": 11,
        "margins": {"top": 1.0, "bottom": 1.0, "left": 1.25, "right": 1.25},
        "columns": 1,
    },
    "book": {
        "typeScale": 1.200,
        "bodySize": 11,
        "margins": {"top": 1.0, "bottom": 1.0, "left": 1.5, "right": 1.0},
        "columns": 1,
    },
    "newsletter": {
        "typeScale": 1.250,
        "bodySize": 10,
        "margins": {"top": 0.75, "bottom": 0.75, "left": 0.75, "right": 0.75},
        "columns": 2,
    },
}


def document_preset(doc_type: str) -> Dict[str, Any]:
    """Return typography and layout preset for a document type."""
    if doc_type not in _DOCUMENT_PRESETS:
        msg = f"Unknown document type: {doc_type!r}. Valid: {', '.join(_DOCUMENT_PRESETS)}"
        raise FormulaError(msg)
    return _DOCUMENT_PRESETS[doc_type]


_FONT_PRESETS: Dict[str, Dict[str, str]] = {
    "corporate": {"sans": "IBM Plex Sans", "serif": "IBM Plex Serif"},
    "technical": {"sans": "Source Sans 3", "serif": "Source Serif 4"},
    "marketing": {"sans": "Montserrat", "serif": "Merriweather"},
    "multilingual": {"sans": "Noto Sans", "serif": "Noto Serif"},
    "digital": {"sans": "Inter", "serif": "Lora"},
}


def font_preset(name: str) -> Dict[str, str]:
    """Return sans/serif font families for a named preset."""
    if name not in _FONT_PRESETS:
        msg = f"Unknown font preset: {name!r}. Valid: {', '.join(_FONT_PRESETS)}"
        raise FormulaError(msg)
    return _FONT_PRESETS[name]


_ICON_FONT_FAMILIES: Dict[str, str] = {
    "tabler": "Tabler Icons",
    "fontawesome": "Font Awesome 6 Free",
    "phosphor": "Phosphor",
    "none": "",
}


def icon_font_family(set_name: str) -> str:
    """Map icon set name to its font family string."""
    if set_name not in _ICON_FONT_FAMILIES:
        msg = f"Unknown icon set: {set_name!r}. Valid: {', '.join(_ICON_FONT_FAMILIES)}"
        raise FormulaError(msg)
    return _ICON_FONT_FAMILIES[set_name]


class FormulaEvaluator:
    """Safe mathematical expression evaluator for token formulas."""

    def __init__(
        self, extra_functions: Mapping[str, FormulaFunction] | None = None
    ) -> None:
        self.functions: Dict[str, FormulaFunction] = {
            # Math functions
            "round": round,
            "abs": abs,
            "min": min,
            "max": max,
            "sqrt": lambda value: value**0.5,
            "floor": lambda value: int(value // 1),
            "ceil": lambda value: int(-(-value // 1)),
            "int": int,
            # Color functions
            "lighten": lighten,
            "darken": darken,
            "hex_to_hsl": hex_to_hsl,
            "hsl_to_hex": hsl_to_hex,
            "hsl_rotate": hsl_rotate,
            "desaturate": desaturate,
            "contrast_color": contrast_color,
            # Unit conversion functions
            "pt_to_twips": pt_to_twips,
            "twips_to_emu": twips_to_emu,
            "pt_to_emu": pt_to_emu,
            "pt_to_hundredths": pt_to_hundredths,
            "inches_to_emu": inches_to_emu,
            # Typography scale functions
            "scale_by_ratio": scale_by_ratio,
            # Style primitive constructors
            "mod_stack": mod_stack,
            "gradient_linear": gradient_linear,
            "gradient_radial": gradient_radial,
            "dash_pattern": dash_pattern,
            "scene_3d": scene_3d,
            "shadow": shadow,
            "reflection": reflection,
            "soft_edge": soft_edge,
            "bevel": bevel,
            # Modifier step helpers
            "tint": tint,
            "shade": shade,
            "sat_mod": sat_mod,
            "lum_mod": lum_mod,
            "alpha_mod": alpha_mod,
            "complement": complement,
            "grayscale": grayscale,
            # AI-editable design token functions
            "hue_rotate": hue_rotate,
            "document_preset": document_preset,
            "font_preset": font_preset,
            "icon_font_family": icon_font_family,
        }
        if extra_functions:
            self.functions.update(extra_functions)

    def evaluate(self, expression: str, context: Mapping[str, Any]) -> Any:
        expression = _normalize_numeric_attrs(expression)
        try:
            tree = ast.parse(expression, mode="eval")
        except SyntaxError as exc:
            raise FormulaError(f"Invalid formula syntax: {expression}") from exc
        return self._eval_node(tree.body, context)

    def _eval_node(self, node: ast.AST, context: Mapping[str, Any]) -> Any:
        if isinstance(node, ast.Constant):
            return node.value
        if isinstance(node, ast.Name):
            return self._resolve_name(node.id, context)
        if isinstance(node, ast.Attribute):
            base = self._eval_node(node.value, context)
            return self._resolve_attr(base, node.attr)
        if isinstance(node, ast.Subscript):
            base = self._eval_node(node.value, context)
            key = (
                self._eval_node(node.slice, context)
                if not isinstance(node.slice, ast.Slice)
                else None
            )
            if key is None:
                raise FormulaError("Slices are not supported in formulas")
            return self._resolve_subscript(base, key)
        if isinstance(node, ast.BinOp):
            left = self._eval_node(node.left, context)
            right = self._eval_node(node.right, context)
            return self._apply_operator(node.op, left, right)
        if isinstance(node, ast.UnaryOp):
            operand = self._eval_node(node.operand, context)
            return self._apply_unary(node.op, operand)
        if isinstance(node, ast.BoolOp):
            values = [self._eval_node(val, context) for val in node.values]
            if isinstance(node.op, ast.And):
                return all(values)
            if isinstance(node.op, ast.Or):
                return any(values)
            raise FormulaError("Unsupported boolean operator")
        if isinstance(node, ast.Compare):
            left = self._eval_node(node.left, context)
            for op, comparator in zip(node.ops, node.comparators):
                right = self._eval_node(comparator, context)
                if not self._apply_compare(op, left, right):
                    return False
                left = right
            return True
        if isinstance(node, ast.IfExp):
            test = self._eval_node(node.test, context)
            return (
                self._eval_node(node.body, context)
                if test
                else self._eval_node(node.orelse, context)
            )
        if isinstance(node, ast.Call):
            func = node.func
            if isinstance(func, ast.Name) and func.id in self.functions:
                args = [self._eval_node(arg, context) for arg in node.args]
                return self.functions[func.id](*args)
            raise FormulaError("Unsupported function call in formula")
        if isinstance(node, ast.List):
            return [self._eval_node(elt, context) for elt in node.elts]
        if isinstance(node, ast.Tuple):
            return tuple(self._eval_node(elt, context) for elt in node.elts)
        if isinstance(node, ast.Dict):
            return {
                self._eval_node(key, context): self._eval_node(value, context)
                for key, value in zip(node.keys, node.values)
                if key is not None
            }
        raise FormulaError(f"Unsupported formula expression: {ast.dump(node)}")

    def _resolve_name(self, name: str, context: Mapping[str, Any]) -> Any:
        if name in {"True", "False", "None"}:
            return {"True": True, "False": False, "None": None}[name]
        if name not in context:
            raise FormulaReferenceError(f"Unknown token reference: {name}")
        value = context[name]
        if is_formula(value):
            raise FormulaReferenceError(f"Unresolved token reference: {name}")
        return value

    def _resolve_attr(self, base: Any, attr: str) -> Any:
        if isinstance(base, Mapping):
            if attr not in base:
                raise FormulaReferenceError(f"Unknown token reference: {attr}")
            value = base[attr]
            if is_formula(value):
                raise FormulaReferenceError(f"Unresolved token reference: {attr}")
            return value
        if hasattr(base, attr):
            value = getattr(base, attr)
            if is_formula(value):
                raise FormulaReferenceError(f"Unresolved token reference: {attr}")
            return value
        raise FormulaReferenceError(f"Attribute {attr} not found in formula context")

    def _resolve_subscript(self, base: Any, key: Any) -> Any:
        try:
            value = base[key]
        except Exception as exc:
            raise FormulaError(f"Invalid subscript access in formula: {key}") from exc
        if is_formula(value):
            raise FormulaReferenceError(f"Unresolved token reference: {key}")
        return value

    @staticmethod
    def _apply_operator(op: ast.operator, left: Any, right: Any) -> Any:
        if isinstance(op, ast.Add):
            return left + right
        if isinstance(op, ast.Sub):
            return left - right
        if isinstance(op, ast.Mult):
            return left * right
        if isinstance(op, ast.Div):
            return left / right
        if isinstance(op, ast.Mod):
            return left % right
        if isinstance(op, ast.Pow):
            if isinstance(right, (int, float)) and abs(right) > 1000:
                raise FormulaError(
                    f"Exponent too large ({right}); maximum allowed is 1000"
                )
            return left**right
        raise FormulaError("Unsupported operator in formula")

    @staticmethod
    def _apply_unary(op: ast.unaryop, value: Any) -> Any:
        if isinstance(op, ast.UAdd):
            return +value
        if isinstance(op, ast.USub):
            return -value
        raise FormulaError("Unsupported unary operator in formula")

    @staticmethod
    def _apply_compare(op: ast.cmpop, left: Any, right: Any) -> bool:
        if isinstance(op, ast.Eq):
            return left == right
        if isinstance(op, ast.NotEq):
            return left != right
        if isinstance(op, ast.Lt):
            return left < right
        if isinstance(op, ast.LtE):
            return left <= right
        if isinstance(op, ast.Gt):
            return left > right
        if isinstance(op, ast.GtE):
            return left >= right
        raise FormulaError("Unsupported comparison in formula")
