"""Telegram message repository — typed CRUD with cross-backend sync."""
from __future__ import annotations

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

from repo_core import BaseRepository
from repo_core.events.hooks import hook
from repo_core.domains.telegram.message.model import TelegramMessage
from repo_core.domains.telegram.message.validator import (
    build_telegram_message_pipeline,
)

logger = logging.getLogger(__name__)


def _to_doris_datetime(value: str) -> str:
    """Normalise an ISO 8601 string to the ``YYYY-MM-DD HH:MM:SS`` format
    that Doris (MySQL protocol) expects for DATETIME column comparisons.

    Doris does not support timezone offsets or sub-second precision in
    DATETIME literals — passing ``+00:00`` or microseconds causes the
    comparison to fail silently and return 0 rows.
    """
    try:
        dt = datetime.fromisoformat(value)
        return dt.strftime("%Y-%m-%d %H:%M:%S")
    except ValueError:
        return value


class TelegramMessageRepository(BaseRepository[TelegramMessage]):
    """Repository for persisting and querying Telegram messages.

    All serialisation (``from_db`` / ``to_db`` / ``from_search`` /
    ``to_search`` / ``from_warehouse`` / ``to_warehouse``) is handled by
    :class:`BaseRepository` — this class only declares the table name,
    hooks, and typed convenience wrappers.

    Usage::

        repo = TelegramMessageRepository(
            model=TelegramMessage,
            db=create_postgres_client(),
            search=ElasticsearchClient(hosts="http://localhost:9200"),
        )
        async with repo:
            msg = TelegramMessage(peer_id=123, message_id=42, message_sent_at=..., message="Hello")
            created = await repo.db_insert_one(TABLE, msg.to_db())
            results = await repo.search_find_messages("hello")
    """

    TABLE = "telegram_messages"
    SEARCH_INDEX = "telegram_messages"
    _backends = {"db", "search", "doris"}
    _pipeline = build_telegram_message_pipeline()

    # ------------------------------------------------------------------
    # Timestamp injection  (UTC datetime, not Unix ints)
    # ------------------------------------------------------------------

    def _inject_model_timestamps(self, data: dict[str, Any]) -> dict[str, Any]:
        """Inject ``created_at``, ``updated_at``, ``crawled_at`` as UTC datetimes."""
        if self.model is None:
            return data
        now = datetime.now(timezone.utc)
        for field in ("created_at", "updated_at", "crawled_at"):
            if field in self.model.model_fields and field not in data:
                data[field] = now
        return data

    def _inject_model_updated_at(self, values: dict[str, Any]) -> dict[str, Any]:
        """Inject ``updated_at`` as a UTC datetime on update operations."""
        if self.model is None:
            return values
        if "updated_at" in self.model.model_fields and "updated_at" not in values:
            values["updated_at"] = datetime.now(timezone.utc)
        return values

    # ------------------------------------------------------------------
    # Hooks
    # ------------------------------------------------------------------

    @hook("before_create", "before_save")
    async def _validate(self, data: dict[str, Any]) -> None:
        data["message"] = await self._pipeline.validate(data.get("message", ""))

    @hook("after_create", "after_save")
    async def _sync_search(self, instance: TelegramMessage) -> None:
        if self._search is not None:
            await self.search.client.index_document(
                self.SEARCH_INDEX, str(instance.id), instance.to_search()
            )

    @hook("before_remove")
    async def _cleanup_search(self, record_id: str) -> None:
        if self._search is not None:
            await self.search.client.delete_document(self.SEARCH_INDEX, str(record_id))

    # ------------------------------------------------------------------
    # db — convenience wrappers with hooks
    # ------------------------------------------------------------------

    async def db_create_message(self, instance: TelegramMessage) -> TelegramMessage:
        data = instance.to_db()
        await self.emit("before_create", data)
        created = await self.insert_one(self.TABLE, data, returning=["*"])
        await self.emit("after_create", created)
        return created

    async def db_save_message(self, instance: TelegramMessage) -> TelegramMessage:
        data = instance.to_db()
        await self.emit("before_save", data)
        saved = await self.save(self.TABLE, data)
        await self.emit("after_save", saved)
        return saved

    async def db_remove_message(self, peer_id: int, message_id: int) -> int:
        await self.emit("before_remove", {"peer_id": peer_id, "message_id": message_id})
        return await self.remove(self.TABLE, {"peer_id": peer_id, "message_id": message_id})

    # Short aliases
    create_message = db_create_message
    save_message = db_save_message
    remove_message = db_remove_message

    # ------------------------------------------------------------------
    # search — convenience wrappers
    # ------------------------------------------------------------------

    async def search_find_messages(
        self,
        conditions: dict[str, Any] | None = None,
        *,
        limit: int | None = None,
        offset: int | None = None,
        order_by: list[str] | None = None,
    ) -> list[TelegramMessage]:
        return await self.search_find(
            self.SEARCH_INDEX, conditions, limit=limit, offset=offset, order_by=order_by
        )

    async def search_create_message(self, instance: TelegramMessage) -> None:
        await self.search.client.index_document(
            self.SEARCH_INDEX, str(instance.id), instance.to_search()
        )

    async def search_remove_message(self, msg_id: str) -> bool:
        return await self.search.client.delete_document(self.SEARCH_INDEX, str(msg_id))

    # ------------------------------------------------------------------
    # Gap detection (backfill) — warehouse (Doris)
    # ------------------------------------------------------------------

    async def detect_message_gaps(
        self,
        peer_id: int,
        *,
        min_id: int | None = None,
        max_id: int | None = None,
        min_sent_at: str | None = None,
        max_sent_at: str | None = None,
    ) -> list[dict[str, int]]:
        """Find missing message-ID ranges for a given Telegram peer (channel).

        Telegram message IDs are sequential within a peer.  Delegates to
        :meth:`db_core.DorisClient.find_sequential_gaps` which uses a
        ``LAG`` window function against the warehouse.

        Args:
            peer_id: The numeric Telegram peer (channel) ID.
            min_id: Optional lower bound for message_id (inclusive).
            max_id: Optional upper bound for message_id (inclusive).
            min_sent_at: Optional lower bound for ``message_sent_at`` —
                ISO 8601 string (e.g. ``"2026-06-22T10:00:00+00:00"``).
            max_sent_at: Optional upper bound for ``message_sent_at`` —
                ISO 8601 string.

        Returns:
            ``[{peer_id: int, gap_start: int, gap_end: int}, ...]``.
            Empty list when no gaps (or < 2 messages for the peer).

        Usage::

            from datetime import datetime, timedelta, timezone
            since = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
            gaps = await repo.detect_message_gaps(peer_id=-100123, min_sent_at=since)
        """
        # Build domain-specific time-range WHERE clauses
        extra_where: list[str] = []
        extra_params: dict[str, Any] = {}
        if min_sent_at is not None:
            extra_where.append("message_sent_at >= :min_sent_at")
            extra_params["min_sent_at"] = _to_doris_datetime(min_sent_at)
        if max_sent_at is not None:
            extra_where.append("message_sent_at <= :max_sent_at")
            extra_params["max_sent_at"] = _to_doris_datetime(max_sent_at)

        logger.debug(
            "Detecting message gaps for peer_id=%s min_id=%s max_id=%s "
            "min_sent_at=%s max_sent_at=%s",
            peer_id, min_id, max_id, min_sent_at, max_sent_at,
        )

        rows = await self.warehouse.client.find_sequential_gaps(
            table="telegram_messages",
            sequence_column="message_id",
            partition_column="peer_id",
            partition_value=peer_id,
            min_sequence=min_id,
            max_sequence=max_id,
            extra_where=extra_where or None,
            extra_params=extra_params or None,
        )

        gaps = [
            {"peer_id": int(r["peer_id"]), "gap_start": int(r["gap_start"]), "gap_end": int(r["gap_end"])}
            for r in rows
        ]

        logger.info(
            "Found %d gap(s) for peer_id=%s: %s",
            len(gaps), peer_id,
            [(g["gap_start"], g["gap_end"]) for g in gaps],
        )
        return gaps
