"""Telegram crawl metric repository — warehouse-only persistence and analytics."""

from __future__ import annotations

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

from repo_core import BaseRepository

logger = logging.getLogger(__name__)

from .model import TelegramCrawlMetric


class TelegramCrawlMetricRepository(BaseRepository[TelegramCrawlMetric]):
    """Repository for persisting and querying Telegram crawl metrics in Doris.

    Warehouse-only — no database or search backends.  Metrics rows are
    keyed by ``(peer_id, message_id)`` — the natural Telegram business key.

    All queries use the ``QueryBuilder`` API — no raw SQL in this file.

    Usage::

        repo = TelegramCrawlMetricRepository(model=TelegramCrawlMetric)
        async with repo:
            await repo.warehouse_insert_one(TABLE, metric.to_warehouse())

            # Analytics
            rows = await repo.dw_messages_per_channel(days=7)
            peak = await repo.dw_peak_hour(days=7)
    """

    TABLE = "telegram_crawl_metrics"
    _MESSAGES_TABLE = "telegram_messages"
    _backends = {"doris"}

    # -- Shared JOIN clause ------------------------------------------------
    # Joins telegram_messages to telegram_crawl_metrics on the natural
    # business key (peer_id, message_id).  Full table names — no aliases.
    _JOIN_ON_PEER_MSG = (
        "telegram_messages.message_id = telegram_crawl_metrics.message_id AND "
        "telegram_messages.peer_id = telegram_crawl_metrics.peer_id"
    )

    # -- Shared SELECT / GROUP BY expressions (Doris) ----------------------
    # Doris 4.x with ONLY_FULL_GROUP_BY does NOT resolve SELECT aliases in
    # GROUP BY when complex expressions / JOINs are involved.  We define the
    # raw expressions once and reuse them in both SELECT and GROUP BY to
    # keep the two in sync.

    _CHANNEL_ID_EXPR = "telegram_messages.peer_id"
    _CHANNEL_TITLE_EXPR = (
        "COALESCE(NULLIF(telegram_channels.title, ''), "
        "NULLIF(telegram_channels.username, ''))"
    )
    _CHANNEL_USERNAME_EXPR = "telegram_channels.username"
    # Friendly display label that never falls back to a bare numeric ID.
    # Falls back to NULL when no telegram_channels row exists — the
    # caller / frontend can use ``channel_id`` as a fallback identifier.
    _CHANNEL_LABEL_EXPR = (
        "COALESCE(NULLIF(telegram_channels.title, ''), "
        "NULLIF(telegram_channels.username, ''), "
        "CONCAT('Channel ', CAST(telegram_messages.peer_id AS VARCHAR)))"
    )
    _DAY_EXPR = "DATE_TRUNC(telegram_messages.message_sent_at, 'DAY')"
    _HOUR_EXPR = "HOUR(telegram_messages.message_sent_at)"

    # -- Convenience wrappers ----------------------------------------------

    async def dw_create_metric(self, instance: TelegramCrawlMetric) -> TelegramCrawlMetric:
        """Insert a metrics row into the warehouse."""
        return await self.warehouse_insert_one(
            self.TABLE,
            instance.to_warehouse(),
            returning=["*"],
        )

    async def dw_find_by_key(
        self, peer_id: int, message_id: int
    ) -> TelegramCrawlMetric | None:
        """Look up the metrics row by natural business key."""
        return await self.warehouse_first(
            self.TABLE,
            {"peer_id": peer_id, "message_id": message_id},
        )

    async def insert_or_update_metric(
        self, peer_id: int, message_id: int, new_payload: dict[str, Any]
    ) -> None:
        """Insert or partial-update a metric row by ``(peer_id, message_id)``.

        If a row exists for the key, only the fields present in
        *new_payload* are updated — other columns are left unchanged.
        If no row exists, a new row is created **only if** the required
        fields ``message_sent_at`` and ``ingested_at`` are present in
        the payload; otherwise the call is silently skipped to avoid
        creating rows with bogus defaults (e.g. epoch timestamps).
        """
        existing = await self.dw_find_by_key(peer_id, message_id)
        if existing:
            update_values = {
                k: v for k, v in new_payload.items()
                if k not in ("peer_id", "message_id") and v is not None
            }
            if update_values:
                await self.warehouse_update(
                    self.TABLE,
                    {"peer_id": peer_id, "message_id": message_id},
                    update_values,
                )
        else:
            data = dict(new_payload)
            data.setdefault("peer_id", peer_id)
            data.setdefault("message_id", message_id)
            # Only create a new row when the full metrics payload is
            # available (message_sent_at + ingested_at).  Partial updates
            # like dw_record_persistence should only touch existing rows.
            if "message_sent_at" not in data or "ingested_at" not in data:
                logger.debug(
                    "Skipping metric row creation for peer_id=%s message_id=%s: "
                    "missing message_sent_at or ingested_at",
                    peer_id, message_id,
                )
                return
            data.setdefault("status", "unknown")
            await self.warehouse_insert_one(
                self.TABLE,
                data,
                returning=["*"],
            )

    async def dw_record_persistence(
        self, peer_id: int, message_id: int, timestamp: datetime | None = None
    ) -> None:
        """Record when the message was persisted to the warehouse."""
        ts = timestamp or datetime.now(timezone.utc)
        await self.insert_or_update_metric(
            peer_id,
            message_id,
            {
                "persisted_at": ts,
                "status": "persisted",
            },
        )

    # -- Date-filter helper ------------------------------------------------

    def _apply_date_filter(
        self, qb, *,
        field: str,
        days: int | None = None,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
    ):
        """Apply a date-range or relative-days filter to a query builder.

        When *start_date* and *end_date* are both provided they take
        precedence over *days*.  The lower bound is inclusive and the
        upper bound is exclusive (``>= start, < end``).
        """
        if start_date is not None and end_date is not None:
            return qb.where_raw(
                field, ">= :date_start", {"date_start": start_date}
            ).where_raw(
                field, "< :date_end", {"date_end": end_date}
            )
        return qb.where_raw(
            field,
            ">= DATE_SUB(NOW(), INTERVAL :days DAY)",
            {"days": days or 7},
        )

    # -- Analytics queries -------------------------------------------------

    async def dw_messages_per_channel(
        self, *,
        days: int = 7,
        limit: int = 20,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
    ) -> list[dict[str, Any]]:
        """Messages per channel over the last N days — WITH channel titles.

        Uses a two-step query (same pattern as ``dw_peak_hour``):
        1. Find the top *limit* channel IDs by total message volume.
        2. Return **all days** for those channels (not just top rows).

        This ensures each channel series has enough data points for the
        frontend irregular-timeseries (datetime x-axis) chart to draw
        connected lines instead of isolated dots.

        LEFT JOINs ``telegram_channels`` (Doris) to resolve numeric
        ``peer_id`` → human-readable ``title`` and ``username``.

        Returns rows like:
        ``{"channel_id": 123, "title": "Crypto News", "username": "cryptonews",
           "label": "Crypto News", "day": "2026-06-20", "msg_count": 42}``.
        """
        msg_date_field = "telegram_messages.message_sent_at"

        # 1. Find the top *limit* channel IDs by total message count ─────
        top_qb = (
            self.warehouse_table(self.TABLE)
            .join(self._MESSAGES_TABLE, self._JOIN_ON_PEER_MSG)
            .select(
                f"{self._CHANNEL_ID_EXPR} AS channel_id",
                "COUNT(*) AS total_msgs",
            )
            .group_by(self._CHANNEL_ID_EXPR)
            .order_by("-total_msgs")
            .limit(limit)
        )
        top_qb = self._apply_date_filter(
            top_qb, field=msg_date_field,
            days=days, start_date=start_date, end_date=end_date,
        )
        top_rows = await top_qb.all()
        top_ids = [r["channel_id"] for r in top_rows]

        if not top_ids:
            return []

        # 2. Get all (channel, day) rows for those channels ───────────
        detail_qb = (
            self.warehouse_table(self.TABLE)
            .join(self._MESSAGES_TABLE, self._JOIN_ON_PEER_MSG)
            .join(
                "telegram_channels",
                "telegram_messages.peer_id = telegram_channels.channel_id",
                join_type="LEFT",
            )
            .select(
                f"{self._CHANNEL_ID_EXPR} AS channel_id",
                f"{self._CHANNEL_TITLE_EXPR} AS title",
                f"{self._CHANNEL_USERNAME_EXPR} AS username",
                f"{self._CHANNEL_LABEL_EXPR} AS label",
                f"{self._DAY_EXPR} AS day",
                "COUNT(*) AS msg_count",
            )
            .where_in(self._CHANNEL_ID_EXPR, top_ids)
        )
        detail_qb = self._apply_date_filter(
            detail_qb, field=msg_date_field,
            days=days, start_date=start_date, end_date=end_date,
        )
        return await (
            detail_qb
            .group_by(
                self._CHANNEL_ID_EXPR,
                self._CHANNEL_TITLE_EXPR,
                self._CHANNEL_USERNAME_EXPR,
                self._CHANNEL_LABEL_EXPR,
                self._DAY_EXPR,
            )
            .order_by(self._CHANNEL_ID_EXPR, self._DAY_EXPR)
            .all()
        )

    async def dw_peak_hour(
        self, *,
        days: int = 7,
        limit: int = 20,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
    ) -> list[dict[str, Any]]:
        """Peak hour of messages per channel over the last N days (or custom date range) — WITH channel titles.

        Returns **all 24 hours** for the top *limit* channels (by total message
        volume), not just the top-N (channel, hour) pairs.  This ensures the
        frontend heatmap / stacked-bar chart sees the full hourly distribution.

        Rows: ``{"channel_id": 123, "title": "...", "username": "...",
                 "hour_of_day": 14, "msg_count": 500}``.
        """
        msg_date_field = "message_sent_at"

        # 1. Find the top *limit* channel IDs by total message count ─────
        top_qb = (
            self.warehouse_table("telegram_messages")
            .select(
                f"{self._CHANNEL_ID_EXPR} AS channel_id",
                "COUNT(*) AS total_msgs",
            )
            .group_by(self._CHANNEL_ID_EXPR)
            .order_by("-total_msgs")
            .limit(limit)
        )
        top_qb = self._apply_date_filter(
            top_qb, field=msg_date_field,
            days=days, start_date=start_date, end_date=end_date,
        )
        top_rows = await top_qb.all()
        top_ids = [r["channel_id"] for r in top_rows]

        if not top_ids:
            return []

        # 2. Get all (channel, hour) pairs for those channels ───────────
        detail_qb = (
            self.warehouse_table("telegram_messages")
            .join(
                "telegram_channels",
                "telegram_messages.peer_id = telegram_channels.channel_id",
                join_type="LEFT",
            )
            .select(
                f"{self._CHANNEL_ID_EXPR} AS channel_id",
                f"{self._CHANNEL_TITLE_EXPR} AS title",
                f"{self._CHANNEL_USERNAME_EXPR} AS username",
                f"{self._CHANNEL_LABEL_EXPR} AS label",
                f"{self._HOUR_EXPR} AS hour_of_day",
                "COUNT(*) AS msg_count",
            )
            .where_in(self._CHANNEL_ID_EXPR, top_ids)
        )
        detail_qb = self._apply_date_filter(
            detail_qb, field=msg_date_field,
            days=days, start_date=start_date, end_date=end_date,
        )
        return await (
            detail_qb
            .group_by(
                self._CHANNEL_ID_EXPR,
                self._CHANNEL_TITLE_EXPR,
                self._CHANNEL_USERNAME_EXPR,
                self._CHANNEL_LABEL_EXPR,
                self._HOUR_EXPR,
            )
            .order_by(self._CHANNEL_ID_EXPR, self._HOUR_EXPR)
            .all()
        )

    async def dw_crawl_delay(
        self, *,
        days: int = 1,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
    ) -> list[dict[str, Any]]:
        """Delay between message post time and crawler receipt time — WITH channel titles.

        Returns rows like:
        ``{"channel_id": 123, "title": "Crypto News", "username": "cryptonews",
           "avg_delay_seconds": 2.3, "p95_delay_seconds": 5.1}``.
        """
        qb = (
            self.warehouse_table(self.TABLE)
            .join(self._MESSAGES_TABLE, self._JOIN_ON_PEER_MSG)
            .join(
                "telegram_channels",
                "telegram_messages.peer_id = telegram_channels.channel_id",
                join_type="LEFT",
            )
            .select(
                f"{self._CHANNEL_ID_EXPR} AS channel_id",
                f"{self._CHANNEL_TITLE_EXPR} AS title",
                f"{self._CHANNEL_USERNAME_EXPR} AS username",
                f"{self._CHANNEL_LABEL_EXPR} AS label",
                "AVG(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.ingested_at)) AS avg_delay_seconds",
                "PERCENTILE_APPROX(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.ingested_at), 0.50, 200000) AS p50_delay_seconds",
                "PERCENTILE_APPROX(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.ingested_at), 0.95, 200000) AS p95_delay_seconds",
                "PERCENTILE_APPROX(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.ingested_at), 0.99, 200000) AS p99_delay_seconds",
                "MAX(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.ingested_at)) AS max_delay_seconds",
            )
        )
        qb = self._apply_date_filter(
            qb, field="telegram_messages.message_sent_at",
            days=days, start_date=start_date, end_date=end_date,
        )
        return await (
            qb
            .group_by(
                self._CHANNEL_ID_EXPR,
                self._CHANNEL_TITLE_EXPR,
                self._CHANNEL_USERNAME_EXPR,
                self._CHANNEL_LABEL_EXPR,
            )
            .order_by("-avg_delay_seconds")
            .all()
        )

    async def dw_end_to_end_latency(
        self, *, days: int = 1
    ) -> list[dict[str, Any]]:
        """Full pipeline latency: channel post → DB persistence — WITH channel titles.

        Returns rows like:
        ``{"channel_id": 123, "title": "Crypto News", "username": "cryptonews",
           "avg_e2e_seconds": 3.1, "p95_e2e_seconds": 8.2}``.
        """
        return await (
            self.warehouse_table(self.TABLE)
            .join(self._MESSAGES_TABLE, self._JOIN_ON_PEER_MSG)
            .join(
                "telegram_channels",
                "telegram_messages.peer_id = telegram_channels.channel_id",
                join_type="LEFT",
            )
            .select(
                f"{self._CHANNEL_ID_EXPR} AS channel_id",
                f"{self._CHANNEL_TITLE_EXPR} AS title",
                f"{self._CHANNEL_USERNAME_EXPR} AS username",
                f"{self._CHANNEL_LABEL_EXPR} AS label",
                "AVG(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.persisted_at)) AS avg_e2e_seconds",
                # Higher compression (200K) for accurate tail percentiles on
                # skewed latency distributions.  Default 10K produces 20-50%+
                # error at p99 when data is sparse in the tail.
                "PERCENTILE_APPROX(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.persisted_at), 0.50, 200000) AS p50_e2e_seconds",
                "PERCENTILE_APPROX(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.persisted_at), 0.95, 200000) AS p95_e2e_seconds",
                "PERCENTILE_APPROX(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.persisted_at), 0.99, 200000) AS p99_e2e_seconds",
                # Include max for outlier detection — if max >> p99 the tail is
                # dominated by a handful of extreme values (retries, rate-limits).
                "MAX(TIMESTAMPDIFF(SECOND, telegram_messages.message_sent_at, telegram_crawl_metrics.persisted_at)) AS max_e2e_seconds",
            )
            .where_not_null("telegram_crawl_metrics.persisted_at")
            .where_raw(
                "telegram_messages.message_sent_at",
                ">= DATE_SUB(NOW(), INTERVAL :days DAY)",
                {"days": days},
            )
            .group_by(
                self._CHANNEL_ID_EXPR,
                self._CHANNEL_TITLE_EXPR,
                self._CHANNEL_USERNAME_EXPR,
                self._CHANNEL_LABEL_EXPR,
            )
            .order_by("-avg_e2e_seconds")
            .all()
        )

    async def dw_messages_per_account(
        self, *,
        days: int = 7,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
    ) -> list[dict[str, Any]]:
        """Messages per account over the last N days (or custom date range).

        Returns rows like ``{"account_id": "...", "msg_count": 999}``.
        """
        qb = (
            self.warehouse_table(self.TABLE)
            .select("account_id", "COUNT(*) AS msg_count")
        )
        qb = self._apply_date_filter(
            qb, field="ingested_at",
            days=days, start_date=start_date, end_date=end_date,
        )
        return await (
            qb
            .group_by("account_id")
            .order_by("-msg_count")
            .all()
        )
    
    async def dw_status_distribution(
        self, *, days: int = 1
    ) -> list[dict[str, Any]]:
        """Count of messages by pipeline status over the last N days.

        Returns rows like:
        ``{"status": "ingested", "count": 12345}``,
        ``{"status": "parsed", "count": 12000}``, …
        """
        return await (
            self.warehouse_table(self.TABLE)
            .select("status", "COUNT(*) AS count")
            .where_raw(
                "ingested_at",
                ">= DATE_SUB(NOW(), INTERVAL :days DAY)",
                {"days": days},
            )
            .group_by("status")
            .order_by("-count")
            .all()
        )

    async def dw_daily_throughput(
        self, *, days: int = 7
    ) -> list[dict[str, Any]]:
        """Messages per day over the last N days (time-series).

        Returns rows like:
        ``{"day": "2026-06-17", "msg_count": 50000}``.
        """
        _day_expr = "DATE_FORMAT(ingested_at, '%Y-%m-%d')"
        return await (
            self.warehouse_table(self.TABLE)
            .select(
                f"{_day_expr} AS day",
                "COUNT(*) AS msg_count",
            )
            .where_raw(
                "ingested_at",
                ">= DATE_SUB(NOW(), INTERVAL :days DAY)",
                {"days": days},
            )
            .group_by(_day_expr)
            .order_by("day")
            .all()
        )

