"""Spreadsheet domain entities."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Mapping, Sequence

from .enums import SortDirection, SyncMode
from .exceptions import DuplicateSheetError, InvalidCellAddressError, InvalidRowError, InvalidSheetError, InvalidSpreadsheetError
from .value_objects import CellAddress, SheetName


def _index_to_column(index: int) -> str:
    if index <= 0:
        raise InvalidCellAddressError("column index must be positive")
    letters = ""
    while index:
        index, remainder = divmod(index - 1, 26)
        letters = chr(65 + remainder) + letters
    return letters


@dataclass
class SheetRow:
    values: dict[str, Any]
    index: int
    created_at: datetime = field(default_factory=datetime.utcnow)
    updated_at: datetime | None = None

    def __post_init__(self) -> None:
        if isinstance(self.index, bool) or not isinstance(self.index, int) or self.index <= 0:
            raise InvalidRowError("row index must be a positive integer")
        if not isinstance(self.values, dict):
            raise InvalidRowError("row values must be a dictionary")


@dataclass
class Sheet:
    name: SheetName
    headers: list[str] = field(default_factory=list)
    rows: list[SheetRow] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if not isinstance(self.name, SheetName):
            raise InvalidSheetError("name must be a SheetName instance")
        if any(not isinstance(header, str) or not header.strip() for header in self.headers):
            raise InvalidSheetError("headers must contain non-empty strings")
        if len(self.headers) != len(set(self.headers)):
            raise InvalidSheetError("headers cannot contain duplicates")
        if any(not isinstance(row, SheetRow) for row in self.rows):
            raise InvalidSheetError("rows must contain SheetRow instances")

    def ensure_header(self, header: str) -> str:
        if not isinstance(header, str) or not header.strip():
            raise InvalidSheetError("header must be a non-empty string")
        header = header.strip()
        if header not in self.headers:
            self.headers.append(header)
        return header

    def append_row(self, values: Mapping[str, Any] | Sequence[Any]) -> SheetRow:
        if isinstance(values, Mapping):
            row_values = dict(values)
            for header in row_values:
                self.ensure_header(header)
        else:
            if not self.headers:
                raise InvalidRowError("sheet must have headers before appending a sequence row")
            if len(values) != len(self.headers):
                raise InvalidRowError("sequence row length must match sheet headers")
            row_values = {header: values[index] for index, header in enumerate(self.headers)}

        row = SheetRow(values={header: row_values.get(header) for header in self.headers}, index=len(self.rows) + 1)
        self.rows.append(row)
        return row

    def _ensure_row_exists(self, row_number: int) -> SheetRow:
        if row_number <= 1:
            raise InvalidCellAddressError("data rows start at row 2 because row 1 contains headers")
        data_index = row_number - 2
        while len(self.rows) <= data_index:
            self.rows.append(SheetRow(values={}, index=len(self.rows) + 1))
        return self.rows[data_index]

    def get_cell(self, address: CellAddress | str) -> Any:
        if isinstance(address, str):
            address = CellAddress(address)
        header_index = address.column_index - 1
        if address.row == 1:
            return self.headers[header_index] if header_index < len(self.headers) else None
        if header_index >= len(self.headers):
            return None
        row_index = address.row - 2
        if row_index < 0 or row_index >= len(self.rows):
            return None
        return self.rows[row_index].values.get(self.headers[header_index])

    def set_cell(self, address: CellAddress | str, value: Any) -> None:
        if isinstance(address, str):
            address = CellAddress(address)
        header_index = address.column_index - 1
        header_name = _index_to_column(address.column_index)
        if address.row == 1:
            while len(self.headers) <= header_index:
                self.headers.append(_index_to_column(len(self.headers) + 1))
            self.headers[header_index] = str(value).strip() if value is not None else ""
            if not self.headers[header_index]:
                raise InvalidCellAddressError("header cells cannot be empty")
            return
        while len(self.headers) <= header_index:
            self.headers.append(_index_to_column(len(self.headers) + 1))
        row = self._ensure_row_exists(address.row)
        row.values[self.headers[header_index]] = value
        row.updated_at = datetime.utcnow()
        self.metadata["last_edited_cell"] = f"{header_name}{address.row}"

    def find_rows(self, criteria: Mapping[str, Any]) -> list[SheetRow]:
        if not isinstance(criteria, Mapping):
            raise InvalidRowError("criteria must be a mapping")
        return [row for row in self.rows if all(row.values.get(key) == value for key, value in criteria.items())]

    def sort_rows(self, key: str, direction: SortDirection = SortDirection.ASC) -> list[SheetRow]:
        if key not in self.headers:
            raise InvalidSheetError(f"unknown header: {key}")
        reverse = direction == SortDirection.DESC
        self.rows.sort(key=lambda row: row.values.get(key), reverse=reverse)
        return self.rows

    def upsert_row(self, key: str, values: Mapping[str, Any]) -> SheetRow:
        if key not in self.headers and key not in values:
            raise InvalidRowError(f"upsert key {key!r} must exist in headers or incoming values")
        if key not in self.headers:
            self.ensure_header(key)
        key_value = values.get(key)
        if key_value is None:
            raise InvalidRowError(f"upsert key {key!r} cannot be empty")
        for row in self.rows:
            if row.values.get(key) == key_value:
                for header, value in values.items():
                    self.ensure_header(header)
                    row.values[header] = value
                row.updated_at = datetime.utcnow()
                return row
        return self.append_row(values)


@dataclass
class Spreadsheet:
    title: str
    sheets: dict[str, Sheet] = field(default_factory=dict)
    active_sheet: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if not isinstance(self.title, str) or not self.title.strip():
            raise InvalidSpreadsheetError("title cannot be empty")
        if any(not isinstance(sheet, Sheet) for sheet in self.sheets.values()):
            raise InvalidSpreadsheetError("sheets must contain Sheet instances")
        if self.active_sheet is not None and self.active_sheet not in self.sheets:
            raise InvalidSpreadsheetError("active_sheet must reference an existing sheet")

    def add_sheet(self, sheet: Sheet) -> Sheet:
        if not isinstance(sheet, Sheet):
            raise InvalidSheetError("sheet must be a Sheet instance")
        if sheet.name.value in self.sheets:
            raise DuplicateSheetError(f"duplicate sheet name: {sheet.name.value}")
        self.sheets[sheet.name.value] = sheet
        self.active_sheet = self.active_sheet or sheet.name.value
        return sheet

    def get_sheet(self, name: SheetName | str) -> Sheet:
        key = name.value if isinstance(name, SheetName) else SheetName(name).value
        try:
            return self.sheets[key]
        except KeyError as exc:
            raise InvalidSheetError(f"sheet not found: {key}") from exc

    def remove_sheet(self, name: SheetName | str) -> Sheet:
        key = name.value if isinstance(name, SheetName) else SheetName(name).value
        try:
            sheet = self.sheets.pop(key)
        except KeyError as exc:
            raise InvalidSheetError(f"sheet not found: {key}") from exc
        if self.active_sheet == key:
            self.active_sheet = next(iter(self.sheets), None)
        return sheet
