"""
Kick-specific implementation of `ChatMessageDao`.

The class persists / queries chat messages stored in the two tables that
were introduced in the Kick migration scripts:

    • kick_chat_messages
    • kick_message_emotes
"""

from __future__ import annotations

import datetime as _dt
import json
from typing import Any, List, Sequence

from common.database import Db
from common.logging import get_logger, span
from common.models.common import ChatMessage
from common.platform.kick.model import KickChatMessage
from .chat_message import ChatMessageDao  # adjust import-path if needed

logger = get_logger(__name__)


class KickChatMessageDao(ChatMessageDao):
    """
    DAO that fulfils the storage-agnostic `ChatMessageDao` interface while
    talking to a PostgreSQL backend through the shared `Db` helper.
    """

    # ------------------------------------------------------------------ #
    # construction                                                       #
    # ------------------------------------------------------------------ #

    def __init__(self, db: Db) -> None:
        self._db = db

    # ------------------------------------------------------------------ #
    # CRUD-like operations                                               #
    # ------------------------------------------------------------------ #

    async def save_message(self, message: ChatMessage) -> str | None:  # noqa: D401
        """
        Persist a **Kick** chat message together with its emotes.

        The call is a no-op when the message already exists (unique
        constraint on `message_id`).
        """
        span_attrs = {
            "message_id": getattr(message, "message_id", None),
            "broadcaster_username": None,
            "sender_username": None,
        }
        if isinstance(message, KickChatMessage):
            span_attrs["broadcaster_username"] = message.broadcaster.username
            span_attrs["sender_username"] = message.sender.username

        with span(
            logger,
            "save_kick_message",
            span_attrs,
        ):
            if not isinstance(message, KickChatMessage):
                logger.error(
                    "Type error: Expected KickChatMessage instance",
                    extra={"received_type": type(message).__name__},
                )
                raise TypeError(
                    "KickChatMessageDao expects `KickChatMessage` instances"
                )

            logger.debug(
                "Saving Kick chat message %s",
                message.message_id,
                extra={"message_id": message.message_id},
            )

            insert_msg_sql = """
                             INSERT INTO kick_chat_messages (message_id,
                                                             broadcaster_user_id,
                                                             broadcaster_username,
                                                             broadcaster_channel_slug,
                                                             sender_user_id,
                                                             sender_username,
                                                             sender_channel_slug,
                                                             content,
                                                             created_at)
                             VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (message_id) DO NOTHING
                RETURNING id; \
                             """

            msg_vals = (
                message.message_id,
                str(message.broadcaster.user_id)
                if message.broadcaster.user_id
                else None,
                message.broadcaster.username,
                message.broadcaster.channel_slug,
                str(message.sender.user_id) if message.sender.user_id else None,
                message.sender.username,
                message.sender.channel_slug,
                message.content,
                message.created_at,
            )

            try:
                async with await self._db.get_connection() as conn:
                    async with conn.cursor() as cur:
                        # parent row ------------------------------------------------
                        logger.debug(
                            "Executing insert for Kick message %s",
                            message.message_id,
                            extra={"message_id": message.message_id},
                        )
                        await cur.execute(insert_msg_sql.encode(), msg_vals)
                        inserted = await cur.fetchone()
                        if not inserted:
                            await conn.commit()
                            logger.debug(
                                "Kick message %s already exists, skipping",
                                message.message_id,
                                extra={"message_id": message.message_id},
                            )
                            return message.message_id

                        # emotes (batch) -------------------------------------------
                        emote_count = 0
                        if message.emotes:
                            logger.debug(
                                "Saving %d emotes for Kick message %s",
                                len(message.emotes),
                                message.message_id,
                                extra={
                                    "message_id": message.message_id,
                                    "emote_count": len(message.emotes),
                                },
                            )
                            em_vals: List[tuple[Any, ...]] = []
                            for order_idx, em in enumerate(message.emotes):
                                for pos in em.positions:
                                    em_vals.append(
                                        (
                                            message.message_id,
                                            em.emote_id,
                                            int(pos.get("s")),
                                            int(pos.get("e")),
                                            order_idx,
                                        )
                                    )
                                    emote_count += 1
                            insert_em_sql = """
                                            INSERT INTO kick_message_emotes (message_id,
                                                                             emote_id,
                                                                             position_start,
                                                                             position_end,
                                                                             emote_order)
                                            VALUES (%s, %s, %s, %s, %s) ON CONFLICT DO NOTHING; \
                                            """
                            await cur.executemany(insert_em_sql.encode(), em_vals)

                    await conn.commit()
                    logger.info(
                        "Kick message %s persisted with %d emotes",
                        message.message_id,
                        emote_count,
                        extra={
                            "message_id": message.message_id,
                            "emote_count": emote_count,
                            "broadcaster_username": message.broadcaster.username,
                            "sender_username": message.sender.username,
                        },
                    )
                    return message.message_id
            except Exception as exc:  # noqa: BLE001
                logger.error(
                    "Could not save Kick chat message: %s",
                    exc,
                    extra={"message_id": message.message_id, "error": str(exc)},
                )
                raise

    async def delete_message(self, message_id: int | str) -> bool | None:
        """
        Remove a stored message by its **table primary key** (`id`).

        Cascade-delete of emotes is handled by the FK in the migration.
        """
        with span(logger, "delete_kick_message", {"message_id": message_id}):
            try:
                try:
                    message_pk = int(message_id)
                except (TypeError, ValueError):
                    logger.error(
                        "Kick message_id must be an integer primary key",
                        extra={"message_id": message_id},
                    )
                    return False

                logger.debug(
                    "Deleting Kick chat message with ID %s",
                    message_pk,
                    extra={"message_id": message_pk},
                )

                await self._db.execute_commit(
                    "DELETE FROM kick_chat_messages WHERE id = %s;", (message_pk,)
                )

                logger.info(
                    "Successfully deleted Kick chat message with ID %s",
                    message_pk,
                    extra={"message_id": message_pk},
                )
                return True
            except Exception as exc:
                logger.error(
                    "Could not delete Kick chat message: %s",
                    exc,
                    extra={"message_id": message_id, "error": str(exc)},
                )
                raise

    async def get_message_by_id(self, message_id: int | str) -> ChatMessage | None:
        """
        Fetch a single message (inc. emotes) by the primary key (`id`).
        """
        with span(logger, "get_kick_message_by_id", {"message_id": message_id}):
            try:
                try:
                    message_pk = int(message_id)
                except (TypeError, ValueError):
                    logger.error(
                        "Kick message_id must be an integer primary key",
                        extra={"message_id": message_id},
                    )
                    return None

                logger.debug(
                    "Fetching Kick chat message with ID %s",
                    message_pk,
                    extra={"message_id": message_pk},
                )

                row = await self._db.fetch_one(
                    """
                    SELECT id,
                           message_id,
                           broadcaster_user_id,
                           broadcaster_username,
                           broadcaster_channel_slug,
                           sender_user_id,
                           sender_username,
                           sender_channel_slug,
                           content,
                           created_at
                    FROM kick_chat_messages
                    WHERE id = %s;
                    """,
                    (message_pk,),
                )
                if not row:
                    logger.debug(
                        "No Kick chat message found with ID %s",
                        message_pk,
                        extra={"message_id": message_pk},
                    )
                    return None

                base_cols = [
                    "id",
                    "message_id",
                    "broadcaster_user_id",
                    "broadcaster_username",
                    "broadcaster_channel_slug",
                    "sender_user_id",
                    "sender_username",
                    "sender_channel_slug",
                    "content",
                    "created_at",
                ]
                data = dict(zip(base_cols, row))

                logger.debug(
                    "Fetching emotes for Kick message ID %s (message_id: %s)",
                    message_pk,
                    data["message_id"],
                    extra={"id": message_pk, "message_id": data["message_id"]},
                )

                em_rows = await self._db.fetch_all(
                    """
                    SELECT message_id,
                           emote_id,
                           position_start,
                           position_end,
                           emote_order
                    FROM kick_message_emotes
                    WHERE message_id = %s
                    ORDER BY emote_order;
                    """,
                    (data["message_id"],),
                )
                if em_rows:
                    em_cols = [
                        "message_id",
                        "emote_id",
                        "position_start",
                        "position_end",
                        "emote_order",
                    ]
                    data["emotes"] = [dict(zip(em_cols, r)) for r in em_rows]
                    logger.debug(
                        "Found %d emotes for Kick message ID %s",
                        len(em_rows),
                        data["message_id"],
                        extra={
                            "message_id": data["message_id"],
                            "emote_count": len(em_rows),
                        },
                    )
                else:
                    logger.debug(
                        "No emotes found for Kick message ID %s",
                        data["message_id"],
                        extra={"message_id": data["message_id"]},
                    )

                message = self._build_message_from_data(data)
                logger.debug(
                    "Successfully retrieved Kick chat message with ID %s",
                    message_pk,
                    extra={"message_id": message_pk},
                )
                return message
            except Exception as exc:
                logger.error(
                    "Error retrieving Kick chat message with ID %s: %s",
                    message_id,
                    exc,
                    extra={"message_id": message_id, "error": str(exc)},
                )
                raise

    # ------------------------------------------------------------------ #
    # Query helpers                                                      #
    # ------------------------------------------------------------------ #

    async def get_messages_by_time_range(
        self,
        broadcaster_user_id: str,
        start_timestamp: float,
        end_timestamp: float,
    ) -> Sequence[ChatMessage]:
        with span(
            logger,
            "get_kick_messages_by_time_range",
            {
                "broadcaster_id": broadcaster_user_id,
                "start_timestamp": start_timestamp,
                "end_timestamp": end_timestamp,
            },
        ):
            try:
                start_dt = _dt.datetime.fromtimestamp(start_timestamp, _dt.timezone.utc)
                end_dt = _dt.datetime.fromtimestamp(end_timestamp, _dt.timezone.utc)

                logger.debug(
                    "Fetching Kick chat messages for broadcaster %s between %s and %s",
                    broadcaster_user_id,
                    start_dt,
                    end_dt,
                    extra={
                        "broadcaster_id": broadcaster_user_id,
                        "start_timestamp": start_timestamp,
                        "end_timestamp": end_timestamp,
                    },
                )

                rows = await self._db.fetch_all(
                    """
                    SELECT *
                    FROM kick_chat_messages
                    WHERE broadcaster_user_id = %s
                      AND created_at BETWEEN %s AND %s
                    ORDER BY created_at;
                    """,
                    (broadcaster_user_id, start_dt, end_dt),
                )

                messages = self._build_messages_from_rows(self._normalize_rows(rows))
                logger.info(
                    "Retrieved %d Kick chat messages for broadcaster %s in time range",
                    len(messages),
                    broadcaster_user_id,
                    extra={
                        "broadcaster_id": broadcaster_user_id,
                        "start_timestamp": start_timestamp,
                        "end_timestamp": end_timestamp,
                        "message_count": len(messages),
                    },
                )
                return messages
            except Exception as exc:
                logger.error(
                    "Error retrieving Kick chat messages for broadcaster %s: %s",
                    broadcaster_user_id,
                    exc,
                    extra={
                        "broadcaster_id": broadcaster_user_id,
                        "start_timestamp": start_timestamp,
                        "end_timestamp": end_timestamp,
                        "error": str(exc),
                    },
                )
                raise

    async def get_messages_by_broadcaster(
        self,
        broadcaster_id: int | str,
        limit: int | None = None,
    ) -> Sequence[ChatMessage]:
        sql = """
              SELECT *
              FROM kick_chat_messages
              WHERE broadcaster_user_id = %s
              ORDER BY created_at DESC \
              """
        if limit:
            sql += " LIMIT %s"
            params: tuple[Any, ...] = (str(broadcaster_id), limit)
        else:
            params = (str(broadcaster_id),)

        rows = await self._db.fetch_all(sql + ";", params)
        return self._build_messages_from_rows(self._normalize_rows(rows))

    async def get_messages_by_chatter(
        self,
        chatter_id: int | str,
        limit: int | None = None,
    ) -> Sequence[ChatMessage]:
        sql = """
              SELECT *
              FROM kick_chat_messages
              WHERE sender_user_id = %s
              ORDER BY created_at DESC \
              """
        if limit:
            sql += " LIMIT %s"
            params: tuple[Any, ...] = (str(chatter_id), limit)
        else:
            params = (str(chatter_id),)

        rows = await self._db.fetch_all(sql + ";", params)
        return self._build_messages_from_rows(self._normalize_rows(rows))

    async def get_messages_by_stream(
        self,
        stream_id: int | str,
        limit: int | None = None,
    ) -> Sequence[ChatMessage]:
        """
        Return messages that belong to the given stream.  The logic assumes
        the `streams` table holds `broadcaster_user_id`, `start_time`,
        `end_time`, and `platform`.
        """
        base_sql = """
                   SELECT km.*
                   FROM kick_chat_messages km
                            JOIN streams s
                                 ON km.broadcaster_user_id = s.broadcaster_user_id
                                     AND s.platform = 'kick'
                                     AND km.created_at BETWEEN s.start_time AND s.end_time
                   WHERE s.id = %s
                   ORDER BY km.created_at \
                   """
        try:
            stream_pk = int(stream_id)
        except (TypeError, ValueError):
            logger.error(
                "stream_id must be an integer primary key",
                extra={"stream_id": stream_id},
            )
            return []

        if limit:
            base_sql += " LIMIT %s"
            params: tuple[Any, ...] = (stream_pk, limit)
        else:
            params = (stream_pk,)

        rows = await self._db.fetch_all(base_sql + ";", params)
        return self._build_messages_from_rows(self._normalize_rows(rows))

    async def count_messages_by_stream(self, stream_id: int | str) -> int:
        try:
            stream_pk = int(stream_id)
        except (TypeError, ValueError):
            logger.error(
                "stream_id must be an integer primary key",
                extra={"stream_id": stream_id},
            )
            return 0

        count_row = await self._db.fetch_one(
            """
            SELECT COUNT(1)
            FROM kick_chat_messages km
                     JOIN streams s
                          ON km.broadcaster_user_id = s.broadcaster_user_id
                              AND s.platform = 'kick'
                              AND km.created_at BETWEEN s.start_time AND s.end_time
            WHERE s.id = %s;
            """,
            (stream_pk,),
        )
        return int(count_row[0]) if count_row else 0

    # ------------------------------------------------------------------ #
    # internal helpers                                                   #
    # ------------------------------------------------------------------ #

    @staticmethod
    def _normalize_rows(rows: object) -> list[tuple[Any, ...]]:
        """Normalize DB results to a concrete list of tuples."""
        if not isinstance(rows, list):
            return []
        normalized: list[tuple[Any, ...]] = []
        for row in rows:
            if isinstance(row, tuple):
                normalized.append(row)
        return normalized

    def _build_messages_from_rows(
        self,
        rows: List[tuple[Any, ...]],
    ) -> List[ChatMessage]:
        """
        Build message objects for a list of raw rows.
        An additional query per message is issued to fetch its emotes.
        """
        logger.debug(
            "Building %d Kick chat messages from database rows",
            len(rows),
            extra={"row_count": len(rows)},
        )
        messages = []
        for row in rows:
            message = self._build_message_from_data(row)
            if message:
                messages.append(message)
        logger.debug(
            "Successfully built %d Kick chat messages",
            len(messages),
            extra={"message_count": len(messages)},
        )
        return messages

    def _build_message_from_data(
        self, data: dict[str, Any] | tuple[Any, ...]
    ) -> ChatMessage:
        """
        Convert raw mapping into a domain object.  The Kick Pydantic model
        can validate / coerce the types on construction.
        """
        if isinstance(data, tuple):
            columns = [
                "id",
                "message_id",
                "broadcaster_user_id",
                "broadcaster_username",
                "broadcaster_channel_slug",
                "sender_user_id",
                "sender_username",
                "sender_channel_slug",
                "content",
                "created_at",
            ]
            payload: dict[str, Any] = dict(zip(columns, data))
        else:
            payload = data
        payload.pop("id", None)
        return KickChatMessage(**payload)

    # For PostgreSQL we can simply JSON-serialise the payload; psycopg’s
    # binary protocol will keep it as JSONB.
    def _to_jsonb(self, payload: Any) -> Any:  # noqa: D401
        return json.dumps(payload)
