"""Repository for parsed web pages — dual-backend (PostgreSQL + Doris).

Both backends are append-only — every parse creates a new row with a
fresh UUID ``id`` and a ``version`` number.
"""

from __future__ import annotations

import logging
import uuid
from datetime import datetime, timezone
from typing import Any

from repo_core import BaseRepository

from .model import WebParsedPage

logger = logging.getLogger(__name__)

# Fields allowed in the Doris warehouse.  Must stay in sync with
# ``WebParsedPage._warehouse_include``.  Defined here (not read from the
# model class) because Pydantic v2 wraps underscore-prefixed class
# attributes and they are not accessible as plain ``set`` objects.
_WAREHOUSE_FIELDS: frozenset[str] = frozenset({
    "id", "url", "seed_id", "raw_url_id",
    "title", "description", "author", "published_time", "main_image",
    "content", "content_html",
    "keywords", "tags", "category",
    "images", "tables", "links", "schema_data",
    "statistics",
    "custom_content",
    "parsed_at", "diagnostics",
    "version",
    "created_at", "updated_at",
})


class WebParsedPageRepository(BaseRepository[WebParsedPage]):
    """Repository for persisting parsed web page content.

    Dual-backend: PostgreSQL and Doris (identical column sets).
    Upserted by ``url`` — re-parsing the same URL replaces the existing row.

    Usage::

        repo = WebParsedPageRepository(model=WebParsedPage)
        async with repo:
            page = await repo.upsert_parsed({
                "url": "https://example.com",
                "seed_id": "seed-uuid",
                "title": "Example Page",
                "content": "Lorem ipsum...",
            })
    """

    TABLE = "web_parsed_pages"
    _backends = {"db", "doris"}

    # ------------------------------------------------------------------
    # Append-only insert — separate methods per backend
    # ------------------------------------------------------------------

    async def insert_parsed_db(self, data: dict[str, Any]) -> WebParsedPage:
        """Insert a parsed page row into PostgreSQL.

        Always inserts — never updates.  Every parse creates a new row
        with a fresh UUID ``id``, matching the append-only versioning
        pattern.  Returns the ``WebParsedPage`` model instance.
        """
        url = data.get("url")
        if not url:
            raise ValueError("'url' is required for insert_parsed_db()")

        data.setdefault("id", str(uuid.uuid4()))
        data.setdefault("version", 1)
        return await self.insert_one(self.TABLE, data)

    async def upsert_parsed_db(self, data: dict[str, Any]) -> WebParsedPage:
        """Upsert a parsed page row into PostgreSQL by ``(url, version)``.

        Generates a deterministic ``id`` from the ``(url, version)``
        pair so that ``save()`` (INSERT … ON CONFLICT DO UPDATE) works
        as an idempotent upsert — re-parsing the same URL at the same
        version is a no-op.

        Returns the ``WebParsedPage`` model instance.
        """
        url = data.get("url")
        if not url:
            raise ValueError("'url' is required for upsert_parsed_db()")

        version = int(data.get("version") or 1)
        data["id"] = str(uuid.uuid5(uuid.NAMESPACE_URL, f"{url}::v{version}"))
        data["version"] = version
        return await self.save(self.TABLE, data)

    async def insert_parsed_warehouse(self, data: dict[str, Any]) -> None:
        """Insert a parsed page row into the Doris warehouse.

        Always inserts — never updates.  Every parse creates a new row
        with a fresh UUID ``id``, matching the append-only versioning
        pattern of ``web_html_raw``.  Warehouse writes are best-effort
        — failures are logged but not re-raised.
        """
        url = data.get("url")
        if not url:
            raise ValueError("'url' is required for insert_parsed_warehouse()")

        try:
            warehouse_data = self._filter_for_warehouse(data)
            warehouse_data["id"] = str(uuid.uuid4())
            warehouse_data.setdefault("version", 1)
            warehouse_data = self._datetimes_to_iso(warehouse_data)
            await self.warehouse_insert_one(self.TABLE, warehouse_data)
        except Exception:
            logger.exception("warehouse_insert_failed url=%s", url)

    async def insert_parsed(self, data: dict[str, Any]) -> WebParsedPage:
        """Insert a parsed page into PostgreSQL, then into Doris.

        Both backends are append-only — every parse creates a new row
        with a fresh UUID ``id`` and a ``version`` number.
        The warehouse write is best-effort — failure does not roll back
        the PostgreSQL write.
        """
        pg_row = await self.insert_parsed_db(data)
        await self.insert_parsed_warehouse(data)
        return pg_row

    async def find_all_by_url(self, url: str) -> list[WebParsedPage]:
        """Return all parsed-page versions for a given URL, newest first."""
        return await self.find(
            self.TABLE,
            {"url": url},
            limit=100,
            order_by=["-version"],
        )

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _strip_id(data: dict[str, Any]) -> dict[str, Any]:
        """Return a copy of *data* without the ``id`` field."""
        return {k: v for k, v in data.items() if k != "id"}

    @staticmethod
    def _datetimes_to_iso(data: dict[str, Any]) -> dict[str, Any]:
        """Convert any ``datetime`` values to ISO 8601 strings for Doris VARCHAR columns."""
        return {
            k: v.isoformat() if isinstance(v, datetime) else v
            for k, v in data.items()
        }

    @staticmethod
    def _filter_for_warehouse(data: dict[str, Any]) -> dict[str, Any]:
        """Return only fields allowed in the Doris warehouse table.

        Uses the module-level ``_WAREHOUSE_FIELDS`` constant, which must
        stay in sync with ``WebParsedPage._warehouse_include``.
        """
        return {k: v for k, v in data.items() if k in _WAREHOUSE_FIELDS}
