import datetime
import json
from typing import Any, Sequence

from common.dao.chat_message import ChatMessageDao
from common.database.db import DBConfig, Db
from common.logging import get_logger, span
from common.models.common import ChatMessage
from common.platform.twitch.model import (
    Badge,
    MessageContent,
    MessageFragment,
    TwitchChatMessage,
)

logger = get_logger(__name__)


class TwitchChatMessageDAO(ChatMessageDao):
    """Data Access Object for Twitch chat messages in PostgreSQL."""

    def __init__(self, db: Db | None = None):
        """Initialize with an existing DB connection or create a new one."""
        self.db = db if db is not None else Db(DBConfig())

    @staticmethod
    def _normalize_rows(rows: object) -> list[tuple[Any, ...]]:
        if not isinstance(rows, list):
            return []
        normalized: list[tuple[Any, ...]] = []
        for row in rows:
            if isinstance(row, tuple):
                normalized.append(row)
        return normalized

    @staticmethod
    def _row_to_payload(row: tuple[Any, ...]) -> dict[str, Any]:
        columns = [
            "id",
            "message_id",
            "broadcaster_user_id",
            "broadcaster_user_login",
            "broadcaster_user_name",
            "chatter_user_id",
            "chatter_user_login",
            "chatter_user_name",
            "message_text",
            "message_type",
            "color",
            "created_at",
            "cheer",
            "reply",
            "channel_points_custom_reward_id",
            "source_broadcaster_user_id",
            "source_broadcaster_user_login",
            "source_broadcaster_user_name",
            "source_message_id",
        ]
        payload = dict(zip(columns, row))
        if "text" not in payload and "message_text" in payload:
            payload["text"] = payload["message_text"]
        return payload

    @staticmethod
    def _decode_jsonb(value: Any) -> Any:
        if isinstance(value, str):
            try:
                return json.loads(value)
            except json.JSONDecodeError:
                return value
        return value

    async def _hydrate_row(self, row: tuple[Any, ...]) -> dict[str, Any]:
        payload = self._row_to_payload(row)
        message_id = payload.get("message_id")
        if not message_id:
            payload["fragments"] = []
            payload["badges"] = []
            payload["source_badges"] = None
            return payload

        message_key = str(message_id)

        fragment_rows = self._normalize_rows(
            await self.db.fetch_all(
                """
                SELECT type, text, cheermote, emote, mention
                FROM message_fragments
                WHERE message_id = %s
                ORDER BY fragment_order
                """,
                (message_key,),
            )
        )
        fragments = [
            MessageFragment(
                type=str(fragment_row[0]),
                text=str(fragment_row[1]),
                cheermote=self._decode_jsonb(fragment_row[2]),
                emote=self._decode_jsonb(fragment_row[3]),
                mention=self._decode_jsonb(fragment_row[4]),
            )
            for fragment_row in fragment_rows
        ]

        badge_rows = self._normalize_rows(
            await self.db.fetch_all(
                """
                SELECT set_id, badge_id, info
                FROM message_badges
                WHERE message_id = %s
                ORDER BY badge_order
                """,
                (message_key,),
            )
        )
        badges = [
            Badge(
                set_id=str(badge_row[0]), id=str(badge_row[1]), info=str(badge_row[2])
            )
            for badge_row in badge_rows
        ]

        source_badge_rows = self._normalize_rows(
            await self.db.fetch_all(
                """
                SELECT set_id, badge_id, info
                FROM source_badges
                WHERE message_id = %s
                ORDER BY badge_order
                """,
                (message_key,),
            )
        )
        source_badges = [
            Badge(
                set_id=str(source_badge_row[0]),
                id=str(source_badge_row[1]),
                info=str(source_badge_row[2]),
            )
            for source_badge_row in source_badge_rows
        ]

        payload["fragments"] = fragments
        payload["badges"] = badges
        payload["source_badges"] = source_badges or None
        return payload

    async def _build_messages_from_rows_async(
        self, rows: list[tuple[Any, ...]]
    ) -> list[ChatMessage]:
        messages: list[ChatMessage] = []
        logger.debug(
            "Building %d Twitch chat messages from database rows",
            len(rows),
            extra={"row_count": len(rows)},
        )
        for row in rows:
            payload = await self._hydrate_row(row)
            messages.append(self._build_message_from_data(payload))
        return messages

    async def get_messages_by_time_range(
        self, broadcaster_user_id: str, start_timestamp: float, end_timestamp: float
    ) -> Sequence[ChatMessage]:
        """Get all chat messages for a broadcaster within a specific time range."""
        with span(
            logger,
            "get_twitch_messages_by_time_range",
            {
                "broadcaster_id": broadcaster_user_id,
                "start_timestamp": start_timestamp,
                "end_timestamp": end_timestamp,
            },
        ):
            try:
                message_rows = self._normalize_rows(
                    await self.db.fetch_all(
                        """
                        SELECT *
                        FROM twitch_chat_messages
                        WHERE broadcaster_user_id = %s
                          AND created_at BETWEEN TIMESTAMP 'epoch' + (%s::double precision * INTERVAL '1 second')
                            AND TIMESTAMP 'epoch' + (%s::double precision * INTERVAL '1 second')
                        ORDER BY created_at
                        """,
                        (broadcaster_user_id, start_timestamp, end_timestamp),
                    )
                )

                messages = await self._build_messages_from_rows_async(message_rows)
                logger.info(
                    "Retrieved %d Twitch 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 Twitch 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

    def _to_jsonb(self, payload: Any) -> Any:
        if payload is None:
            return None
        return json.dumps(payload)

    async def save_message(self, message: ChatMessage) -> str | None:
        """Save a complete chat message with its related entities."""
        if not isinstance(message, TwitchChatMessage):
            raise TypeError("TwitchChatMessageDAO expects TwitchChatMessage instances")

        with span(
            logger,
            "save_twitch_message",
            {
                "message_id": message.message_id,
                "broadcaster_id": message.broadcaster_user_id,
                "chatter_id": message.chatter_user_id,
            },
        ):
            try:
                # 1. Insert main message; we continue using the UUID for related inserts.
                await self.db.execute_commit(
                    """
                    INSERT INTO twitch_chat_messages (message_id, broadcaster_user_id,
                                                      broadcaster_user_login,
                                                      broadcaster_user_name,
                                                      chatter_user_id, chatter_user_login,
                                                      chatter_user_name, message_text,
                                                      message_type, color, created_at,
                                                      cheer, reply,
                                                      channel_points_custom_reward_id,
                                                      source_broadcaster_user_id,
                                                      source_broadcaster_user_login,
                                                      source_broadcaster_user_name,
                                                      source_message_id)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
                            %s, %s)
                    """,
                    (
                        message.message_id,
                        message.broadcaster_user_id,
                        message.broadcaster_user_login,
                        message.broadcaster_user_name,
                        message.chatter_user_id,
                        message.chatter_user_login,
                        message.chatter_user_name,
                        message.message.text,
                        message.message_type,
                        self._to_jsonb(message.color),
                        datetime.datetime.fromtimestamp(message.created_at),
                        self._to_jsonb(message.cheer),
                        self._to_jsonb(message.reply),
                        message.channel_points_custom_reward_id,
                        message.source_broadcaster_user_id,
                        message.source_broadcaster_user_login,
                        message.source_broadcaster_user_name,
                        message.source_message_id,
                    ),
                )

                # 2. Insert message fragments using the UUID message_id (matches table definition)
                fragment_count = 0
                if message.message.fragments:
                    for i, fragment in enumerate(message.message.fragments):
                        await self.db.execute_commit(
                            """
                            INSERT INTO message_fragments (message_id, type, text,
                                                           cheermote, emote,
                                                           mention, fragment_order)
                            VALUES (%s, %s, %s, %s, %s, %s, %s)
                            """,
                            (
                                message.message_id,
                                fragment.type,
                                fragment.text,
                                self._to_jsonb(fragment.cheermote),
                                self._to_jsonb(fragment.emote),
                                self._to_jsonb(fragment.mention),
                                i,
                            ),
                        )
                        fragment_count += 1

                # 3. Insert badges using the UUID message_id
                badge_count = 0
                if message.badges:
                    for i, badge in enumerate(message.badges):
                        await self.db.execute_commit(
                            """
                            INSERT INTO message_badges (message_id, set_id, badge_id, info,
                                                        badge_order)
                            VALUES (%s, %s, %s, %s, %s)
                            """,
                            (message.message_id, badge.set_id, badge.id, badge.info, i),
                        )
                        badge_count += 1

                # 4. Insert source badges if present using the UUID message_id
                source_badge_count = 0
                if message.source_badges:
                    for i, badge in enumerate(message.source_badges):
                        await self.db.execute_commit(
                            """
                            INSERT INTO source_badges (message_id, set_id, badge_id, info, badge_order)
                            VALUES (%s, %s, %s, %s, %s)
                            """,
                            (message.message_id, badge.set_id, badge.id, badge.info, i),
                        )
                        source_badge_count += 1

                logger.info(
                    "Twitch message %s persisted with %d fragments, %d badges, and %d source badges",
                    message.message_id,
                    fragment_count,
                    badge_count,
                    source_badge_count,
                    extra={
                        "message_id": message.message_id,
                        "fragment_count": fragment_count,
                        "badge_count": badge_count,
                        "source_badge_count": source_badge_count,
                    },
                )
                return message.message_id
            except Exception as exc:
                logger.error(
                    "Could not save Twitch chat message: %s",
                    exc,
                    extra={"message_id": message.message_id, "error": str(exc)},
                )
                raise

    async def get_message_by_id(self, message_id: int | str) -> ChatMessage | None:
        """Retrieve a complete chat message by its ID."""
        message_key = str(message_id)
        with span(logger, "get_twitch_message_by_id", {"message_id": message_key}):
            try:
                message_row = await self.db.fetch_one(
                    """
                    SELECT *
                    FROM twitch_chat_messages
                    WHERE message_id = %s
                    """,
                    (message_key,),
                )

                if not isinstance(message_row, tuple):
                    return None

                payload = await self._hydrate_row(message_row)
                return self._build_message_from_data(payload)
            except Exception as exc:
                logger.error(
                    "Error retrieving Twitch chat message with ID %s: %s",
                    message_key,
                    exc,
                    extra={"message_id": message_key, "error": str(exc)},
                )
                raise

    async def get_messages_by_broadcaster(
        self, broadcaster_id: int | str, limit: int | None = None
    ) -> Sequence[ChatMessage]:
        """Get messages by broadcaster ID with optional limit."""
        row_limit = limit or 100
        message_rows = self._normalize_rows(
            await self.db.fetch_all(
                """
                SELECT *
                FROM twitch_chat_messages
                WHERE broadcaster_user_id = %s
                ORDER BY created_at DESC
                LIMIT %s
                """,
                (str(broadcaster_id), row_limit),
            )
        )
        return await self._build_messages_from_rows_async(message_rows)

    async def get_messages_by_chatter(
        self, chatter_id: int | str, limit: int | None = None
    ) -> Sequence[ChatMessage]:
        """Get messages by chatter ID with optional limit."""
        row_limit = limit or 100
        message_rows = self._normalize_rows(
            await self.db.fetch_all(
                """
                SELECT *
                FROM twitch_chat_messages
                WHERE chatter_user_id = %s
                ORDER BY created_at DESC
                LIMIT %s
                """,
                (str(chatter_id), row_limit),
            )
        )
        return await self._build_messages_from_rows_async(message_rows)

    async def get_messages_by_stream(
        self, stream_id: int | str, limit: int | None = None
    ) -> Sequence[ChatMessage]:
        """Get messages by stream ID with optional limit."""
        row_limit = limit or 100
        message_rows = self._normalize_rows(
            await self.db.fetch_all(
                """
                SELECT *
                FROM twitch_chat_messages
                WHERE source_message_id = %s
                ORDER BY created_at DESC
                LIMIT %s
                """,
                (str(stream_id), row_limit),
            )
        )
        return await self._build_messages_from_rows_async(message_rows)

    async def count_messages_by_stream(self, stream_id: int | str) -> int:
        """Count messages by stream ID."""
        try:
            stream_pk = int(stream_id)
        except (TypeError, ValueError):
            return 0

        stream = await self.db.fetch_one(
            """
            SELECT s.stream_id, s.stream_start_time, s.stream_end_time, ta.twitch_id
            FROM streams s
            LEFT JOIN twitch_account ta ON s.user_id = ta.user_id
            WHERE s.id = %s
            """,
            (stream_pk,),
        )
        if not isinstance(stream, tuple):
            return 0

        broadcaster_user_id = stream[3]
        if not broadcaster_user_id:
            return 0

        start_time = stream[1]
        end_time = stream[2]
        if isinstance(start_time, datetime.datetime):
            start_time_value = start_time.timestamp()
        elif isinstance(start_time, (int, float)):
            start_time_value = float(start_time)
        else:
            return 0

        if isinstance(end_time, datetime.datetime):
            end_time_value = end_time.timestamp()
        elif isinstance(end_time, (int, float)):
            end_time_value = float(end_time)
        else:
            return 0

        chats = await self.get_messages_by_time_range(
            str(broadcaster_user_id), start_time_value, end_time_value
        )
        return len(chats)

    async def delete_message(self, message_id: int | str) -> bool | None:
        """Delete a message and all its related data."""
        message_key = str(message_id)
        with span(logger, "delete_twitch_message", {"message_id": message_key}):
            try:
                await self.db.execute_commit(
                    """
                    DELETE
                    FROM twitch_chat_messages
                    WHERE message_id = %s
                    """,
                    (message_key,),
                )
                logger.info(
                    "Successfully deleted Twitch chat message with ID %s",
                    message_key,
                    extra={"message_id": message_key},
                )
                return True
            except Exception as exc:
                logger.error(
                    "Error deleting Twitch chat message with ID %s: %s",
                    message_key,
                    exc,
                    extra={"message_id": message_key, "error": str(exc)},
                )
                raise

    def _build_messages_from_rows(
        self, rows: list[tuple[Any, ...]]
    ) -> list[ChatMessage]:
        """Build message objects from rows without loading related entities."""
        messages: list[ChatMessage] = []
        for row in rows:
            messages.append(self._build_message_from_data(row))
        return messages

    def _build_message_from_data(
        self, data: dict[str, Any] | tuple[Any, ...]
    ) -> ChatMessage:
        payload = self._row_to_payload(data) if isinstance(data, tuple) else dict(data)

        fragments_raw = payload.get("fragments")
        fragments: list[MessageFragment] = []
        if isinstance(fragments_raw, list):
            for fragment in fragments_raw:
                if isinstance(fragment, MessageFragment):
                    fragments.append(fragment)
                elif isinstance(fragment, dict):
                    fragments.append(
                        MessageFragment(
                            type=str(fragment.get("type", "text")),
                            text=str(fragment.get("text", "")),
                            cheermote=self._decode_jsonb(fragment.get("cheermote")),
                            emote=self._decode_jsonb(fragment.get("emote")),
                            mention=self._decode_jsonb(fragment.get("mention")),
                        )
                    )

        badges_raw = payload.get("badges")
        badges: list[Badge] = []
        if isinstance(badges_raw, list):
            for badge in badges_raw:
                if isinstance(badge, Badge):
                    badges.append(badge)
                elif isinstance(badge, dict):
                    badges.append(
                        Badge(
                            set_id=str(badge.get("set_id", "")),
                            id=str(badge.get("id", "")),
                            info=str(badge.get("info", "")),
                        )
                    )

        source_badges_raw = payload.get("source_badges")
        source_badges: list[Badge] | None = None
        if isinstance(source_badges_raw, list):
            source_badges = []
            for source_badge in source_badges_raw:
                if isinstance(source_badge, Badge):
                    source_badges.append(source_badge)
                elif isinstance(source_badge, dict):
                    source_badges.append(
                        Badge(
                            set_id=str(source_badge.get("set_id", "")),
                            id=str(source_badge.get("id", "")),
                            info=str(source_badge.get("info", "")),
                        )
                    )
            if not source_badges:
                source_badges = None

        created_at_raw = payload.get("created_at")
        created_at = 0.0
        if isinstance(created_at_raw, datetime.datetime):
            created_at = created_at_raw.timestamp()
        elif isinstance(created_at_raw, (int, float)):
            created_at = float(created_at_raw)

        color_value = self._decode_jsonb(payload.get("color"))
        message_text = payload.get("text", payload.get("message_text", ""))

        message_content = MessageContent(text=str(message_text), fragments=fragments)
        return TwitchChatMessage(
            broadcaster_user_id=str(payload.get("broadcaster_user_id", "")),
            broadcaster_user_login=str(payload.get("broadcaster_user_login", "")),
            broadcaster_user_name=str(payload.get("broadcaster_user_name", "")),
            chatter_user_id=str(payload.get("chatter_user_id", "")),
            chatter_user_login=str(payload.get("chatter_user_login", "")),
            chatter_user_name=str(payload.get("chatter_user_name", "")),
            message_id=str(payload.get("message_id", "")),
            message=message_content,
            message_type=str(payload.get("message_type", "text")),
            color=color_value if isinstance(color_value, str) else None,
            created_at=created_at,
            cheer=self._decode_jsonb(payload.get("cheer")),
            reply=self._decode_jsonb(payload.get("reply")),
            badges=badges,
            channel_points_custom_reward_id=payload.get(
                "channel_points_custom_reward_id"
            ),
            source_broadcaster_user_id=payload.get("source_broadcaster_user_id"),
            source_broadcaster_user_login=payload.get("source_broadcaster_user_login"),
            source_broadcaster_user_name=payload.get("source_broadcaster_user_name"),
            source_message_id=payload.get("source_message_id"),
            source_badges=source_badges,
        )
