"""Author styling shared by Page Designer blocks and Form Designer fields.

`styles` maps a part of the element to its properties. The browser turns
them into CSS scoped to that one block/field (frontend lib/elementStyle.ts):
"root" is the block or field itself, the rest reach inside it — its labels,
inputs, buttons, links and any table's th / tr / td. `custom_css` is free
CSS for the same scope, nested under it.

Every value ends up inside a stylesheet, so each is held to a shape that
can't close the declaration or the rule it sits in (no ";", "{", "}", "<").
"""

import re
from typing import Annotated, Literal

from pydantic import AfterValidator, BaseModel, ConfigDict, Field, StringConstraints, model_serializer

StylePart = Literal["root", "label", "input", "button", "link", "th", "tr", "tr_alt", "tr_hover", "td"]

# #fff, #ffffffcc, red, rgb(0 0 0 / 50%), hsl(...), transparent, currentColor
_COLOR = r"^[#a-zA-Z0-9(),.%/\s-]{1,60}$"
# 1 to 4 lengths: "8", "8px 12px", "1rem auto", "50%"
_LENGTH_TOKEN = r"(?:auto|-?\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw)?)"
_LENGTHS = rf"^{_LENGTH_TOKEN}(?:\s+{_LENGTH_TOKEN}){{0,3}}$"
_FONT_FAMILY = r"""^[\w\s,'"-]{1,120}$"""

Color = Annotated[str, StringConstraints(pattern=_COLOR)]
Lengths = Annotated[str, StringConstraints(pattern=_LENGTHS, max_length=60)]
Length = Annotated[str, StringConstraints(pattern=rf"^{_LENGTH_TOKEN}$")]


class StyleProps(BaseModel):
    model_config = ConfigDict(extra="ignore")

    # Text
    color: Color | None = None
    font_family: Annotated[str, StringConstraints(pattern=_FONT_FAMILY)] | None = None
    font_size: Annotated[float, Field(ge=4, le=200)] | None = None  # px
    font_weight: Literal["300", "400", "500", "600", "700", "800"] | None = None
    font_style: Literal["normal", "italic"] | None = None
    text_align: Literal["left", "center", "right", "justify"] | None = None
    text_transform: Literal["none", "uppercase", "lowercase", "capitalize"] | None = None
    text_decoration: Literal["none", "underline", "line-through"] | None = None
    line_height: float | None = None  # unitless multiple of the font size
    letter_spacing: float | None = None  # px
    white_space: Literal["normal", "nowrap", "pre-wrap"] | None = None
    vertical_align: Literal["top", "middle", "bottom"] | None = None
    # Fill and border
    background_color: Color | None = None
    border_width: float | None = None  # px
    border_style: Literal["solid", "dashed", "dotted", "double", "none"] | None = None
    border_color: Color | None = None
    border_radius: float | None = None  # px
    shadow: Literal["none", "sm", "md", "lg"] | None = None
    opacity: Annotated[float, Field(ge=0, le=1)] | None = None
    # Spacing and size
    padding: Lengths | None = None
    margin: Lengths | None = None
    width: Length | None = None
    height: Length | None = None

    # Only what's set — a stored design doesn't carry two dozen nulls per part.
    @model_serializer(mode="wrap")
    def _drop_unset(self, handler):
        return {k: v for k, v in handler(self).items() if v is not None}


ElementStyles = dict[StylePart, StyleProps]


def _check_css(value: str) -> str:
    if "<" in value:
        raise ValueError("Custom CSS can't contain '<'")
    if re.search(r"@import", value, re.IGNORECASE):
        raise ValueError("Custom CSS can't @import other stylesheets")
    return value


CustomCss = Annotated[str, StringConstraints(max_length=20000), AfterValidator(_check_css)]
