import json
from typing import Optional, List, Set

from fastapi import HTTPException
from common.database import DBConfig, Db
from common.logging import get_logger, span
from common.config.config import get_pro_model_settings

from common.models.common import Platform
from common.models.stream_signal import (
    StreamSignalConfig,
    StreamSignalUpsert,
    StreamSignalPublishPlatform,  # NEW
)

from common.models.stream_signal_history import (
    StreamSignalHistoryCreate,
    StreamSignalHistoryPlatformCreate,
    StreamSignalHistoryItem,
)

from common.models.stream_signal import TikTokSettings


logger = get_logger(__name__)

GET_USER_TIER_QUERY = """
    SELECT tier
    FROM users
    WHERE id = %s
    LIMIT 1
"""


SELECT_SETTINGS_ID_BY_USER = """
    SELECT id, tiktok_settings_id
    FROM stream_signal_settings
    WHERE user_id = %s
    LIMIT 1
"""

SELECT_CONFIG_BY_USER = """
    SELECT
        s.user_id,
        s.is_active,
        s.webcam,
        s.layout,
        s.tagline,

        s.camera_position_top,
        s.camera_position_left,
        s.camera_position_width,
        s.camera_position_height,

        s.add_live_badge,

        COALESCE((
            SELECT array_agg(lp.platform ORDER BY lp.platform)
            FROM stream_signal_live_platforms lp
            WHERE lp.stream_signal_id = s.id
        ), ARRAY[]::text[]) AS live_platforms,

        COALESCE((
            SELECT jsonb_agg(
                jsonb_build_object(
                    'platform', pp.platform,
                    'is_reel', pp.is_reel,
                    'is_story', pp.is_story
                )
                ORDER BY pp.platform
            )
            FROM stream_signal_publish_platforms pp
            WHERE pp.stream_signal_id = s.id
        ), '[]'::jsonb) AS publish_platforms,

        CASE
            WHEN ts.id IS NULL THEN NULL
            ELSE jsonb_build_object(
                'id', ts.id,
                'comment', ts.comment,
                'duet', ts.duet,
                'stitch', ts.stitch,
                'promotional_content', ts.promotional_content,
                'promoting_yourself', ts.promoting_yourself,
                'paid_partnership', ts.paid_partnership,
                'privacy_settings', ts.privacy_settings
            )
        END AS tiktok_settings

    FROM stream_signal_settings s
    LEFT JOIN tiktok_settings ts ON ts.id = s.tiktok_settings_id
    WHERE s.user_id = %s
    LIMIT 1
"""


DELETE_LIVE_PLATFORMS_BY_SIGNAL_ID = """
    DELETE FROM stream_signal_live_platforms
    WHERE stream_signal_id = %s
"""

DELETE_PUBLISH_PLATFORMS_BY_SIGNAL_ID = """
    DELETE FROM stream_signal_publish_platforms
    WHERE stream_signal_id = %s
"""

INSERT_STREAM_SIGNAL_HISTORY_QUERY = """
    INSERT INTO stream_signal_history (user_id, clip_id, description)
    VALUES (%s, %s, %s)
    RETURNING id, user_id, clip_id, description, created_at, deleted_at
"""

SOFT_DELETE_STREAM_SIGNAL_HISTORY_QUERY = """
    UPDATE stream_signal_history
    SET deleted_at = NOW()
    WHERE id = %s
      AND deleted_at IS NULL
"""

INSERT_STREAM_SIGNAL_HISTORY_PLATFORM_QUERY = """
    INSERT INTO stream_signal_history_platform (stream_signal_history_id, platform)
    VALUES (%s, %s)
    RETURNING id, stream_signal_history_id, platform
"""

FETCH_STREAM_SIGNAL_HISTORY_BY_USER_QUERY = """
    SELECT
        c.kind AS clip_type,
        c.thumbnail_url,
        h.description,
        h.created_at,
        COALESCE(array_agg(hp.platform ORDER BY hp.platform) FILTER (WHERE hp.platform IS NOT NULL), ARRAY[]::text[]) AS platforms
    FROM stream_signal_history h
    JOIN clips c ON c.id = h.clip_id
    LEFT JOIN stream_signal_history_platform hp ON hp.stream_signal_history_id = h.id
    WHERE h.user_id = %s
      AND h.deleted_at IS NULL
    GROUP BY c.kind, c.thumbnail_url, h.description, h.created_at, h.id
    ORDER BY h.created_at DESC, h.id DESC
"""


class StreamSignalDAO:
    """DAO for Stream Signal settings + platforms."""

    def __init__(self, db: Optional[Db] = None):
        self.db = db if db else Db(DBConfig(pg_dsn=None))
        logger.debug(
            "StreamSignalDAO initialized", extra={"component": "StreamSignalDAO"}
        )

    def _coerce_json(self, raw, default):
        if raw is None:
            return default
        if isinstance(raw, (dict, list)):
            return raw
        if isinstance(raw, str):
            try:
                return json.loads(raw)
            except Exception:
                return default
        return default

    async def _insert_tiktok_settings(self, s: TikTokSettings) -> int:
        inserted_id = await self.db.insert(
            "tiktok_settings",
            [
                "comment",
                "duet",
                "stitch",
                "promotional_content",
                "promoting_yourself",
                "paid_partnership",
                "privacy_settings",
            ],
            (
                s.comment,
                s.duet,
                s.stitch,
                s.promotional_content,
                s.promoting_yourself,
                s.paid_partnership,
                s.privacy_settings,
            ),
        )
        if not inserted_id:
            raise HTTPException(
                status_code=500, detail="Failed to create tiktok_settings"
            )
        return int(inserted_id)

    async def _update_tiktok_settings(
        self, tiktok_settings_id: int, s: TikTokSettings
    ) -> None:
        await self.db.update(
            table="tiktok_settings",
            set_columns=[
                "comment",
                "duet",
                "stitch",
                "promotional_content",
                "promoting_yourself",
                "paid_partnership",
                "privacy_settings",
            ],
            set_values=[
                s.comment,
                s.duet,
                s.stitch,
                s.promotional_content,
                s.promoting_yourself,
                s.paid_partnership,
                s.privacy_settings,
            ],
            where_clause="id = %s",
            where_params=(tiktok_settings_id,),
        )

    async def _resolve_tiktok_settings_id_for_create(
        self, tiktok: Optional[TikTokSettings]
    ) -> Optional[int]:
        # If not provided => FK null.
        if tiktok is None:
            return None

        # Create only supports id=None (recommended).
        if tiktok.id is None:
            return await self._insert_tiktok_settings(tiktok)

        # If id is provided, we can update it then link it (optional behavior).
        # Safer approach would be to reject this for create.
        await self._update_tiktok_settings(int(tiktok.id), tiktok)
        return int(tiktok.id)

    async def _resolve_tiktok_settings_id_for_update(
        self,
        current_tiktok_settings_id: Optional[int],
        tiktok: Optional[TikTokSettings],
    ) -> Optional[int]:
        # If not provided => FK null (as you requested).
        if tiktok is None:
            return None

        # First time setup => create new row and link.
        if tiktok.id is None:
            return await self._insert_tiktok_settings(tiktok)

        # If id is provided => update existing settings row.
        # Security/consistency: only allow updating the row currently linked to this config.
        if current_tiktok_settings_id != int(tiktok.id):
            raise HTTPException(
                status_code=422,
                detail="tiktok_settings.id must match the currently linked settings id",
            )

        await self._update_tiktok_settings(int(tiktok.id), tiktok)
        return int(tiktok.id)

    async def _require_pro_tier(self, user_id: int) -> None:
        row = await self.db.fetch_one(GET_USER_TIER_QUERY, (user_id,))
        if not row:
            raise HTTPException(status_code=404, detail="User not found")
        tier = row[0]
        if tier != get_pro_model_settings().pro_tier:
            raise HTTPException(
                status_code=403, detail="Stream Signal is available for Pro tier only"
            )

    # CHANGED: publish_platforms type + how we read values
    def _validate_platform_sets(
        self,
        live_platforms: List[Platform],
        publish_platforms: List[StreamSignalPublishPlatform],
    ) -> None:
        # Keep validation aligned with DB CHECK constraints.
        allowed_live: Set[str] = {"twitch", "kick", "youtube"}
        allowed_publish: Set[str] = {
            "youtube",
            "facebook",
            "instagram",
            "discord",
            "reddit",
            "x",
            "tiktok",
        }

        live_values = {p.value for p in (live_platforms or [])}
        publish_values = {p.platform.value for p in (publish_platforms or [])}

        invalid_live = live_values - allowed_live
        if invalid_live:
            raise HTTPException(
                status_code=422,
                detail=f"Invalid live platforms: {sorted(invalid_live)}",
            )

        invalid_publish = publish_values - allowed_publish
        if invalid_publish:
            raise HTTPException(
                status_code=422,
                detail=f"Invalid publish platforms: {sorted(invalid_publish)}",
            )

        if not publish_values:
            raise HTTPException(
                status_code=422, detail="publish_platforms cannot be empty"
            )

    def _row_to_model(self, row) -> StreamSignalConfig:
        live = [Platform(x) for x in (row[10] or [])]

        raw_publish = row[11] or []
        publish = [StreamSignalPublishPlatform(**obj) for obj in raw_publish]

        raw_tiktok = self._coerce_json(row[12], None)
        tiktok_settings = TikTokSettings(**raw_tiktok) if raw_tiktok else None

        return StreamSignalConfig(
            user_id=int(row[0]),
            is_active=bool(row[1]),
            webcam=str(row[2]),
            layout=str(row[3]),
            tagline=row[4],
            camera_position_top=row[5],
            camera_position_left=row[6],
            camera_position_width=row[7],
            camera_position_height=row[8],
            add_live_badge=bool(row[9]),
            live_platforms=live,
            publish_platforms=publish,
            tiktok_settings=tiktok_settings,
        )

    async def get_by_user_id(self, user_id: int) -> Optional[StreamSignalConfig]:
        """Return Stream Signal config for user or None if it doesn't exist."""
        with span(logger, "stream_signal_get_by_user_id", {"user_id": user_id}):
            row = await self.db.fetch_one(SELECT_CONFIG_BY_USER, (user_id,))
            if not row:
                return None
            return self._row_to_model(row)

    async def create(
        self, user_id: int, data: StreamSignalUpsert
    ) -> StreamSignalConfig:
        """
        Insert into:
          - stream_signal_settings
          - stream_signal_live_platforms (optional)
          - stream_signal_publish_platforms (required)
        """
        with span(logger, "stream_signal_create", {"user_id": user_id}):
            await self._require_pro_tier(user_id)
            self._validate_platform_sets(data.live_platforms, data.publish_platforms)

            existing_id = await self.db.fetch_one(
                SELECT_SETTINGS_ID_BY_USER, (user_id,)
            )
            if existing_id:
                raise HTTPException(
                    status_code=409,
                    detail="Stream Signal settings already exist for this user",
                )

            tiktok_settings_id = await self._resolve_tiktok_settings_id_for_create(
                data.tiktok_settings
            )

            settings_id = await self.db.insert(
                "stream_signal_settings",
                [
                    "user_id",
                    "is_active",
                    "webcam",
                    "layout",
                    "tagline",
                    "camera_position_top",
                    "camera_position_left",
                    "camera_position_width",
                    "camera_position_height",
                    "add_live_badge",
                    "tiktok_settings_id",
                ],
                (
                    user_id,
                    data.is_active,
                    data.webcam,
                    data.layout,
                    data.tagline,
                    data.camera_position_top,
                    data.camera_position_left,
                    data.camera_position_width,
                    data.camera_position_height,
                    data.add_live_badge,
                    tiktok_settings_id,
                ),
            )

            if not settings_id:
                raise HTTPException(
                    status_code=500, detail="Failed to create Stream Signal settings"
                )

            # Insert live platforms (can be empty)
            for p in data.live_platforms or []:
                await self.db.insert(
                    "stream_signal_live_platforms",
                    ["stream_signal_id", "platform"],
                    (settings_id, p.value),
                )

            # CHANGED: insert with flags
            for p in data.publish_platforms:
                await self.db.insert(
                    "stream_signal_publish_platforms",
                    ["stream_signal_id", "platform", "is_reel", "is_story"],
                    (settings_id, p.platform.value, p.is_reel, p.is_story),
                )

            # Return full config
            created = await self.get_by_user_id(user_id)
            if not created:
                raise HTTPException(
                    status_code=500,
                    detail="Failed to load Stream Signal settings after create",
                )
            return created

    async def update(
        self, user_id: int, data: StreamSignalUpsert
    ) -> StreamSignalConfig:
        """
        Update settings row + replace platforms:
          - delete existing platform rows by stream_signal_id
          - insert new ones
        """
        with span(logger, "stream_signal_update", {"user_id": user_id}):
            await self._require_pro_tier(user_id)
            self._validate_platform_sets(data.live_platforms, data.publish_platforms)

            row = await self.db.fetch_one(SELECT_SETTINGS_ID_BY_USER, (user_id,))
            if not row:
                raise HTTPException(
                    status_code=404, detail="Stream Signal settings not found"
                )

            stream_signal_id = int(row[0])

            tiktok_settings_id = await self._resolve_tiktok_settings_id_for_update(
                row[1], data.tiktok_settings
            )

            # Update settings
            await self.db.update(
                table="stream_signal_settings",
                set_columns=[
                    "is_active",
                    "webcam",
                    "layout",
                    "tagline",
                    "camera_position_top",
                    "camera_position_left",
                    "camera_position_width",
                    "camera_position_height",
                    "add_live_badge",
                    "tiktok_settings_id",
                ],
                set_values=[
                    data.is_active,
                    data.webcam,
                    data.layout,
                    data.tagline,
                    data.camera_position_top,
                    data.camera_position_left,
                    data.camera_position_width,
                    data.camera_position_height,
                    data.add_live_badge,
                    tiktok_settings_id,
                ],
                where_clause="id = %s",
                where_params=(stream_signal_id,),
            )

            await self.db.execute_commit(
                DELETE_LIVE_PLATFORMS_BY_SIGNAL_ID, (stream_signal_id,)
            )
            await self.db.execute_commit(
                DELETE_PUBLISH_PLATFORMS_BY_SIGNAL_ID, (stream_signal_id,)
            )

            for p in data.live_platforms or []:
                await self.db.insert(
                    "stream_signal_live_platforms",
                    ["stream_signal_id", "platform"],
                    (stream_signal_id, p.value),
                )

            for p in data.publish_platforms:
                await self.db.insert(
                    "stream_signal_publish_platforms",
                    ["stream_signal_id", "platform", "is_reel", "is_story"],
                    (stream_signal_id, p.platform.value, p.is_reel, p.is_story),
                )

            updated = await self.get_by_user_id(user_id)
            if not updated:
                raise HTTPException(
                    status_code=500,
                    detail="Failed to load Stream Signal settings after update",
                )
            return updated

    async def create_stream_signal_history(
        self, data: StreamSignalHistoryCreate
    ) -> int:
        """Insert a stream_signal_history row and return inserted id."""
        with span(
            logger,
            "stream_signal_history_create",
            {"user_id": data.user_id, "clip_id": data.clip_id},
        ):
            inserted_id = await self.db.insert(
                "stream_signal_history",
                ["user_id", "clip_id", "description"],
                (data.user_id, data.clip_id, data.description),
            )
            if not inserted_id:
                raise HTTPException(
                    status_code=500, detail="Failed to create stream_signal_history"
                )
            return int(inserted_id)

    async def delete_stream_signal_history(self, history_id: int) -> None:
        """Soft delete a stream_signal_history row by id (sets deleted_at)."""
        with span(logger, "stream_signal_history_delete", {"history_id": history_id}):
            await self.db.execute_commit(
                SOFT_DELETE_STREAM_SIGNAL_HISTORY_QUERY, (history_id,)
            )

    async def create_stream_signal_history_platform(
        self, data: StreamSignalHistoryPlatformCreate
    ) -> int:
        """Insert a stream_signal_history_platform row and return inserted id."""
        with span(
            logger,
            "stream_signal_history_platform_create",
            {
                "history_id": data.stream_signal_history_id,
                "platform": data.platform.value,
            },
        ):
            inserted_id = await self.db.insert(
                "stream_signal_history_platform",
                ["stream_signal_history_id", "platform"],
                (data.stream_signal_history_id, data.platform.value),
            )
            if not inserted_id:
                raise HTTPException(
                    status_code=500,
                    detail="Failed to create stream_signal_history_platform",
                )
            return int(inserted_id)

    async def get_stream_signal_history(
        self, user_id: int
    ) -> List[StreamSignalHistoryItem]:
        """
        Fetch all history rows for a user:
        returns clip kind, thumbnail_url, history description, history created_at,
        and an array of platforms.
        """
        with span(logger, "stream_signal_history_get_by_user", {"user_id": user_id}):
            rows = await self.db.fetch_all(
                FETCH_STREAM_SIGNAL_HISTORY_BY_USER_QUERY, (user_id,)
            )
            if not rows:
                return []

            out: List[StreamSignalHistoryItem] = []
            for row in rows:
                platforms = [Platform(p) for p in (row[4] or [])]
                out.append(
                    StreamSignalHistoryItem(
                        clip_type=str(row[0]),
                        thumbnail_url=row[1],
                        description=row[2],
                        created_at=row[3],
                        platforms=platforms,
                    )
                )
            return out
