r"""
ExcelWriter - Write values and text to Excel files with customizable cell styles.

Supports:
- Creating new Excel files or appending to existing files
- Writing values/text to cells by coordinate
- Custom cell styles: font (name, size, bold, italic), font color, background color

Requires: pip install yclibs[excel]
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from openpyxl.workbook import Workbook
    from openpyxl.worksheet.worksheet import Worksheet

from yclibs.excel.reader import ExcelError

logger = logging.getLogger(__name__)


def _normalize_hex_color(color: str) -> str:
    """Normalize hex color to 8-char AARRGGBB format for openpyxl."""
    s = color.strip().lstrip("#").upper()
    if len(s) == 6:
        return f"00{s}"
    if len(s) == 8:
        return s
    msg = f"Color must be 6 or 8 digit hex (e.g. 'FF0000' or 'FFFF0000'), got: {color}"
    raise ValueError(msg)


@dataclass(frozen=True)
class CellStyle:
    """
    Cell style options for font and color.

    All fields are optional; only specified attributes are applied.

    Attributes:
        font_name: Font family (e.g., 'Arial', 'Calibri').
        font_size: Font size in points.
        bold: Bold text.
        italic: Italic text.
        underline: Underline style ('none', 'single', 'double', 'singleAccounting', 'doubleAccounting').
        font_color: Font color as hex RRGGBB or AARRGGBB (e.g., 'FF0000' for red).
        bg_color: Background fill color as hex RRGGBB or AARRGGBB (e.g., 'FFFF00' for yellow).
    """

    font_name: str | None = None
    font_size: int | float | None = None
    bold: bool | None = None
    italic: bool | None = None
    underline: str | None = None
    font_color: str | None = None
    bg_color: str | None = None

    def _to_openpyxl_font(self) -> Any | None:
        """Build openpyxl Font from this style, or None if no font attributes."""
        from openpyxl.styles import Font

        kwargs: dict[str, Any] = {}
        if self.font_name is not None:
            kwargs["name"] = self.font_name
        if self.font_size is not None:
            kwargs["size"] = self.font_size
        if self.bold is not None:
            kwargs["bold"] = self.bold
        if self.italic is not None:
            kwargs["italic"] = self.italic
        if self.underline is not None:
            kwargs["underline"] = self.underline
        if self.font_color is not None:
            kwargs["color"] = _normalize_hex_color(self.font_color)
        if not kwargs:
            return None
        return Font(**kwargs)

    def _to_openpyxl_fill(self) -> Any | None:
        """Build openpyxl PatternFill from this style, or None if no fill."""
        if self.bg_color is None:
            return None
        from openpyxl.styles import PatternFill

        return PatternFill(
            fill_type="solid",
            fgColor=_normalize_hex_color(self.bg_color),
        )

    def apply_to_cell(self, cell: Any) -> None:
        """Apply this style to an openpyxl cell."""
        font = self._to_openpyxl_font()
        if font is not None:
            cell.font = font
        fill = self._to_openpyxl_fill()
        if fill is not None:
            cell.fill = fill


class ExcelWriter:
    """
    Write values and formatted text to new or existing Excel files (.xlsx).

    Example:
        writer = ExcelWriter("output.xlsx", create_new=True)
        writer.write("A1", "Title", CellStyle(bold=True, font_size=14))
        writer.write("B1", 123.45, CellStyle(bg_color="FFFF00"))
        writer.save()

        # Append to existing file
        writer = ExcelWriter("existing.xlsx")
        writer.write("A5", "New value")
        writer.save()
    """

    def __init__(
        self,
        path: Path | str,
        *,
        create_new: bool = False,
        sheet: str | int | None = None,
    ) -> None:
        """
        Initialize the Excel writer.

        Args:
            path: Path to the Excel file (.xlsx).
            create_new: If True, always create a new workbook (overwrites existing).
                If False, load existing file or create new if it doesn't exist.
            sheet: Sheet name (str), 0-based index (int), or None for active sheet.
        """
        self._path = Path(path)
        self._create_new = create_new
        self._sheet = sheet
        self._workbook: Workbook | None = None

    def _ensure_workbook(self) -> Workbook:
        """Load or create workbook lazily."""
        if self._workbook is not None:
            return self._workbook
        try:
            from openpyxl import Workbook, load_workbook
        except ImportError as e:
            msg = "openpyxl is required for Excel support. Install with: pip install yclibs[excel]"
            raise ExcelError(msg) from e
        if self._create_new or not self._path.exists():
            self._workbook = Workbook()
            logger.debug("Created new workbook for %s", self._path)
        else:
            try:
                self._workbook = load_workbook(self._path, read_only=False)
                logger.debug("Loaded existing workbook: %s", self._path)
            except Exception as e:
                raise ExcelError(f"Failed to load Excel file: {self._path}") from e
        return self._workbook

    def _get_worksheet(self) -> Worksheet:
        """Resolve the target worksheet."""
        wb = self._ensure_workbook()
        if self._sheet is None:
            return wb.active
        if isinstance(self._sheet, int):
            if self._sheet < 0 or self._sheet >= len(wb.worksheets):
                raise ExcelError(
                    f"Sheet index {self._sheet} out of range (0..{len(wb.worksheets) - 1})"
                )
            return wb.worksheets[self._sheet]
        if self._sheet not in wb.sheetnames:
            return wb.create_sheet(self._sheet)
        return wb[self._sheet]

    def write(
        self,
        cell_ref: str,
        value: Any,
        style: CellStyle | None = None,
    ) -> ExcelWriter:
        """
        Write a value to a cell, optionally with style.

        Args:
            cell_ref: Cell reference (e.g., "A1", "B5").
            value: Value to write (str, int, float, datetime, etc.).
            style: Optional CellStyle for font and color.

        Returns:
            self for method chaining.
        """
        ws = self._get_worksheet()
        if not cell_ref or not isinstance(cell_ref, str):
            raise ExcelError("Cell reference must be a non-empty string")
        cell_ref = cell_ref.strip().upper()
        cell = ws[cell_ref]
        cell.value = value
        if style is not None:
            style.apply_to_cell(cell)
        return self

    def add_sheet(self, name: str, *, make_active: bool = False) -> ExcelWriter:
        """
        Add a new sheet.

        Args:
            name: Sheet name.
            make_active: If True, set as the active sheet for subsequent writes.

        Returns:
            self for method chaining.
        """
        wb = self._ensure_workbook()
        if name in wb.sheetnames:
            raise ExcelError(f"Sheet '{name}' already exists")
        ws = wb.create_sheet(name)
        if make_active:
            wb.active = ws
            self._sheet = name
        return self

    def save(self) -> Path:
        """
        Save the workbook to the configured path.

        Returns:
            Path to the saved file.

        Raises:
            ExcelError: If save fails.
        """
        wb = self._ensure_workbook()
        self._path.parent.mkdir(parents=True, exist_ok=True)
        try:
            wb.save(self._path)
        except Exception as e:
            raise ExcelError(f"Failed to save Excel file: {self._path}") from e
        logger.debug("Saved workbook: %s", self._path)
        return self._path

    def close(self) -> None:
        """Release workbook resources."""
        if self._workbook is not None:
            self._workbook.close()
            self._workbook = None
            logger.debug("Closed workbook: %s", self._path)

    def __enter__(self) -> ExcelWriter:
        self._ensure_workbook()
        return self

    def __exit__(self, *args: object) -> None:
        self.close()
