from __future__ import annotations

from copy import copy
from dataclasses import dataclass
from typing import Literal

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


HorizontalAlign = Literal["left", "center", "right", "justify", "fill", "general"]
VerticalAlign = Literal["top", "center", "bottom", "justify", "distributed"]


@dataclass(frozen=True)
class CellStyleSpec:
    font_name: str | None = None
    font_size: float | None = None
    bold: bool | None = None
    italic: bool | None = None
    underline: str | bool | None = None
    font_color: str | None = None

    fill_color: str | None = None

    align_horizontal: HorizontalAlign | None = None
    align_vertical: VerticalAlign | None = None
    wrap_text: bool | None = None
    shrink_to_fit: bool | None = None
    text_rotation: int | None = None

    border_style: str | None = None
    border_color: str | None = None

    number_format: str | None = None


_EMPTY_STYLE = CellStyleSpec()


def merge_cell_styles(*styles: CellStyleSpec | None) -> CellStyleSpec | None:
    merged = _EMPTY_STYLE
    has_value = False

    for style in styles:
        if style is None:
            continue
        has_value = True
        merged = CellStyleSpec(
            font_name=style.font_name if style.font_name is not None else merged.font_name,
            font_size=style.font_size if style.font_size is not None else merged.font_size,
            bold=style.bold if style.bold is not None else merged.bold,
            italic=style.italic if style.italic is not None else merged.italic,
            underline=style.underline if style.underline is not None else merged.underline,
            font_color=style.font_color if style.font_color is not None else merged.font_color,
            fill_color=style.fill_color if style.fill_color is not None else merged.fill_color,
            align_horizontal=(
                style.align_horizontal if style.align_horizontal is not None else merged.align_horizontal
            ),
            align_vertical=style.align_vertical if style.align_vertical is not None else merged.align_vertical,
            wrap_text=style.wrap_text if style.wrap_text is not None else merged.wrap_text,
            shrink_to_fit=style.shrink_to_fit if style.shrink_to_fit is not None else merged.shrink_to_fit,
            text_rotation=style.text_rotation if style.text_rotation is not None else merged.text_rotation,
            border_style=style.border_style if style.border_style is not None else merged.border_style,
            border_color=style.border_color if style.border_color is not None else merged.border_color,
            number_format=style.number_format if style.number_format is not None else merged.number_format,
        )

    if not has_value:
        return None
    return merged


def apply_cell_style(
    cell,
    spec: CellStyleSpec | None,
    default_font: Font | None = None,
    default_fill: PatternFill | None = None,
    default_border: Border | None = None,
    default_alignment: Alignment | None = None,
) -> None:
    if spec is None:
        return

    font = _build_font(base=copy(default_font or cell.font), spec=spec)
    fill = _build_fill(base=copy(default_fill or cell.fill), spec=spec)
    border = _build_border(base=copy(default_border or cell.border), spec=spec)
    alignment = _build_alignment(base=copy(default_alignment or cell.alignment), spec=spec)

    cell.font = font
    cell.fill = fill
    cell.border = border
    cell.alignment = alignment

    if spec.number_format is not None:
        cell.number_format = spec.number_format


def _build_font(base: Font, spec: CellStyleSpec) -> Font:
    if spec.font_name is not None:
        base.name = spec.font_name
    if spec.font_size is not None:
        base.sz = spec.font_size
    if spec.bold is not None:
        base.bold = spec.bold
    if spec.italic is not None:
        base.italic = spec.italic
    if spec.underline is not None:
        base.underline = "single" if spec.underline is True else spec.underline
    if spec.font_color is not None:
        base.color = _normalize_color(spec.font_color)
    return base


def _build_fill(base: PatternFill, spec: CellStyleSpec) -> PatternFill:
    if spec.fill_color is None:
        return base
    base.fill_type = "solid"
    normalized = _normalize_color(spec.fill_color)
    base.fgColor = normalized
    return base


def _build_border(base: Border, spec: CellStyleSpec) -> Border:
    if spec.border_style is None and spec.border_color is None:
        return base

    def _side(existing: Side) -> Side:
        style = spec.border_style if spec.border_style is not None else existing.style
        color = _normalize_color(spec.border_color) if spec.border_color is not None else existing.color
        if style is None and color is not None:
            style = "thin"
        return Side(style=style, color=color)

    base.left = _side(base.left)
    base.right = _side(base.right)
    base.top = _side(base.top)
    base.bottom = _side(base.bottom)
    return base


def _build_alignment(base: Alignment, spec: CellStyleSpec) -> Alignment:
    if spec.align_horizontal is not None:
        base.horizontal = spec.align_horizontal
    if spec.align_vertical is not None:
        base.vertical = spec.align_vertical
    if spec.wrap_text is not None:
        base.wrap_text = spec.wrap_text
    if spec.shrink_to_fit is not None:
        base.shrink_to_fit = spec.shrink_to_fit
    if spec.text_rotation is not None:
        base.text_rotation = spec.text_rotation
    return base


def _normalize_color(color: str) -> str:
    value = color.strip()
    if value.startswith("#"):
        value = value[1:]
    if len(value) == 8:
        return value.upper()
    if len(value) == 6:
        return value.upper()
    return value
