from __future__ import annotations

from dataclasses import dataclass
from io import BytesIO
from typing import Literal
import re

from openpyxl.drawing.image import Image as XLImage
from openpyxl.worksheet.worksheet import Worksheet
from openpyxl.utils import get_column_letter

from xllayout.core.canvas import Anchor, Box
from xllayout.core.theme import Theme


ChartType = Literal["bar", "line", "pie"]

try:
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
except ModuleNotFoundError:  # pragma: no cover
    matplotlib = None
    plt = None


@dataclass
class MatplotlibChart:
    chart_type: ChartType
    categories: list[str]
    values: list[float]
    title: str | None = None
    span_cols: int = 6
    span_rows: int = 12
    rotate_labels: int = 25

    def measure(self, anchor: Anchor, span_rows: int | None = None, span_cols: int | None = None) -> Box:
        width = span_cols if span_cols is not None else self.span_cols
        height = span_rows if span_rows is not None else self.span_rows
        if width <= 0 or height <= 0:
            raise ValueError("MatplotlibChart width/height must be > 0")
        return Box(anchor.row, anchor.col, anchor.row + height - 1, anchor.col + width - 1)

    def draw(self, ws: Worksheet, box: Box, theme: Theme) -> None:
        if plt is None:
            raise RuntimeError("matplotlib is required for MatplotlibChart")

        if not self.categories or not self.values:
            return

        fig_width = max(4.0, box.width * 0.9)
        fig_height = max(2.6, box.height * 0.24)
        fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=120)

        self._draw_chart(ax, theme)
        if self.title:
            ax.set_title(self.title, fontsize=10, fontweight="bold")

        buffer = BytesIO()
        fig.tight_layout()
        fig.savefig(buffer, format="png", bbox_inches="tight")
        plt.close(fig)

        buffer.seek(0)
        image = XLImage(buffer)
        image.width = int(box.width * 72)
        image.height = int(box.height * 18)

        anchor = f"{get_column_letter(box.c1)}{box.r1}"
        ws.add_image(image, anchor)

    def _draw_chart(self, ax, theme: Theme) -> None:
        colors = [self._normalize_color(theme.accent_color), "#6A8FB8", "#9FB8D3", "#C7D6E6"]
        active_colors = [colors[i % len(colors)] for i in range(len(self.values))]

        if self.chart_type == "bar":
            bars = ax.bar(self.categories, self.values, color=active_colors, edgecolor="#E6ECF2")
            ax.tick_params(axis="x", rotation=self.rotate_labels, labelsize=8)
            ax.grid(axis="y", linestyle="--", linewidth=0.5, alpha=0.35)
            for bar, value in zip(bars, self.values):
                ax.text(
                    bar.get_x() + bar.get_width() / 2,
                    bar.get_height(),
                    f"{int(value):,}" if float(value).is_integer() else f"{value:,.2f}",
                    ha="center",
                    va="bottom",
                    fontsize=7,
                )
            return

        if self.chart_type == "line":
            ax.plot(self.categories, self.values, color=theme.accent_color, linewidth=2.0, marker="o", markersize=4)
            ax.tick_params(axis="x", rotation=self.rotate_labels, labelsize=8)
            ax.grid(axis="y", linestyle="--", linewidth=0.5, alpha=0.35)
            return

        # pie
        total = sum(self.values)
        if total <= 0:
            return
        ax.pie(
            self.values,
            labels=self.categories,
            autopct=lambda p: f"{p:.1f}%" if p >= 4 else "",
            startangle=90,
            colors=active_colors,
            textprops={"fontsize": 8},
            wedgeprops={"linewidth": 0.8, "edgecolor": "white"},
        )
        ax.axis("equal")

    @staticmethod
    def _normalize_color(color: str) -> str:
        """
        Normalize color tokens so matplotlib accepts either:
        - '#RRGGBB'
        - 'RRGGBB' (openpyxl-like)
        - 'AARRGGBB' (openpyxl ARGB)
        """
        if not isinstance(color, str):
            return color

        value = color.strip()
        if value.startswith("#"):
            return value

        if re.fullmatch(r"[0-9A-Fa-f]{6}", value):
            return f"#{value}"

        if re.fullmatch(r"[0-9A-Fa-f]{8}", value):
            return f"#{value[2:]}"

        return value
