from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Any

from openpyxl.cell import Cell
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side

from ._parser import parse_bool

_CSS_COLOR_NAMES = {
    "black": "#000000",
    "white": "#ffffff",
    "red": "#ff0000",
    "green": "#008000",
    "blue": "#0000ff",
    "yellow": "#ffff00",
    "gray": "#808080",
    "grey": "#808080",
    "silver": "#c0c0c0",
    "maroon": "#800000",
    "purple": "#800080",
    "teal": "#008080",
    "navy": "#000080",
    "orange": "#ffa500",
}

_RGB_PATTERN = re.compile(
    r"rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})"
    r"(?:\s*,\s*(\d*\.?\d+))?\s*\)",
    re.IGNORECASE,
)

_BORDER_STYLE_MAP = {
    "hair": "hair",
    "thin": "thin",
    "medium": "medium",
    "thick": "thick",
    "double": "double",
    "dotted": "dotted",
    "dashed": "dashed",
    "dashdot": "dashDot",
    "dashdotdot": "dashDotDot",
    "slantdashdot": "slantDashDot",
}


def merge_styles(*styles: dict[str, str]) -> dict[str, str]:
    merged: dict[str, str] = {}
    for style in styles:
        merged.update(style)
    return merged


def parse_style_string(style: str | None) -> dict[str, str]:
    if not style:
        return {}

    parsed: dict[str, str] = {}
    for declaration in style.split(";"):
        if ":" not in declaration:
            continue
        prop, _, value = declaration.partition(":")
        prop = prop.strip()
        value = value.strip()
        if prop:
            parsed[prop.lower()] = value
    return parsed


def normalize_color(value: str) -> str | None:
    if not value:
        return None

    value = value.strip()
    lower = value.lower()

    if lower in {"none", "transparent"}:
        return None

    if lower in _CSS_COLOR_NAMES:
        value = _CSS_COLOR_NAMES[lower]

    if value.startswith("#"):
        hex_value = value[1:]
        if len(hex_value) == 3:
            hex_value = "".join(ch * 2 for ch in hex_value)
        if len(hex_value) == 6:
            return f"FF{hex_value.upper()}"
        return None

    match = _RGB_PATTERN.match(value)
    if match:
        r, g, b = (max(0, min(255, int(match.group(i) or 0))) for i in range(1, 4))
        return f"FF{r:02X}{g:02X}{b:02X}"

    return value


def apply_styles(cell: Cell, style_map: dict[str, str]) -> None:
    if not style_map:
        return

    state = _StyleState()
    for key, raw_value in style_map.items():
        value = raw_value.strip()
        if value:
            _process_style_property(key, value, state)

    if state.font_args:
        cell.font = cell.font + Font(**state.font_args)
    if state.fill_color:
        cell.fill = PatternFill(
            fill_type="solid",
            fgColor=state.fill_color,
            bgColor=state.fill_color,
        )

    if state.alignment_args or state.wrap is not None or state.rotation is not None:
        alignment = dict(state.alignment_args)
        if state.wrap is not None:
            alignment["wrap_text"] = state.wrap
        if state.rotation is not None:
            alignment["text_rotation"] = max(-90, min(90, state.rotation))
        cell.alignment = cell.alignment + Alignment(**alignment)

    if state.number_format:
        cell.number_format = state.number_format

    if state.border_updates:
        cell.border = Border(
            left=state.border_updates.get("left", cell.border.left),
            right=state.border_updates.get("right", cell.border.right),
            top=state.border_updates.get("top", cell.border.top),
            bottom=state.border_updates.get("bottom", cell.border.bottom),
        )


def apply_styles_to_range(
    get_cell: Any,
    style_map: dict[str, str],
    *,
    start_row: int,
    end_row: int,
    start_column: int,
    end_column: int,
) -> None:
    if not style_map:
        return

    for row in range(start_row, end_row + 1):
        for column in range(start_column, end_column + 1):
            apply_styles(get_cell(row=row, column=column), style_map)


def _parse_border(value: str) -> Side | None:
    tokens = [token for token in re.split(r"\s+", value.strip()) if token]
    if not tokens:
        return None

    style: str | None = None
    color: str | None = None
    width: float | None = None

    for token in tokens:
        low = token.lower()
        if low.endswith("px"):
            width = float(low.rstrip("px"))
        elif low in _BORDER_STYLE_MAP:
            style = _BORDER_STYLE_MAP[low]
        elif _looks_like_color(token):
            color = normalize_color(token)

    if style is None and width is not None:
        if width <= 0.5:
            style = "hair"
        elif width <= 1:
            style = "thin"
        elif width <= 2:
            style = "medium"
        else:
            style = "thick"

    return Side(style=style or "thin", color=color or "FF000000")


def _looks_like_color(value: str) -> bool:
    value = value.strip()
    return (
        value.startswith("#")
        or value.lower() in _CSS_COLOR_NAMES
        or value.lower().startswith("rgb")
    )


def _map_horizontal(value: str) -> str:
    mapping = {
        "left": "left",
        "right": "right",
        "center": "center",
        "centre": "center",
        "justify": "justify",
        "distributed": "distributed",
    }
    return mapping.get(value.lower(), "general")


def _map_vertical(value: str) -> str:
    mapping = {
        "top": "top",
        "bottom": "bottom",
        "center": "center",
        "middle": "center",
        "justify": "justify",
        "distributed": "distributed",
    }
    return mapping.get(value.lower(), "bottom")


@dataclass
class _StyleState:
    font_args: dict[str, Any] = field(default_factory=dict)
    alignment_args: dict[str, Any] = field(default_factory=dict)
    border_updates: dict[str, Side] = field(default_factory=dict)
    fill_color: str | None = None
    number_format: str | None = None
    wrap: bool | None = None
    rotation: int | None = None


def _process_style_property(key: str, value: str, state: _StyleState) -> None:
    low_key = key.lower()
    low_value = value.lower()
    if _apply_font_property(low_key, value, low_value, state):
        return
    if _apply_fill_property(low_key, value, state):
        return
    if _apply_alignment_property(low_key, value, low_value, state):
        return
    if _apply_format_property(low_key, value, state):
        return
    _apply_border_property(low_key, value, state)


def _apply_font_property(
    low_key: str,
    value: str,
    low_value: str,
    state: _StyleState,
) -> bool:
    if low_key == "font-weight":
        state.font_args["bold"] = low_value in {"bold", "700", "600"}
    elif low_key == "font-style":
        state.font_args["italic"] = low_value == "italic"
    elif low_key in {"text-decoration", "text-decoration-line"}:
        if "underline" in low_value:
            state.font_args["underline"] = "single"
        if "line-through" in low_value or "strike" in low_value:
            state.font_args["strike"] = True
    elif low_key == "font-size":
        state.font_args["size"] = float(value.replace("px", "").replace("pt", ""))
    elif low_key == "font-family":
        if font_name := _parse_font_family(value):
            state.font_args["name"] = font_name
    elif low_key == "color":
        if color := normalize_color(value):
            state.font_args["color"] = color
    else:
        return False
    return True


def _apply_fill_property(low_key: str, value: str, state: _StyleState) -> bool:
    if low_key in {"background-color", "bg-color", "fill"}:
        state.fill_color = normalize_color(value)
        return True
    return False


def _apply_alignment_property(
    low_key: str,
    value: str,
    low_value: str,
    state: _StyleState,
) -> bool:
    if low_key in {"text-align", "align"}:
        state.alignment_args["horizontal"] = _map_horizontal(value)
    elif low_key in {"vertical-align", "valign"}:
        state.alignment_args["vertical"] = _map_vertical(value)
    elif low_key in {"wrap-text", "white-space"}:
        state.wrap = low_value in {"true", "wrap", "wrap-text", "pre-wrap"}
    elif low_key in {"text-rotation", "rotation"}:
        state.rotation = int(float(value))
    elif low_key == "shrink-to-fit":
        state.alignment_args["shrink_to_fit"] = parse_bool(value, default=False)
    else:
        return False
    return True


def _apply_format_property(low_key: str, value: str, state: _StyleState) -> bool:
    if low_key in {"number-format", "format"}:
        state.number_format = value
        return True
    return False


def _apply_border_property(low_key: str, value: str, state: _StyleState) -> bool:
    if low_key == "border":
        side = _parse_border(value)
        if side:
            for direction in ("left", "right", "top", "bottom"):
                state.border_updates[direction] = side
        return True
    if low_key.startswith("border-"):
        direction = low_key.split("-", 1)[1]
        side = _parse_border(value)
        if side:
            state.border_updates[direction] = side
        return True
    return False


def _parse_font_family(value: str) -> str | None:
    families = [part.strip() for part in value.split(",")]
    for family in families:
        stripped = family.strip()
        if not stripped:
            continue
        if (stripped.startswith('"') and stripped.endswith('"')) or (
            stripped.startswith("'") and stripped.endswith("'")
        ):
            stripped = stripped[1:-1].strip()
        if stripped:
            return stripped
    return None
