from datetime import datetime
from typing import List, Tuple
from typing import Optional

from common.database import DBConfig, Db
from common.config.config import get_pro_model_settings
from common.logging import get_logger, span
from common.models.clip import (
    ClipUpdate,
    Clip,
    ClipCreate,
    ClipAnalytics,
    ClipAnalyticsCreate,
    ClipAnalyticsUpdate,
    EditedClip,
    EditedClipCreate,
    EditedClipUpdate,
    ClipLimitsStatus,
)

logger = get_logger(__name__)


SHOULD_STOP_GENERATING_CLIPS = """
    WITH ctx AS (
    SELECT u.id AS user_id, u.tier
    FROM streams s
    JOIN users   u ON u.id = s.user_id
    WHERE s.id = %s
    ),
    agg AS (
    SELECT
        ctx.user_id,
        ctx.tier,
        COUNT(*) FILTER (
        WHERE c.deleted_at IS NULL
            AND c.created_at::date = CURRENT_DATE
        ) AS clips_today,
        COUNT(*) FILTER (
        WHERE c.deleted_at IS NULL
        ) AS clips_this_month
    FROM ctx
    LEFT JOIN clips c ON c.user_id = ctx.user_id
    GROUP BY ctx.user_id, ctx.tier
    )
    SELECT
    (tier = %s AND (clips_today >= %s OR clips_this_month >= %s)) AS should_stop
    FROM agg;
"""


CLIP_LIMITS_STATUS_BY_USER_ID = """
    SELECT
        COUNT(*) FILTER (
            WHERE deleted_at IS NULL
              AND created_at >= NOW() - INTERVAL '1 day'
        ) AS clips_last_day,
        COUNT(*) FILTER (
            WHERE deleted_at IS NULL
              AND created_at >= NOW() - INTERVAL '30 days'
        ) AS clips_last_month
    FROM clips
    WHERE user_id = %s
"""


CLIP_CORE_SELECT_COLUMNS = """
    c.id,
    c.url,
    c.kind,
    c.thumbnail_url,
    c.watermark_url,
    c.caption,
    c.start_time,
    c.created_at,
    c.updated_at,
    c.deleted_at,
    c.favorited,
    c.views,
    c.stream_id,
    c.owner_username,
    c.user_id,
    c.source_type,
    c.upload_session_id,
    s.platform
"""

CLIP_ANALYTICS_SELECT_COLUMNS = """
    ca.id,
    ca.views,
    ca.heart_reactions,
    ca.smile_reactions,
    ca.laugh_reactions,
    ca.sign_reactions,
    ca.surprised_reactions,
    ca.created_at,
    ca.updated_at,
    ca.deleted_at
"""


class ClipDAO:
    """Data Access Object for clips in PostgreSQL."""

    def __init__(self, db: Optional[Db] = None):
        """Initialize with an existing DB connection or create a new one."""
        self.db = db if db else Db(DBConfig())
        logger.debug("ClipDAO initialized", extra={"component": "ClipDAO"})

    async def create_clip(self, clip: ClipCreate) -> Clip:
        """
        Create a new clip in the database.

        Args:
            clip: The clip data to create

        Returns:
            0 on success, 1 on failure
        """
        clip_attributes = {
            "kind": clip.kind,
            "stream_id": clip.stream_id,
            "owner_username": clip.owner_username,
        }
        with span(logger, "create_clip", clip_attributes):
            columns = [
                "url",
                "kind",
                "stream_id",
                "thumbnail_url",
                "watermark_url",
                "caption",
                "start_time",
                "owner_username",
                "user_id",
                "prompt_version",
                "dimension_scores",
                "combined_score",
                "game_config_name",
                "source_type",
                "upload_session_id",
            ]
            # Prepare a RETURNING clause to get the new clip id.
            # Convert dimension_scores dict to JSON string for JSONB column
            import json

            dimension_scores_json = (
                json.dumps(clip.dimension_scores) if clip.dimension_scores else None
            )

            values = (
                clip.url,
                clip.kind,
                clip.stream_id,
                clip.thumbnail_url,
                clip.watermark_url,
                clip.caption,
                clip.start_time,
                clip.owner_username,
                clip.user_id,
                clip.prompt_version,
                dimension_scores_json,
                clip.combined_score,
                clip.game_config_name,
                clip.source_type,
                str(clip.upload_session_id) if clip.upload_session_id else None,
            )

            try:
                res = await self.db.insert("clips", columns, values)
                if not res:
                    logger.error(
                        f"Failed to create clip of kind '{clip.kind}' for stream {clip.stream_id}"
                    )
                    raise Exception("Failed to create clip")

                # Create clip object with ID
                clip_obj = Clip(
                    id=res,
                    url=clip.url,
                    kind=clip.kind,
                    thumbnail_url=clip.thumbnail_url,
                    watermark_url=clip.watermark_url,
                    caption=clip.caption,
                    start_time=clip.start_time,
                    created_at=datetime.now(),
                    updated_at=datetime.now(),
                    deleted_at=None,
                    favorited=False,
                    views=0,
                    stream_id=clip.stream_id,
                    owner_username=clip.owner_username,
                    user_id=clip.user_id,
                    source_type=clip.source_type,
                    upload_session_id=clip.upload_session_id,
                    prompt_version=clip.prompt_version,
                    dimension_scores=clip.dimension_scores,
                    combined_score=clip.combined_score,
                    game_config_name=clip.game_config_name,
                )

                # Log success with complete details
                logger.info(
                    "Clip created successfully",
                    extra={
                        "clip_id": clip_obj.id,
                        "kind": clip_obj.kind,
                        "stream_id": clip_obj.stream_id,
                        "url": clip_obj.url,
                        "thumbnail_url": clip_obj.thumbnail_url,
                        "owner_username": clip_obj.owner_username,
                        "user_id": clip_obj.user_id,
                    },
                )

                return clip_obj
            except Exception as e:
                logger.error(f"Error creating clip: {e}")
                raise

    async def get_edited_clips_by_user_id(
        self, user_id: int, limit: int = 10, offset: int = 0
    ) -> List[Clip]:
        """Get edited clips by user ID with pagination."""
        with span(
            logger,
            "get_edited_clips_by_user_id",
            {"user_id": user_id, "limit": limit, "offset": offset},
        ):
            query = """
                    SELECT ec.id,
                           ec.url,
                           c.kind,
                           ec.thumbnail_url,
                           c.watermark_url,
                           ec.name as caption,
                           c.start_time,
                           ec.created_at,
                           c.updated_at,
                           c.deleted_at,
                           c.favorited,
                           c.views,
                           c.stream_id,
                           c.owner_username,
                           c.user_id,
                           c.source_type,
                           c.upload_session_id,
                           s.platform
                    FROM clips c
                    INNER JOIN edited_clips ec ON c.id = ec.original_clip_id
                    INNER JOIN streams s on s.id = c.stream_id
                    WHERE c.user_id = %s
                      AND c.deleted_at IS NULL AND ec.deleted_at IS NULL
                    ORDER BY ec.created_at DESC
                        LIMIT %s
                    OFFSET %s \
                    """
            result = await self.db.fetch_all(query, (user_id, limit, offset))
            if not result:
                logger.debug(
                    f"No edited clips found for user ID {user_id}",
                    extra={"user_id": user_id},
                )
                return []
            clips = [self._row_to_clip(row) for row in result]
            logger.debug(
                f"Found {len(clips)} edited clips for user ID {user_id}",
                extra={"user_id": user_id, "clip_count": len(clips)},
            )
            return clips

    async def get_clips_by_user_id(
        self, user_id: int, limit: int = 10, offset: int = 0
    ) -> List[Clip]:
        """Get clips by user ID with pagination."""
        with span(
            logger,
            "get_clips_by_user_id",
            {"user_id": user_id, "limit": limit, "offset": offset},
            log_entry=False,
            log_exit=False,
        ):
            query = f"""
                    SELECT {CLIP_CORE_SELECT_COLUMNS},
                           {CLIP_ANALYTICS_SELECT_COLUMNS}
                    FROM clips c
                    INNER JOIN streams s on s.id = c.stream_id
                    LEFT JOIN clip_analytics ca on ca.clip_id = c.id AND ca.deleted_at IS NULL
                    WHERE c.user_id = %s
                      AND c.deleted_at IS NULL
                    ORDER BY c.created_at DESC
                        LIMIT %s
                    OFFSET %s \
                    """
            result = await self.db.fetch_all(query, (user_id, limit, offset))
            if not result:
                logger.debug(
                    f"No clips found for user ID {user_id}", extra={"user_id": user_id}
                )
                return []
            clips = [self._row_to_clip(row) for row in result]
            logger.debug(
                f"Found {len(clips)} clips for user ID {user_id}",
                extra={"user_id": user_id, "clip_count": len(clips)},
            )
            return clips

    async def get_clip_limits_status(self, user_id: int) -> ClipLimitsStatus:
        """Return rolling clip counts (last 24h / last 30d) plus configured limits."""
        with span(
            logger,
            "get_clip_limits_status",
            {"user_id": user_id},
            log_entry=False,
            log_exit=False,
        ):
            settings = get_pro_model_settings()

            row = await self.db.fetch_one(CLIP_LIMITS_STATUS_BY_USER_ID, (user_id,))
            clips_last_day = int((row[0] if row else 0) or 0)
            clips_last_month = int((row[1] if row else 0) or 0)

            return ClipLimitsStatus(
                clips_last_day=clips_last_day,
                clips_last_month=clips_last_month,
                max_clips_per_day=int(settings.max_clips_per_day),
                max_clips_per_month=int(settings.max_clips_per_month),
            )

    async def count_user_clips_this_month(self, user_id: int) -> int:
        """Count non-deleted clips a user has created in the current calendar month."""

        with span(
            logger,
            "count_user_clips_this_month",
            {"user_id": user_id},
            log_entry=False,
            log_exit=False,
        ):
            query = """
                SELECT COUNT(*)
                FROM clips
                WHERE user_id = %s
                  AND deleted_at IS NULL
                  AND date_trunc('month', created_at AT TIME ZONE 'utc') = date_trunc('month', NOW() AT TIME ZONE 'utc')
            """

            result = await self.db.fetch_one(query, (user_id,))
            if not result:
                logger.debug(
                    "No clips found for user in current month",
                    extra={"user_id": user_id},
                )
                return 0

            count = int(result[0])
            logger.debug(
                "Counted user clips for current month",
                extra={"user_id": user_id, "clip_count": count},
            )
            return count

    async def get_clips_by_user_id_paginated(
        self, user_id: int, cursor: Optional[str] = None, limit: int = 20
    ) -> Tuple[List[Clip], Optional[str], bool]:
        """
        Get clips by user ID with cursor-based pagination.

        Args:
            user_id: User ID to fetch clips for
            cursor: Base64-encoded cursor from previous page (format: "id_timestamp")
            limit: Maximum number of clips to return (default 20)

        Returns:
            Tuple of (clips, next_cursor, has_more)
        """
        with span(
            logger,
            "get_clips_by_user_id_paginated",
            {"user_id": user_id, "has_cursor": cursor is not None, "limit": limit},
            log_entry=False,
            log_exit=False,
        ):
            # Decode cursor if present
            if cursor:
                from base64 import b64decode

                try:
                    decoded = b64decode(cursor).decode("utf-8")
                    cursor_id, cursor_timestamp = decoded.split("_")
                    cursor_id = int(cursor_id)
                    cursor_timestamp = int(cursor_timestamp)

                    query = f"""
                        SELECT {CLIP_CORE_SELECT_COLUMNS},
                               {CLIP_ANALYTICS_SELECT_COLUMNS}
                        FROM clips c
                        INNER JOIN streams s on s.id = c.stream_id
                        LEFT JOIN clip_analytics ca on ca.clip_id = c.id AND ca.deleted_at IS NULL
                        WHERE c.user_id = %s
                          AND c.deleted_at IS NULL
                          AND (EXTRACT(EPOCH FROM c.created_at), c.id) < (%s, %s)
                        ORDER BY c.created_at DESC, c.id DESC
                        LIMIT %s
                    """
                    result = await self.db.fetch_all(
                        query, (user_id, cursor_timestamp, cursor_id, limit + 1)
                    )
                except Exception as e:
                    logger.error(
                        f"Invalid cursor format: {e}",
                        extra={"user_id": user_id, "cursor": cursor},
                    )
                    # Fall back to first page
                    cursor = None

            if not cursor:
                # First page
                query = f"""
                    SELECT {CLIP_CORE_SELECT_COLUMNS},
                           {CLIP_ANALYTICS_SELECT_COLUMNS}
                    FROM clips c
                    INNER JOIN streams s on s.id = c.stream_id
                    LEFT JOIN clip_analytics ca on ca.clip_id = c.id AND ca.deleted_at IS NULL
                    WHERE c.user_id = %s
                      AND c.deleted_at IS NULL
                    ORDER BY c.created_at DESC, c.id DESC
                    LIMIT %s
                """
                result = await self.db.fetch_all(query, (user_id, limit + 1))

            if not result:
                logger.debug(
                    f"No clips found for user ID {user_id}", extra={"user_id": user_id}
                )
                return [], None, False

            # Check if there are more results
            has_more = len(result) > limit
            items = result[:limit]

            # Generate next cursor from last item
            next_cursor = None
            if has_more and items:
                from base64 import b64encode

                last_item = items[-1]
                last_id = last_item[0]
                last_created_at = last_item[7]
                cursor_str = f"{last_id}_{int(last_created_at.timestamp())}"
                next_cursor = b64encode(cursor_str.encode("utf-8")).decode("utf-8")

            clips = [self._row_to_clip(row) for row in items]
            logger.debug(
                f"Found {len(clips)} clips for user ID {user_id}",
                extra={
                    "user_id": user_id,
                    "clip_count": len(clips),
                    "has_more": has_more,
                },
            )
            return clips, next_cursor, has_more

    async def get_edited_clips_by_user_id_paginated(
        self, user_id: int, cursor: Optional[str] = None, limit: int = 20
    ) -> Tuple[List[Clip], Optional[str], bool]:
        """
        Get edited clips by user ID with cursor-based pagination.

        Args:
            user_id: User ID to fetch edited clips for
            cursor: Base64-encoded cursor from previous page (format: "id_timestamp")
            limit: Maximum number of clips to return (default 20)

        Returns:
            Tuple of (clips, next_cursor, has_more)
        """
        with span(
            logger,
            "get_edited_clips_by_user_id_paginated",
            {"user_id": user_id, "has_cursor": cursor is not None, "limit": limit},
            log_entry=False,
            log_exit=False,
        ):
            # Decode cursor if present
            if cursor:
                from base64 import b64decode

                try:
                    decoded = b64decode(cursor).decode("utf-8")
                    cursor_id, cursor_timestamp = decoded.split("_")
                    cursor_id = int(cursor_id)
                    cursor_timestamp = int(cursor_timestamp)

                    query = """
                        SELECT ec.id,
                               ec.url,
                               c.kind,
                               ec.thumbnail_url,
                               c.watermark_url,
                               ec.name as caption,
                               c.start_time,
                               ec.created_at,
                               c.updated_at,
                               c.deleted_at,
                               c.favorited,
                               c.views,
                               c.stream_id,
                               c.owner_username,
                               c.user_id,
                               c.source_type,
                               c.upload_session_id,
                               s.platform
                        FROM clips c
                        INNER JOIN edited_clips ec ON c.id = ec.original_clip_id
                        INNER JOIN streams s on s.id = c.stream_id
                        WHERE c.user_id = %s
                          AND c.deleted_at IS NULL AND ec.deleted_at IS NULL
                          AND (EXTRACT(EPOCH FROM ec.created_at), ec.id) < (%s, %s)
                        ORDER BY ec.created_at DESC, ec.id DESC
                        LIMIT %s
                    """
                    result = await self.db.fetch_all(
                        query, (user_id, cursor_timestamp, cursor_id, limit + 1)
                    )
                except Exception as e:
                    logger.error(
                        f"Invalid cursor format: {e}",
                        extra={"user_id": user_id, "cursor": cursor},
                    )
                    # Fall back to first page
                    cursor = None

            if not cursor:
                # First page
                query = """
                    SELECT ec.id,
                           ec.url,
                           c.kind,
                           ec.thumbnail_url,
                           c.watermark_url,
                           ec.name as caption,
                           c.start_time,
                           ec.created_at,
                           c.updated_at,
                           c.deleted_at,
                           c.favorited,
                           c.views,
                           c.stream_id,
                           c.owner_username,
                           c.user_id,
                           c.source_type,
                           c.upload_session_id,
                           s.platform
                    FROM clips c
                    INNER JOIN edited_clips ec ON c.id = ec.original_clip_id
                    INNER JOIN streams s on s.id = c.stream_id
                    WHERE c.user_id = %s
                      AND c.deleted_at IS NULL AND ec.deleted_at IS NULL
                    ORDER BY ec.created_at DESC, ec.id DESC
                    LIMIT %s
                """
                result = await self.db.fetch_all(query, (user_id, limit + 1))

            if not result:
                logger.debug(
                    f"No edited clips found for user ID {user_id}",
                    extra={"user_id": user_id},
                )
                return [], None, False

            # Check if there are more results
            has_more = len(result) > limit
            items = result[:limit]

            # Generate next cursor from last item
            next_cursor = None
            if has_more and items:
                from base64 import b64encode

                last_item = items[-1]
                last_id = last_item[0]
                last_created_at = last_item[7]
                cursor_str = f"{last_id}_{int(last_created_at.timestamp())}"
                next_cursor = b64encode(cursor_str.encode("utf-8")).decode("utf-8")

            clips = [self._row_to_clip(row) for row in items]
            logger.debug(
                f"Found {len(clips)} edited clips for user ID {user_id}",
                extra={
                    "user_id": user_id,
                    "clip_count": len(clips),
                    "has_more": has_more,
                },
            )
            return clips, next_cursor, has_more

    async def get_clips_by_username(
        self, username: str, limit: int = 10, offset: int = 1
    ) -> List[Clip]:
        """Get clips by owner username with pagination."""
        with span(
            logger,
            "get_clips_by_username",
            {"username": username, "limit": limit, "offset": offset},
        ):
            query = f"""
                    SELECT {CLIP_CORE_SELECT_COLUMNS},
                           {CLIP_ANALYTICS_SELECT_COLUMNS}
                    FROM clips c
                    INNER JOIN streams s on s.id = c.stream_id
                    LEFT JOIN clip_analytics ca on ca.clip_id = c.id AND ca.deleted_at IS NULL
                    WHERE c.owner_username = %s
                      AND c.deleted_at IS NULL
                    ORDER BY c.created_at DESC
                        LIMIT %s
                    OFFSET %s \
                    """
            result = await self.db.fetch_all(query, (username, limit, offset))
            if not result:
                logger.debug(
                    f"No clips found for username {username}",
                    extra={"username": username},
                )
                return []
            clips = [self._row_to_clip(row) for row in result]
            logger.debug(
                f"Found {len(clips)} clips for username {username}",
                extra={"username": username, "clip_count": len(clips)},
            )
            return clips

    async def get_clips_by_stream_ids(self, stream_ids: List[str]) -> List[Clip]:
        """Get all clips for the specified stream IDs sorted by start time."""
        with span(
            logger,
            "get_clips_by_stream_ids",
            {"stream_ids_count": len(stream_ids) if stream_ids else 0},
        ):
            if not stream_ids:
                logger.debug("No stream IDs provided, returning empty list")
                return []

            rows = await self.db.fetch_all(
                f"""
                SELECT {CLIP_CORE_SELECT_COLUMNS},
                       {CLIP_ANALYTICS_SELECT_COLUMNS}
                FROM clips c
                INNER JOIN streams s on s.id = c.stream_id
                LEFT JOIN clip_analytics ca on ca.clip_id = c.id AND ca.deleted_at IS NULL
                WHERE c.stream_id = ANY (%s)
                  AND c.deleted_at IS NULL
                ORDER BY c.start_time ASC
                """,
                (stream_ids,),
            )

            if not rows:
                logger.debug(
                    "No clips found for provided stream IDs",
                    extra={"stream_ids_count": len(stream_ids)},
                )
                return []

            clips = [self._row_to_clip(row) for row in rows]
            logger.debug(
                f"Found {len(clips)} clips for {len(stream_ids)} stream IDs",
                extra={"stream_ids_count": len(stream_ids), "clip_count": len(clips)},
            )
            return clips

    async def should_stop_generating_clips(self, stream_id: str) -> bool:
        """Validates for free tier users should generating clips or not"""
        """True: stop generating, False: good to go"""
        with span(logger, "should_stop_generating_clips"):
            settings = get_pro_model_settings()
            if not settings.pro_feature_enable:
                return False
            try:
                row = await self.db.fetch_one(
                    SHOULD_STOP_GENERATING_CLIPS,
                    (
                        stream_id,
                        settings.free_tier,
                        settings.max_clips_per_day,
                        settings.max_clips_per_month,
                    ),
                )
                if not row:
                    logger.error(
                        f"Error fetching database info for generate clip validation on stream {stream_id}"
                    )
                    return False
                else:
                    return bool(row[0])
            except Exception as e:
                logger.error(
                    f"Error validating for free tier users if should generating clips on stream {stream_id}, error message: {e}"
                )
                raise

    async def get_clip_by_id(self, clip_id: int) -> Optional[Clip]:
        """Get a clip by its ID."""
        with span(logger, "get_clip_by_id", {"clip_id": clip_id}):
            row = await self.db.fetch_one(
                f"""
                                          SELECT {CLIP_CORE_SELECT_COLUMNS},
                                                 {CLIP_ANALYTICS_SELECT_COLUMNS}
                                          FROM clips c
                                          INNER JOIN streams s on s.id = c.stream_id
                                          LEFT JOIN clip_analytics ca on ca.clip_id = c.id AND ca.deleted_at IS NULL
                                          WHERE c.id = %s
                                            AND c.deleted_at IS NULL
                                          """,
                (clip_id,),
            )

            if not row:
                logger.debug(
                    f"Clip with ID {clip_id} not found", extra={"clip_id": clip_id}
                )
                return None

            clip = self._row_to_clip(row)
            logger.debug(
                f"Found clip with ID {clip_id}",
                extra={"clip_id": clip_id, "clip_kind": clip.kind},
            )
            return clip

    async def get_clips_by_stream(
        self, stream_id: str, limit: int = 50, offset: int = 0
    ):
        """Get all clips for a specific stream."""
        with span(
            logger,
            "get_clips_by_stream",
            {"stream_id": stream_id, "limit": limit, "offset": offset},
        ):
            rows = await self.db.fetch_all(
                f"""
                                           SELECT {CLIP_CORE_SELECT_COLUMNS},
                                                  {CLIP_ANALYTICS_SELECT_COLUMNS}
                                           FROM clips c
                                           INNER JOIN streams s on s.id = c.stream_id
                                           LEFT JOIN clip_analytics ca on ca.clip_id = c.id AND ca.deleted_at IS NULL
                                           WHERE c.stream_id = %s
                                             AND c.deleted_at IS NULL
                                           ORDER BY c.start_time ASC
                                               LIMIT %s
                                           OFFSET %s
                                           """,
                (stream_id, limit, offset),
            )

            if not rows:
                logger.debug(
                    f"No clips found for stream ID {stream_id}",
                    extra={"stream_id": stream_id},
                )
                return []

            clips = [self._row_to_clip(row) for row in rows]
            logger.debug(
                f"Found {len(clips)} clips for stream ID {stream_id}",
                extra={"stream_id": stream_id, "clip_count": len(clips)},
            )
            return clips

    async def get_clips_by_kind(
        self, kind: str, limit: int = 50, offset: int = 0
    ) -> List[Clip]:
        """Get clips by their type/kind."""
        with span(
            logger,
            "get_clips_by_kind",
            {"kind": kind, "limit": limit, "offset": offset},
        ):
            rows = await self.db.fetch_all(
                f"""
                                           SELECT {CLIP_CORE_SELECT_COLUMNS},
                                                  {CLIP_ANALYTICS_SELECT_COLUMNS}
                                           FROM clips c
                                           INNER JOIN streams s on s.id = c.stream_id
                                           LEFT JOIN clip_analytics ca on ca.clip_id = c.id AND ca.deleted_at IS NULL
                                           WHERE c.kind = %s
                                             AND c.deleted_at IS NULL
                                           ORDER BY c.created_at DESC
                                               LIMIT %s
                                           OFFSET %s
                                           """,
                (kind, limit, offset),
            )
            if not rows:
                logger.debug(f"No clips found for kind '{kind}'", extra={"kind": kind})
                return []

            clips = [self._row_to_clip(row) for row in rows]
            logger.debug(
                f"Found {len(clips)} clips for kind '{kind}'",
                extra={"kind": kind, "clip_count": len(clips)},
            )
            return clips

    async def get_clips_after_time(self, timestamp: int) -> List[Clip]:
        """Get clips created after a specific timestamp."""
        with span(logger, "get_clips_after_time", {"timestamp": timestamp}):
            rows = await self.db.fetch_all(
                f"""
                                           SELECT {CLIP_CORE_SELECT_COLUMNS},
                                                  {CLIP_ANALYTICS_SELECT_COLUMNS}
                                           FROM clips c
                                           INNER JOIN streams s on s.id = c.stream_id
                                           LEFT JOIN clip_analytics ca on ca.clip_id = c.id AND ca.deleted_at IS NULL
                                           WHERE c.start_time >= %s
                                             AND c.deleted_at IS NULL
                                           ORDER BY c.start_time DESC
                                           """,
                (timestamp,),
            )
            if not rows:
                logger.debug(
                    f"No clips found after timestamp {timestamp}",
                    extra={"timestamp": timestamp},
                )
                return []

            clips = [self._row_to_clip(row) for row in rows]
            logger.debug(
                f"Found {len(clips)} clips after timestamp {timestamp}",
                extra={"timestamp": timestamp, "clip_count": len(clips)},
            )
            return clips

    async def count_clips_by_stream_and_kind(self, stream_id: int, kind: str) -> int:
        """Count clips by internal stream ID and kind."""
        with span(
            logger,
            "count_clips_by_stream_and_kind",
            {"stream_id": stream_id, "kind": kind},
        ):
            row = await self.db.fetch_one(
                """
                                          SELECT COUNT(*)
                                          FROM clips
                                          WHERE stream_id = %s
                                            AND LOWER(kind) LIKE %s
                                            AND deleted_at IS NULL
                                          """,
                (stream_id, f"%{kind.lower()}%"),
            )
            count = row[0] if row else 0
            logger.debug(
                f"Counted {count} clips for stream ID {stream_id} with kind '{kind}'",
                extra={"stream_id": stream_id, "kind": kind, "count": count},
            )
            return count

    async def count_clips_by_stream(self, stream_id: int) -> int:
        """Count clips by internal stream ID."""
        with span(logger, "count_clips_by_stream", {"stream_id": stream_id}):
            row = await self.db.fetch_one(
                """
                                          SELECT COUNT(*)
                                          FROM clips
                                          WHERE stream_id = %s
                                            AND deleted_at IS NULL
                                          """,
                (stream_id,),
            )
            count = row[0] if row else 0
            logger.debug(
                f"Counted {count} clips for stream ID {stream_id}",
                extra={"stream_id": stream_id, "count": count},
            )
            return count

    async def has_one_clip(self, stream_id: int) -> bool:
        """Return True if the given internal stream ID has exactly one non-deleted clip."""
        with span(logger, "has_one_clip", {"stream_id": stream_id}):
            try:
                count = await self.count_clips_by_stream(stream_id)
                logger.debug(
                    "Checked clip count for stream",
                    extra={"stream_id": stream_id, "clip_count": count},
                )
                return count == 1
            except Exception as e:
                logger.error(
                    "Error checking if stream has a single clip",
                    extra={"stream_id": stream_id, "error": str(e)},
                )
                raise

    async def count_clip_views_by_stream(self, stream_id: int) -> int:
        """Count clip views by internal stream ID."""
        with span(logger, "count_clip_views_by_stream", {"stream_id": stream_id}):
            row = await self.db.fetch_one(
                """
                                          SELECT SUM(views)
                                          FROM clips
                                          WHERE stream_id = %s
                                            AND deleted_at IS NULL
                                          """,
                (stream_id,),
            )
            count = row[0] if row and row[0] is not None else 0
            logger.debug(
                f"Counted {count} total views for stream ID {stream_id}",
                extra={"stream_id": stream_id, "view_count": count},
            )
            return count

    async def update_clip(self, clip_id: int, update: ClipUpdate) -> None:
        """
        Update clip details.

        Args:
            clip_id: The ID of the clip to update
            update: The update data
        """
        update_dict = update.dict(exclude_none=True)
        if not update_dict:
            logger.debug(f"No updates provided for clip ID {clip_id}, skipping")
            return

        with span(
            logger,
            "update_clip",
            {"clip_id": clip_id, "fields": list(update_dict.keys())},
        ):
            set_clauses = ["updated_at = CURRENT_TIMESTAMP"]
            values = []

            for key, value in update_dict.items():
                set_clauses.append(key)
                values.append(value)

            try:
                await self.db.update(
                    table="clips",
                    set_columns=set_clauses,
                    set_values=values,
                    where_clause="id = %s AND deleted_at IS NULL",
                )
                values.append(clip_id)
                logger.info(
                    f"Successfully updated clip ID {clip_id} with fields: {list(update_dict.keys())}"
                )
            except Exception as e:
                logger.error(f"Error updating clip ID {clip_id}: {e}")
                raise

    async def soft_delete_clip(self, clip_id: int) -> None:
        """
        Soft delete a clip by setting deleted_at.

        Args:
            clip_id: The ID of the clip to soft delete
        """
        with span(logger, "soft_delete_clip", {"clip_id": clip_id}):
            try:
                await self.db.execute_commit(
                    """
                                             UPDATE clips
                                             SET deleted_at = CURRENT_TIMESTAMP
                                             WHERE id = %s
                                               AND deleted_at IS NULL
                                             """,
                    (clip_id,),
                )
                logger.info(f"Successfully soft deleted clip ID {clip_id}")
            except Exception as e:
                logger.error(f"Error soft deleting clip ID {clip_id}: {e}")
                raise

    async def hard_delete_clip(self, clip_id: int) -> None:
        """
        Hard delete a clip from the database.

        Args:
            clip_id: The ID of the clip to permanently delete
        """
        with span(logger, "hard_delete_clip", {"clip_id": clip_id}):
            logger.warning(f"Permanently deleting clip ID {clip_id} from database")
            try:
                await self.db.execute_commit(
                    """
                                             DELETE
                                             FROM clips
                                             WHERE id = %s
                                             """,
                    (clip_id,),
                )
                logger.info(f"Successfully hard deleted clip ID {clip_id}")
            except Exception as e:
                logger.error(f"Error hard deleting clip ID {clip_id}: {e}")
                raise

    async def increment_view_count(self, clip_id: int) -> None:
        """
        Increment the view count of a clip by 1.

        Args:
            clip_id: The ID of the clip to increment views for
        """
        with span(logger, "increment_view_count", {"clip_id": clip_id}):
            try:
                await self.db.execute_commit(
                    """
                                             UPDATE clips
                                             SET views      = views + 1,
                                                 updated_at = CURRENT_TIMESTAMP
                                             WHERE id = %s
                                               AND deleted_at IS NULL
                                             """,
                    (clip_id,),
                )
            except Exception as e:
                logger.error(
                    f"Error incrementing view count for clip ID {clip_id}: {e}"
                )
                raise

    def _row_to_clip(self, row) -> Clip:
        """
        Map columns as:
          0: id
          1: url
          2: kind
          3: thumbnail_url
          4: watermark_url
          5: caption
          6: start_time (int)
          7: created_at (datetime)
          8: updated_at (datetime)
          9: deleted_at (datetime or None)
          10: favorited
          11: views
          12: stream_id (int)
          13: owner_username (string)
          14: user_id (int)
          15: source_type
          16: upload_session_id (UUID or None)
          17: platform (text) from streams table
          18 - 27: analytics
        """
        caption = str(row[5]) if row[5] is not None else ""
        start_time = int(row[6])
        created_at = row[7]
        updated_at = row[8] if isinstance(row[8], datetime) else datetime.now()
        deleted_at = row[9] if isinstance(row[9], datetime) else None

        # Safely handle presence or absence of analytics columns.
        has_analytics = len(row) > 18 and row[18] is not None

        return Clip(
            id=row[0],
            url=row[1],
            kind=row[2],
            thumbnail_url=row[3],
            watermark_url=row[4],
            caption=caption,
            start_time=start_time,
            created_at=created_at,
            updated_at=updated_at,
            deleted_at=deleted_at,
            favorited=row[10] if row[10] is not None else False,
            views=int(row[11]),
            stream_id=int(row[12]),
            owner_username=str(row[13]) if row[13] is not None else "",
            user_id=int(row[14]) if row[14] is not None else -1,
            source_type=row[15] if row[15] is not None else "stream",
            upload_session_id=row[16],
            platform=row[17] if row[17] is not None else None,
            analytics=(
                ClipAnalytics(
                    id=row[18],
                    clip_id=row[0],
                    views=row[19],
                    heart_reactions=row[20],
                    smile_reactions=row[21],
                    laugh_reactions=row[22],
                    sign_reactions=row[23],
                    surprised_reactions=row[24],
                    created_at=row[25],
                    updated_at=row[26],
                    deleted_at=row[27],
                )
                if has_analytics
                else None
            ),
        )

    # Edited Clips Methods

    async def create_edited_clip(self, clip: EditedClipCreate) -> EditedClip:
        """
        Create a new edited clip in the database.

        Args:
            clip: The edited clip data to create

        Returns:
            The created edited clip with its ID and timestamps

        Raises:
            Exception: If the clip creation fails
        """
        with span(
            logger, "create_edited_clip", {"original_clip_id": clip.original_clip_id}
        ):
            columns = ["original_clip_id", "url", "name", "metadata", "thumbnail_url"]
            values = (
                clip.original_clip_id,
                clip.url,
                clip.name,
                clip.metadata,
                clip.thumbnail_url,
            )

            try:
                clip_id = await self.db.insert("edited_clips", columns, values)
                if not clip_id:
                    logger.error(
                        f"Failed to create edited clip for original clip {clip.original_clip_id}"
                    )
                    raise Exception("Failed to create edited clip")

                logger.info(
                    f"Successfully created edited clip for original clip {clip.original_clip_id}"
                )

                # Fetch the created clip to return with all fields
                created_clip = await self.get_edited_clip_by_id(clip_id)
                if not created_clip:
                    logger.error(f"Failed to fetch created edited clip ID {clip_id}")
                    raise Exception("Failed to fetch created edited clip")
                return created_clip
            except Exception as e:
                logger.error(f"Error creating edited clip: {e}")
                raise

    async def get_edited_clip_by_id(self, clip_id: int) -> Optional[EditedClip]:
        """
        Get an edited clip by its ID.

        Args:
            clip_id: The ID of the edited clip to retrieve

        Returns:
            The edited clip if found, None otherwise
        """
        with span(logger, "get_edited_clip_by_id", {"clip_id": clip_id}):
            row = await self.db.fetch_one(
                """
                SELECT id, original_clip_id, url, name, metadata, created_at, updated_at, deleted_at, thumbnail_url
                FROM edited_clips
                WHERE id = %s AND deleted_at IS NULL
            """,
                (clip_id,),
            )

            if not row:
                logger.debug(f"Edited clip with ID {clip_id} not found")
                return None

            edited_clip = self._row_to_edited_clip(row)
            logger.debug(f"Found edited clip with ID {clip_id}")
            return edited_clip

    async def get_edited_clips_by_original_clip_id(
        self, original_clip_id: int
    ) -> List[EditedClip]:
        """
        Get all edited versions of an original clip.

        Args:
            original_clip_id: The ID of the original clip

        Returns:
            A list of edited clips for the original clip
        """
        with span(
            logger,
            "get_edited_clips_by_original_clip_id",
            {"original_clip_id": original_clip_id},
        ):
            rows = await self.db.fetch_all(
                """
                SELECT id, original_clip_id, url, name, metadata, created_at, updated_at, deleted_at, thumbnail_url
                FROM edited_clips
                WHERE original_clip_id = %s AND deleted_at IS NULL
                ORDER BY created_at DESC
            """,
                (original_clip_id,),
            )

            if not rows:
                logger.debug(
                    f"No edited clips found for original clip ID {original_clip_id}"
                )
                return []

            edited_clips = [self._row_to_edited_clip(row) for row in rows]
            logger.debug(
                f"Found {len(edited_clips)} edited clips for original clip ID {original_clip_id}"
            )
            return edited_clips

    async def update_edited_clip(self, clip_id: int, update: EditedClipUpdate) -> None:
        """
        Update an edited clip.

        Args:
            clip_id: The ID of the edited clip to update
            update: The update data

        Raises:
            Exception: If the update fails
        """
        update_dict = update.model_dump(exclude_none=True)
        if not update_dict:
            logger.debug(f"No updates provided for edited clip ID {clip_id}, skipping")
            return

        with span(
            logger,
            "update_edited_clip",
            {"clip_id": clip_id, "fields": list(update_dict.keys())},
        ):
            set_clauses = []
            values = []

            for key, value in update_dict.items():
                set_clauses.append(key)
                values.append(value)

            try:
                await self.db.update(
                    table="edited_clips",
                    set_columns=set_clauses,
                    set_values=values,
                    where_clause="id = %s AND deleted_at IS NULL",
                    where_params=(clip_id,),
                )
                # values.append(clip_id)
                logger.info(
                    f"Successfully updated edited clip ID {clip_id} with fields: {list(update_dict.keys())}"
                )
            except Exception as e:
                logger.error(f"Error updating edited clip ID {clip_id}: {e}")
                raise

    async def soft_delete_edited_clip(self, clip_id: int) -> None:
        """
        Soft delete an edited clip by setting deleted_at.

        Args:
            clip_id: The ID of the edited clip to soft delete

        Raises:
            Exception: If the deletion fails
        """
        with span(logger, "soft_delete_edited_clip", {"clip_id": clip_id}):
            try:
                await self.db.execute_commit(
                    """
                    UPDATE edited_clips
                    SET deleted_at = CURRENT_TIMESTAMP
                    WHERE id = %s AND deleted_at IS NULL
                """,
                    (clip_id,),
                )
                logger.info(f"Successfully soft deleted edited clip ID {clip_id}")
            except Exception as e:
                logger.error(f"Error soft deleting edited clip ID {clip_id}: {e}")
                raise

    def _row_to_edited_clip(self, row) -> EditedClip:
        """
        Convert a database row to an EditedClip model.

        Args:
            row: The database row

        Returns:
            An EditedClip model
        """
        return EditedClip(
            id=row[0],
            original_clip_id=row[1],
            url=row[2],
            name=row[3],
            metadata=row[4],
            created_at=row[5],
            updated_at=row[6],
            deleted_at=row[7],
            thumbnail_url=row[8],
        )


class ClipAnalyticsDAO:
    """Data Access Object for clip analytics in PostgreSQL."""

    def __init__(self, db: Db):
        # Initialize with an existing DB connection or create a new one.
        self.db = db if db else Db(DBConfig())
        logger.debug(
            "ClipAnalyticsDAO initialized", extra={"component": "ClipAnalyticsDAO"}
        )

    async def create_clip_analytics(
        self, analytics: ClipAnalyticsCreate
    ) -> Optional[ClipAnalytics]:
        """
        Create a new clip analytics record.
        """
        with span(logger, "create_clip_analytics", {"clip_id": analytics.clip_id}):
            columns = [
                "clip_id",
                "views",
                "heart_reactions",
                "smile_reactions",
                "laugh_reactions",
                "sign_reactions",
                "surprised_reactions",
            ]
            values = (
                analytics.clip_id,
                analytics.views,
                analytics.heart_reactions,
                analytics.smile_reactions,
                analytics.laugh_reactions,
                analytics.sign_reactions,
                analytics.surprised_reactions,
            )

            try:
                result = await self.db.insert(
                    table="clip_analytics", columns=columns, values=values
                )
                if result:
                    logger.info(
                        f"Created analytics for clip ID {analytics.clip_id}",
                        extra={
                            "clip_id": analytics.clip_id,
                            "views": analytics.views,
                            "heart_reactions": analytics.heart_reactions,
                        },
                    )
                    # Commented code preserved as it might be needed in the future
                    # return ClipAnalytics(
                    #     id=result[0],
                    #     clip_id=analytics.clip_id,
                    #     views=analytics.views,
                    #     hearts=analytics.hearts,
                    #     created_at=result[1],
                    #     updated_at=result[2],
                    #     deleted_at=None
                    # )
                else:
                    logger.error(
                        f"Failed to create analytics for clip ID {analytics.clip_id}",
                        extra={"clip_id": analytics.clip_id},
                    )
            except Exception as e:
                logger.error(
                    f"Error creating analytics for clip ID {analytics.clip_id}: {e}",
                    extra={"clip_id": analytics.clip_id, "error": str(e)},
                )

            return None

    async def get_clip_analytics_by_clip_id(
        self, clip_id: int
    ) -> Optional[ClipAnalytics]:
        """
        Retrieve the analytics for a specific clip using its ID.
        """
        with span(logger, "get_clip_analytics_by_clip_id", {"clip_id": clip_id}):
            try:
                row = await self.db.fetch_one(
                    """
                                              SELECT *
                                              FROM clip_analytics
                                              WHERE clip_id = %s
                                                AND deleted_at IS NULL
                                              """,
                    (clip_id,),
                )
                if not row:
                    logger.debug(
                        f"No analytics found for clip ID {clip_id}",
                        extra={"clip_id": clip_id},
                    )
                    return None

                analytics = self._row_to_clip_analytics(row)
                logger.debug(
                    f"Retrieved analytics for clip ID {clip_id}",
                    extra={
                        "clip_id": clip_id,
                        "views": analytics.views,
                        "heart_reactions": analytics.heart_reactions,
                    },
                )
                return analytics
            except Exception as e:
                logger.error(
                    f"Error retrieving analytics for clip ID {clip_id}: {e}",
                    extra={"clip_id": clip_id, "error": str(e)},
                )
                return None

    async def update_clip_analytics(
        self, analytics_id: int, update: ClipAnalyticsUpdate
    ) -> None:
        """
        Update clip analytics details.
        """
        with span(logger, "update_clip_analytics", {"analytics_id": analytics_id}):
            update_dict = update.dict(exclude_none=True)
            if not update_dict:
                logger.debug(
                    f"No updates provided for analytics ID {analytics_id}, skipping",
                    extra={"analytics_id": analytics_id},
                )
                return

            set_clauses = ["updated_at = CURRENT_TIMESTAMP"]
            values = []
            for key, value in update_dict.items():
                set_clauses.append(key)
                values.append(value)

            try:
                # Update the clip analytics record.
                await self.db.update(
                    table="clip_analytics",
                    set_columns=set_clauses,
                    set_values=values,
                    where_clause="id = %s AND deleted_at IS NULL",
                )
                values.append(analytics_id)
                logger.info(
                    f"Updated analytics ID {analytics_id} with fields: {list(update_dict.keys())}",
                    extra={
                        "analytics_id": analytics_id,
                        "fields": list(update_dict.keys()),
                    },
                )
            except Exception as e:
                logger.error(
                    f"Error updating analytics ID {analytics_id}: {e}",
                    extra={"analytics_id": analytics_id, "error": str(e)},
                )
                raise

    async def soft_delete_clip_analytics(self, analytics_id: int) -> None:
        """
        Soft delete a clip analytics record by setting deleted_at.
        """
        with span(logger, "soft_delete_clip_analytics", {"analytics_id": analytics_id}):
            try:
                await self.db.execute_commit(
                    """
                                             UPDATE clip_analytics
                                             SET deleted_at = CURRENT_TIMESTAMP
                                             WHERE id = %s
                                               AND deleted_at IS NULL
                                             """,
                    (analytics_id,),
                )
                logger.info(
                    f"Soft deleted analytics ID {analytics_id}",
                    extra={"analytics_id": analytics_id},
                )
            except Exception as e:
                logger.error(
                    f"Error soft deleting analytics ID {analytics_id}: {e}",
                    extra={"analytics_id": analytics_id, "error": str(e)},
                )
                raise

    async def increment_emoji_reaction(self, clip_id: int, emoji_type: str) -> None:
        """
        Increment the specified emoji reaction count for a clip by 1.
        Creates analytics record if it doesn't exist.

        Args:
            clip_id: The ID of the clip to increment emoji reaction for
            emoji_type: The type of emoji reaction (heart, smile, laugh, sign, surprised)
        """
        with span(
            logger,
            "increment_emoji_reaction",
            {"clip_id": clip_id, "emoji_type": emoji_type},
        ):
            # Map emoji types to column names
            emoji_column_map = {
                "heart": "heart_reactions",
                "smile": "smile_reactions",
                "laugh": "laugh_reactions",
                "sign": "sign_reactions",
                "surprised": "surprised_reactions",
            }

            if emoji_type not in emoji_column_map:
                raise ValueError(f"Invalid emoji type: {emoji_type}")

            column_name = emoji_column_map[emoji_type]

            try:
                # Try to update existing analytics record
                result = await self.db.execute_commit(
                    f"""
                    UPDATE clip_analytics
                    SET {column_name} = {column_name} + 1,
                        updated_at = CURRENT_TIMESTAMP
                    WHERE clip_id = %s
                      AND deleted_at IS NULL
                    """,
                    (clip_id,),
                )

                # If no rows were affected, create a new analytics record
                # execute_commit returns affected row count (or None if unavailable)
                if not result:
                    from common.models.clip import ClipAnalyticsCreate

                    new_analytics = ClipAnalyticsCreate(
                        clip_id=clip_id,
                        views=0,
                        heart_reactions=1 if emoji_type == "heart" else 0,
                        smile_reactions=1 if emoji_type == "smile" else 0,
                        laugh_reactions=1 if emoji_type == "laugh" else 0,
                        sign_reactions=1 if emoji_type == "sign" else 0,
                        surprised_reactions=1 if emoji_type == "surprised" else 0,
                    )
                    await self.create_clip_analytics(new_analytics)
                    logger.info(
                        f"Created new analytics record with {emoji_type} reaction for clip ID {clip_id}"
                    )

            except Exception as e:
                logger.error(
                    f"Error incrementing {emoji_type} reaction for clip ID {clip_id}: {e}",
                    extra={
                        "clip_id": clip_id,
                        "emoji_type": emoji_type,
                        "error": str(e),
                    },
                )
                raise

    async def increment_views(self, clip_id: int) -> None:
        """
        Increment the views count for a clip in clip_analytics, creating
        the analytics row if it does not exist yet.

        Args:
            clip_id: The ID of the clip to increment views for
        """
        with span(logger, "increment_analytics_views", {"clip_id": clip_id}):
            try:
                result = await self.db.execute_commit(
                    """
                    UPDATE clip_analytics
                    SET views = views + 1,
                        updated_at = CURRENT_TIMESTAMP
                    WHERE clip_id = %s
                      AND deleted_at IS NULL
                    """,
                    (clip_id,),
                )

                if not result:
                    # Create analytics row with views=1 when missing
                    from common.models.clip import ClipAnalyticsCreate

                    new_analytics = ClipAnalyticsCreate(
                        clip_id=clip_id,
                        views=1,
                        heart_reactions=0,
                        smile_reactions=0,
                        laugh_reactions=0,
                        sign_reactions=0,
                        surprised_reactions=0,
                    )
                    await self.create_clip_analytics(new_analytics)
                    logger.info(
                        f"Created new analytics record with 1 view for clip ID {clip_id}"
                    )
            except Exception as e:
                logger.error(
                    f"Error incrementing analytics views for clip ID {clip_id}: {e}",
                    extra={"clip_id": clip_id, "error": str(e)},
                )
                raise

    def _row_to_clip_analytics(self, row) -> ClipAnalytics:
        """
        Convert a database row to a ClipAnalytics model.
        """
        return ClipAnalytics(
            id=row[0],
            clip_id=row[1],
            views=row[2],
            heart_reactions=row[3],
            smile_reactions=row[4],
            laugh_reactions=row[5],
            sign_reactions=row[6],
            surprised_reactions=row[7],
            created_at=row[8],
            updated_at=row[9],
            deleted_at=row[10],
        )
