r"""
ExcelReader - Flexible Excel value extraction by coordinate, anchor text, or regex pattern.

Provides methods for:
- Coordinate: Extract value from a specific cell (e.g., "A1", "B5")
- Anchor: Find anchor text and extract value by applying row/column offset
- Regex anchor: Find text matching a regex pattern and extract value by applying offset

Requires: pip install yclibs[excel]
"""

from __future__ import annotations

import logging
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any

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

logger = logging.getLogger(__name__)


class ExcelError(Exception):
    """Base exception for Excel-related operations."""

    pass


class ExcelReader:
    """
    Read values from Excel files (.xlsx) by coordinate, anchor text, or regex pattern.

    Example:
        reader = ExcelReader("report.xlsx")
        value = reader.coordinate("A1")
        value = reader.anchor("Total:", row_offset=0, col_offset=1)
        value = reader.regex_anchor(r"Q[1-4]\\s*:\\s*", row_offset=0, col_offset=1)
    """

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

        Args:
            path: Path to the Excel file (.xlsx).
            sheet: Sheet name (str), 0-based index (int), or None for active sheet.
            data_only: If True, read cached formula values; if False, read formula strings.
        """
        self._path = Path(path)
        self._sheet = sheet
        self._data_only = data_only
        self._workbook: Workbook | None = None

    def _ensure_workbook(self) -> Workbook:
        """Load workbook lazily; raise ExcelError if file not found or invalid."""
        if self._workbook is not None:
            return self._workbook
        try:
            from openpyxl import 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 not self._path.exists():
            raise ExcelError(f"Excel file not found: {self._path}")
        try:
            self._workbook = load_workbook(
                self._path,
                read_only=False,
                data_only=self._data_only,
            )
        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 from sheet name/index."""
        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:
            raise ExcelError(f"Sheet '{self._sheet}' not found. Available: {wb.sheetnames}")
        return wb[self._sheet]

    def coordinate(self, cell_ref: str) -> Any:
        """
        Extract value from a cell by coordinate.

        Args:
            cell_ref: Cell reference (e.g., "A1", "B5", "AB10").

        Returns:
            The cell value, or None if empty.

        Raises:
            ExcelError: If the coordinate is invalid.
        """
        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()
        try:
            cell = ws[cell_ref]
            return cell.value
        except Exception as e:
            raise ExcelError(f"Invalid cell reference '{cell_ref}'") from e

    def anchor(
        self,
        anchor_text: str,
        *,
        row_offset: int = 0,
        col_offset: int = 0,
        match_case: bool = False,
        exact: bool = False,
    ) -> Any:
        """
        Find anchor text in the sheet and extract value at the given offset.

        Args:
            anchor_text: Text to search for.
            row_offset: Row offset from anchor (positive = down, negative = up).
            col_offset: Column offset from anchor (positive = right, negative = left).
            match_case: If True, match case-sensitively.
            exact: If True, cell value must equal anchor_text; if False, contain it.

        Returns:
            The value at the offset cell, or None if not found.

        Raises:
            ExcelError: If anchor is not found.
        """
        ws = self._get_worksheet()
        anchor_cell = self._find_anchor(ws, anchor_text, match_case, exact)
        if anchor_cell is None:
            raise ExcelError(f"Anchor text '{anchor_text}' not found in sheet")
        return self._value_at_offset(ws, anchor_cell.row, anchor_cell.column, row_offset, col_offset)

    def regex_anchor(
        self,
        pattern: str | re.Pattern[str],
        *,
        row_offset: int = 0,
        col_offset: int = 0,
    ) -> Any:
        """
        Find first cell matching a regex pattern and extract value at the given offset.

        Args:
            pattern: Regex pattern (str or compiled re.Pattern).
            row_offset: Row offset from match (positive = down, negative = up).
            col_offset: Column offset from match (positive = right, negative = left).

        Returns:
            The value at the offset cell, or None if not found.

        Raises:
            ExcelError: If no cell matches the pattern.
        """
        ws = self._get_worksheet()
        match_cell = self._find_regex(ws, pattern)
        if match_cell is None:
            pattern_str = pattern.pattern if isinstance(pattern, re.Pattern) else pattern
            raise ExcelError(f"No cell matches regex pattern: {pattern_str}")
        return self._value_at_offset(
            ws, match_cell.row, match_cell.column, row_offset, col_offset
        )

    def _find_anchor(
        self,
        ws: Worksheet,
        anchor_text: str,
        match_case: bool,
        exact: bool,
    ) -> Any:
        """Return the first cell containing (or equaling) anchor_text."""
        cmp_text = anchor_text if match_case else anchor_text.lower()
        for row in ws.iter_rows():
            for cell in row:
                if cell.value is None:
                    continue
                val_str = str(cell.value)
                check = val_str if match_case else val_str.lower()
                if exact:
                    if check == cmp_text:
                        return cell
                elif cmp_text in check:
                    return cell
        return None

    def _find_regex(self, ws: Worksheet, pattern: str | re.Pattern[str]) -> Any:
        """Return the first cell whose value matches the regex."""
        compiled = re.compile(pattern) if isinstance(pattern, str) else pattern
        for row in ws.iter_rows():
            for cell in row:
                if cell.value is not None and compiled.search(str(cell.value)):
                    return cell
        return None

    def _value_at_offset(
        self,
        ws: Worksheet,
        base_row: int,
        base_col: int,
        row_offset: int,
        col_offset: int,
    ) -> Any:
        """Return the value at (base_row + row_offset, base_col + col_offset)."""
        target_row = base_row + row_offset
        target_col = base_col + col_offset
        if target_row < 1 or target_col < 1:
            return None
        cell = ws.cell(row=target_row, column=target_col)
        return cell.value

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

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

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