"""Field descriptor and query expression types for the typed ECM model API."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, overload

_MISSING: Any = object()
"""Sentinel indicating that no explicit ``default`` was provided to ``ECMField``."""

from ecmind_blue_client.ecm.builder._common import (
    DmsQueryConditionFieldOperatorsT,
    DmsQueryFieldValueT,
)

if TYPE_CHECKING:
    from ecmind_blue_client.ecm.model._ecm_model import _ECMModelBase

FieldT = TypeVar("FieldT")
RowT = TypeVar("RowT")


class ECMField(Generic[FieldT]):
    """Typed ECM field descriptor and condition builder.

    At class level returns itself so that comparison operators produce ``ECMCondition`` instances.
    At instance level returns the stored field value.

    Args:
        field_type: Python type of the field value.
        internal_name: ECM internal field name; defaults to the attribute name via ``__set_name__``.
        owner_identifier: ECM object type identifier; set by ``__set_name__`` or passed for dynamic fields.
        mandatory: Whether this field must have a non-``None`` value before the object is saved.
            Defaults to ``False``. This flag is not enforced by the framework; it serves as
            documentation and can be evaluated by application code.
        default: Optional default value for the field. Pass ``None`` for non-mandatory fields so
            that type checkers (via ``@dataclass_transform``) recognise the field as optional in
            the synthesised ``__init__``. Has no effect at runtime — field values always default
            to ``None`` via ``_raw_fields_.get()``.
    """

    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
        self,
        field_type: type[FieldT] | None = None,
        internal_name: str | None = None,
        owner_identifier: str | int | None = None,
        mandatory: bool = False,
        default: FieldT | None = _MISSING,  # pylint: disable=unused-argument
    ) -> None:
        self._field_type = field_type
        self._internal_name = internal_name
        self._owner_identifier: str | int | None = owner_identifier
        self.mandatory = mandatory

    def __set_name__(self, owner: type, name: str) -> None:
        if self._internal_name is None:
            self._internal_name = name
        if self._owner_identifier is None:
            ident = getattr(owner, "_internal_name_", None) or getattr(owner, "_type_id_", None)
            if ident is not None:
                self._owner_identifier = ident

    @property
    def internal_name(self) -> str:
        """The resolved ECM internal field name."""
        assert self._internal_name is not None, "ECMField used before __set_name__ was called"
        return self._internal_name

    # ------------------------------------------------------------------
    # Descriptor protocol
    # ------------------------------------------------------------------

    @overload
    def __get__(self, obj: None, objtype: type) -> "ECMField[FieldT]": ...

    @overload
    def __get__(self, obj: _ECMModelBase, objtype: type | None) -> FieldT | None: ...

    def __get__(self, obj: Any, objtype: type | None = None) -> "ECMField[FieldT] | FieldT | None":
        if obj is None:
            # Class-level access: return self so comparison operators work.
            return self
        return obj._raw_fields_.get(self.internal_name)  # type: ignore[union-attr]

    def __set__(self, obj: Any, value: FieldT | None) -> None:
        obj._raw_fields_[self.internal_name] = value  # type: ignore[union-attr]

    # ------------------------------------------------------------------
    # Comparison operators — return ECMCondition for use in where()
    # ------------------------------------------------------------------

    def __eq__(self, value: Any) -> "ECMCondition":  # type: ignore[override]  # pyright: ignore[reportIncompatibleMethodOverride]
        return ECMCondition(self.internal_name, "=", value, self._owner_identifier)

    def __ne__(self, value: Any) -> "ECMCondition":  # type: ignore[override]  # pyright: ignore[reportIncompatibleMethodOverride]
        return ECMCondition(self.internal_name, "!=", value, self._owner_identifier)

    def __lt__(self, value: Any) -> "ECMCondition":
        return ECMCondition(self.internal_name, "<", value, self._owner_identifier)

    def __le__(self, value: Any) -> "ECMCondition":
        return ECMCondition(self.internal_name, "<=", value, self._owner_identifier)

    def __gt__(self, value: Any) -> "ECMCondition":
        return ECMCondition(self.internal_name, ">", value, self._owner_identifier)

    def __ge__(self, value: Any) -> "ECMCondition":
        return ECMCondition(self.internal_name, ">=", value, self._owner_identifier)

    # ------------------------------------------------------------------
    # Collection operators
    # ------------------------------------------------------------------

    def in_(self, *values: DmsQueryFieldValueT) -> "ECMCondition":
        """Return an ``IN`` condition for the given values."""
        return ECMCondition(self.internal_name, "IN", list(values), self._owner_identifier)

    def not_in(self, *values: DmsQueryFieldValueT) -> "ECMCondition":
        """Return a ``NOT IN`` condition for the given values."""
        return ECMCondition(self.internal_name, "NOT IN", list(values), self._owner_identifier)

    def between(self, lower: DmsQueryFieldValueT, upper: DmsQueryFieldValueT) -> "ECMCondition":
        """Return a ``BETWEEN`` condition for the inclusive range ``[lower, upper]``."""
        return ECMCondition(self.internal_name, "BETWEEN", [lower, upper], self._owner_identifier)

    @property
    def ASC(self) -> "ECMSortOrder":
        """Return an ascending sort directive for this field.

        Example:
            ::

                ecm.select(Invoice).order_by(Invoice.Date.ASC, Invoice.Title.ASC).execute()
        """
        return ECMSortOrder(self.internal_name, "ASC")

    @property
    def DESC(self) -> "ECMSortOrder":
        """Return a descending sort directive for this field.

        Example:
            ::

                ecm.select(Invoice).order_by(Invoice.Date.DESC).execute()
        """
        return ECMSortOrder(self.internal_name, "DESC")

    def __repr__(self) -> str:
        return f"ECMField({self._internal_name!r})"


# Keep ECMFieldProxy as an alias so existing code that imports it still works.
ECMFieldProxy = ECMField


class ECMSortOrder:
    """Sort directive for a single ECM field, created via ``ECMField.ASC`` or ``ECMField.DESC``.

    Args:
        field_name: ECM internal field name to sort on.
        direction: Sort direction — ``"ASC"`` or ``"DESC"``.
    """

    def __init__(self, field_name: str, direction: Literal["ASC", "DESC"]) -> None:
        self.field_name = field_name
        self.direction: Literal["ASC", "DESC"] = direction

    def __repr__(self) -> str:
        return f"ECMSortOrder({self.field_name!r}, {self.direction!r})"


class ECMCondition:
    """A single field condition expression for use in ``select().where()``.

    Args:
        field_name: ECM internal field name.
        operator: Comparison operator (e.g. ``"="``, ``"!="``, ``"<"``).
        value: The value to compare against.
        object_type_identifier: The ECM object type identifier of the model this
            condition originates from. Used to route the condition to the correct
            ``ConditionObject`` when multiple models appear in one ``where()`` call.
            ``None`` means the condition belongs to the primary query model.
    """

    def __init__(
        self,
        field_name: str,
        operator: DmsQueryConditionFieldOperatorsT,
        value: Any,
        object_type_identifier: str | int | None = None,
    ) -> None:
        self.field_name = field_name
        self.operator = operator
        self.value = value
        self.object_type_identifier = object_type_identifier

    def __and__(self, other: ECMCondition | ECMConditionGroup) -> ECMConditionGroup:
        return ECMConditionGroup("AND", [self, other])

    def __or__(self, other: ECMCondition | ECMConditionGroup) -> ECMConditionGroup:
        return ECMConditionGroup("OR", [self, other])

    def __repr__(self) -> str:
        return f"ECMCondition({self.field_name!r} {self.operator} {self.value!r})"


class ECMConditionGroup:
    """Logical group of conditions combined with AND or OR.

    Created by combining ``ECMCondition`` instances with ``&`` / ``|``.

    Args:
        operator: ``"AND"`` or ``"OR"``.
        conditions: Conditions or nested groups to combine.
    """

    def __init__(self, operator: str, conditions: list[ECMCondition | ECMConditionGroup]) -> None:
        self.operator = operator
        self.conditions = conditions

    def __and__(self, other: ECMCondition | ECMConditionGroup) -> ECMConditionGroup:
        # Flatten consecutive AND groups.
        if self.operator == "AND":
            return ECMConditionGroup("AND", [*self.conditions, other])
        return ECMConditionGroup("AND", [self, other])

    def __or__(self, other: ECMCondition | ECMConditionGroup) -> ECMConditionGroup:
        # Flatten consecutive OR groups.
        if self.operator == "OR":
            return ECMConditionGroup("OR", [*self.conditions, other])
        return ECMConditionGroup("OR", [self, other])


class ECMTableList(list, Generic[RowT]):
    """List of ``ECMTableRowModel`` instances with table-formatted ``str()`` output.

    Returned by ``ECMTableField.__get__`` at instance level. Mutations such as
    ``append``, ``insert``, or ``del`` affect the backing list in-place and are
    therefore reflected by change-tracking on the parent model.
    """

    def __str__(self) -> str:
        if not self:
            return "**empty table**"
        first = self[0]
        cols: list[str] = list(first._raw_fields_.keys()) if hasattr(first, "_raw_fields_") else []
        if not cols:
            return list.__repr__(self)
        col_widths = [
            max(len(c), max((len(str(row._raw_fields_.get(c, ""))) for row in self if hasattr(row, "_raw_fields_")), default=0))
            for c in cols
        ]
        header = "| " + " | ".join(c.ljust(w) for c, w in zip(cols, col_widths)) + " |"
        sep = "| " + " | ".join("-" * w for w in col_widths) + " |"
        data_rows = "\n".join(
            "| "
            + " | ".join(
                str(row._raw_fields_.get(c, "")).ljust(w) if hasattr(row, "_raw_fields_") else "".ljust(w) for c, w in zip(cols, col_widths)
            )
            + " |"
            for row in self
        )
        return f"{header}\n{sep}\n{data_rows}"

    def __repr__(self) -> str:
        return self.__str__()


class ECMTableField(Generic[RowT]):
    """Typed ECM table field descriptor.

    At class level returns the descriptor itself. At instance level returns an
    ``ECMTableList[RowT]`` of typed row instances.

    Args:
        row_type: Row model class; may be ``None`` when inferred from the generic annotation.
        internal_name: ECM internal field name; defaults to the attribute name via ``__set_name__``.
        default_factory: Unused at runtime — accepted so that ``@dataclass_transform``-aware
            type checkers (Pyright) recognise table fields as optional in the synthesised
            ``__init__``. Always pass ``list`` (e.g. ``ECMTableField(default_factory=list)``).
    """

    def __init__(
        self,
        row_type: type[RowT] | None = None,
        internal_name: str | None = None,
        default_factory: Any = _MISSING,  # pylint: disable=unused-argument
    ) -> None:
        self._row_type = row_type
        self._internal_name = internal_name

    def __set_name__(self, owner: type, name: str) -> None:  # noqa: ARG002
        if self._internal_name is None:
            self._internal_name = name

    @property
    def internal_name(self) -> str:
        """The resolved ECM internal field name."""
        assert self._internal_name is not None, "ECMTableField used before __set_name__ was called"
        return self._internal_name

    @overload
    def __get__(self, obj: None, objtype: type) -> "ECMTableField[RowT]": ...

    @overload
    def __get__(self, obj: "_ECMModelBase", objtype: type | None) -> "ECMTableList[RowT]": ...

    def __get__(self, obj: Any, objtype: type | None = None) -> "ECMTableField[RowT] | ECMTableList[RowT]":  # noqa: ARG002
        if obj is None:
            return self
        return obj._raw_table_fields_.get(self.internal_name, ECMTableList())  # type: ignore[union-attr]

    def __set__(self, obj: Any, value: "list[RowT | dict[str, Any]]") -> None:
        from ecmind_blue_client.ecm.model._ecm_model import ECMTableRowModel  # pylint: disable=import-outside-toplevel

        row_type: type[ECMTableRowModel] = self._row_type or ECMTableRowModel  # type: ignore[assignment]
        rows: ECMTableList = ECMTableList()
        for item in value:
            if isinstance(item, ECMTableRowModel):
                rows.append(item)
            else:
                row_instance = row_type.__new__(row_type)
                row_instance._row_id_ = None  # pylint: disable=protected-access
                row_instance._raw_fields_ = dict(item)  # type: ignore[arg-type]  # pylint: disable=protected-access
                row_instance._original_fields_ = {}  # pylint: disable=protected-access
                rows.append(row_instance)
        obj._raw_table_fields_[self.internal_name] = rows  # type: ignore[union-attr]

    def __repr__(self) -> str:
        return f"ECMTableField({self._internal_name!r})"
