from __future__ import annotations

import json
from typing import Any

from pydantic import BaseModel, ConfigDict


class DomainModel(BaseModel):
    """
    Base class for all strongly-typed domain models.

    Built on Pydantic v2 with ``from_attributes=True`` so models can be
    constructed directly from ORM objects or raw database rows::

        class Article(DomainModel):
            id: int
            title: str
            body: str

        article = Article.from_db({"id": 1, "title": "Hello", "body": "..."})
        row = article.to_db()

    Per-backend serialisation
    -------------------------
    Each ``to_<backend>()`` / ``from_<backend>()`` pair defaults to
    ``model_dump()`` / ``model_validate()``.  To restrict which fields a
    backend sees, set the corresponding ``_<backend>_include`` class
    attribute (``None`` means *all* fields)::

        class MyModel(DomainModel):
            id: int
            title: str
            body: str
            score: float

            _db_include = {"id", "title", "body"}
            _search_include = {"id", "title"}
            _warehouse_include = None  # all fields
    """

    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True,
    )

    # ------------------------------------------------------------------
    # Per-backend field sets  (None == all fields)
    # ------------------------------------------------------------------

    _db_include: set[str] | None = None
    _search_include: set[str] | None = None
    _warehouse_include: set[str] | None = None

    # ------------------------------------------------------------------
    # Auto-managed timestamp fields
    # ------------------------------------------------------------------
    # Fields listed here are automatically set to the current UTC time
    # during insert / update operations by ``BaseRepository`` and
    # ``DBClient``.  Override to use different field names or set to
    # an empty ``set()`` to opt out of auto-timestamps entirely.
    #
    # Example::
    #
    #     class CustomModel(DomainModel):
    #         _timestamps = {"created_at", "modified_at"}
    _timestamps: "set[str]" = {"created_at", "updated_at"}

    # ------------------------------------------------------------------
    # Database  (primary relational store)
    # ------------------------------------------------------------------

    @classmethod
    def from_db(cls, row: dict[str, Any]) -> "DomainModel":
        """Construct a model instance from a database row dict."""
        return cls.model_validate(row)

    def to_db(self) -> dict[str, Any]:
        """Serialise to a dict suitable for database insertion."""
        return self.model_dump(include=self._db_include)

    def to_db_partial(self) -> dict[str, Any]:
        """Serialise only explicitly-set fields (useful for PATCH)."""
        return self.model_dump(exclude_unset=True, include=self._db_include)

    # ------------------------------------------------------------------
    # Search engine  (Elasticsearch / full-text index)
    # ------------------------------------------------------------------

    @classmethod
    def from_search(cls, row: dict[str, Any]) -> "DomainModel":
        """Construct a model instance from a search-engine document."""
        return cls.model_validate(row)

    def to_search(self) -> dict[str, Any]:
        """Serialise to a dict suitable for indexing in a search engine."""
        return self.model_dump(include=self._search_include)

    def to_search_partial(self) -> dict[str, Any]:
        """Serialise only explicitly-set fields for a search patial update."""
        return self.model_dump(exclude_unset=True, include=self._search_include)

    # ------------------------------------------------------------------
    # Warehouse  (OLAP / analytics store)
    # ------------------------------------------------------------------

    @classmethod
    def from_warehouse(cls, row: dict[str, Any]) -> "DomainModel":
        """Construct a model instance from a warehouse row.

        Warehouse backends (e.g. Doris) may return JSON columns as
        strings.  Parse any string value that looks like JSON (starts
        with ``{`` or ``[``) back to its Python equivalent before
        validation.
        """
        for key, value in row.items():
            if isinstance(value, str) and value.strip():
                first_char = value.strip()[0]
                if first_char in ("{", "["):
                    try:
                        row[key] = json.loads(value)
                    except (json.JSONDecodeError, TypeError):
                        pass
        return cls.model_validate(row)

    def to_warehouse(self) -> dict[str, Any]:
        """Serialise to a dict suitable for bulk-loading into a warehouse."""
        return self.model_dump(include=self._warehouse_include)

    def to_warehouse_partial(self) -> dict[str, Any]:
        """Serialise only explicitly-set fields for a warehouse patch."""
        return self.model_dump(exclude_unset=True, include=self._warehouse_include)
