"""Data Access Object for video templates."""

import json
from typing import List, Optional, Tuple

from common.database import DBConfig, Db
from common.logging import get_logger, span
from common.models.video_template import (
    ModifiableField,
    TierLevel,
    UserTemplatePreferences,
    UserTemplatePreferencesUpdate,
    VideoTemplate,
    VideoTemplateCreate,
    VideoTemplateUpdate,
)

logger = get_logger(__name__)

# Tier hierarchy for access control
TIER_HIERARCHY = {
    TierLevel.BASIC: 0,
    TierLevel.PRO: 1,
    TierLevel.ENTERPRISE: 2,
}

SELECT_TEMPLATE_BASE = """
    SELECT id, user_id, creatomate_template_id,
           name, description, thumbnail_url,
           is_global, tier_required, category,
           output_width, output_height, output_format,
           modifiable_fields, use_count,
           created_at, updated_at, deleted_at
    FROM video_templates
"""

SELECT_PREFERENCES_BASE = """
    SELECT id, user_id,
           default_clip_template_id, default_highlight_template_id, default_story_template_id,
           created_at, updated_at
    FROM user_template_preferences
"""


class VideoTemplateDAO:
    """Data Access Object for video_templates 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(
            "VideoTemplateDAO initialized", extra={"component": "VideoTemplateDAO"}
        )

    async def get_template_by_id(self, template_id: int) -> Optional[VideoTemplate]:
        """Get a template by its ID."""
        with span(
            logger,
            "vt_get_by_id",
            {"template_id": template_id},
            log_entry=False,
            log_exit=False,
        ):
            row = await self.db.fetch_one(
                SELECT_TEMPLATE_BASE + "WHERE id = %s AND deleted_at IS NULL",
                (template_id,),
            )
            if not row:
                return None
            return self._row_to_template(row)

    async def get_by_creatomate_id(
        self, creatomate_template_id: str
    ) -> Optional[VideoTemplate]:
        """Get a template by its Creatomate template ID.

        Args:
            creatomate_template_id: The Creatomate template ID to look up.

        Returns:
            The VideoTemplate if found, None otherwise.
        """
        with span(
            logger,
            "vt_get_by_creatomate_id",
            {"creatomate_template_id": creatomate_template_id},
            log_entry=False,
            log_exit=False,
        ):
            row = await self.db.fetch_one(
                SELECT_TEMPLATE_BASE
                + "WHERE creatomate_template_id = %s AND deleted_at IS NULL",
                (creatomate_template_id,),
            )
            if not row:
                return None
            return self._row_to_template(row)

    async def upsert_by_creatomate_id(
        self, template: VideoTemplateCreate
    ) -> Tuple[VideoTemplate, bool]:
        """Insert or update a template by its Creatomate template ID.

        If a template with the same creatomate_template_id exists, it will be
        updated with the new values. Otherwise, a new template is created.

        Args:
            template: The template data to insert or update.

        Returns:
            Tuple of (VideoTemplate, was_created) where was_created is True
            if a new template was created, False if an existing one was updated.
        """
        with span(
            logger,
            "vt_upsert_by_creatomate_id",
            {"creatomate_template_id": template.creatomate_template_id},
            log_entry=False,
            log_exit=False,
        ):
            existing = await self.get_by_creatomate_id(template.creatomate_template_id)

            if existing:
                # Update existing template
                update = VideoTemplateUpdate(
                    name=template.name,
                    description=template.description,
                    thumbnail_url=template.thumbnail_url,
                    tier_required=template.tier_required,
                    category=template.category,
                    output_width=template.output_width,
                    output_height=template.output_height,
                    output_format=template.output_format,
                    modifiable_fields=template.modifiable_fields,
                )
                updated = await self.update_template(existing.id, update)
                if not updated:
                    raise RuntimeError(
                        f"Failed to update template {existing.id} "
                        f"(creatomate_id={template.creatomate_template_id})"
                    )
                logger.info(
                    "Updated video template via upsert",
                    extra={
                        "template_id": updated.id,
                        "creatomate_template_id": template.creatomate_template_id,
                    },
                )
                return (updated, False)
            else:
                # Create new template
                created = await self.create_template(template)
                logger.info(
                    "Created video template via upsert",
                    extra={
                        "template_id": created.id,
                        "creatomate_template_id": template.creatomate_template_id,
                    },
                )
                return (created, True)

    async def get_templates_for_user(
        self,
        user_id: int,
        user_tier: str,
        category: Optional[str] = None,
        limit: int = 50,
        offset: int = 0,
    ) -> List[VideoTemplate]:
        """Get templates accessible to a user based on their tier.

        Returns both:
        - Global templates the user's tier can access
        - User's own templates
        """
        with span(
            logger,
            "vt_get_for_user",
            {"user_id": user_id, "tier": user_tier, "category": category},
            log_entry=False,
            log_exit=False,
        ):
            # Determine accessible tiers
            try:
                user_tier_level = TIER_HIERARCHY.get(TierLevel(user_tier), 0)
            except ValueError:
                user_tier_level = 0

            accessible_tiers = [
                tier.value
                for tier, level in TIER_HIERARCHY.items()
                if level <= user_tier_level
            ]

            query = (
                SELECT_TEMPLATE_BASE
                + """
                WHERE deleted_at IS NULL
                AND (
                    (is_global = TRUE AND tier_required = ANY(%s))
                    OR user_id = %s
                )
            """
            )
            params: list = [accessible_tiers, user_id]

            if category:
                query += " AND category = %s"
                params.append(category)

            query += (
                " ORDER BY is_global DESC, use_count DESC, name ASC LIMIT %s OFFSET %s"
            )
            params.extend([limit, offset])

            rows = await self.db.fetch_all(query, tuple(params))
            return [self._row_to_template(row) for row in (rows or [])]

    async def get_global_templates(
        self,
        tier: Optional[str] = None,
        category: Optional[str] = None,
        limit: int = 50,
        offset: int = 0,
    ) -> List[VideoTemplate]:
        """Get global templates, optionally filtered by tier and category."""
        with span(
            logger,
            "vt_get_global",
            {"tier": tier, "category": category},
            log_entry=False,
            log_exit=False,
        ):
            query = (
                SELECT_TEMPLATE_BASE + "WHERE deleted_at IS NULL AND is_global = TRUE"
            )
            params: list = []

            if tier:
                query += " AND tier_required = %s"
                params.append(tier)

            if category:
                query += " AND category = %s"
                params.append(category)

            query += " ORDER BY use_count DESC, name ASC LIMIT %s OFFSET %s"
            params.extend([limit, offset])

            rows = await self.db.fetch_all(query, tuple(params))
            return [self._row_to_template(row) for row in (rows or [])]

    async def get_user_templates(
        self,
        user_id: int,
        limit: int = 50,
        offset: int = 0,
    ) -> List[VideoTemplate]:
        """Get templates owned by a specific user."""
        with span(
            logger,
            "vt_get_user_templates",
            {"user_id": user_id},
            log_entry=False,
            log_exit=False,
        ):
            query = (
                SELECT_TEMPLATE_BASE
                + """
                WHERE user_id = %s AND deleted_at IS NULL
                ORDER BY updated_at DESC
                LIMIT %s OFFSET %s
            """
            )
            rows = await self.db.fetch_all(query, (user_id, limit, offset))
            return [self._row_to_template(row) for row in (rows or [])]

    async def create_template(self, template: VideoTemplateCreate) -> VideoTemplate:
        """Create a new template."""
        with span(
            logger,
            "vt_create",
            {"template_name": template.name, "is_global": template.is_global},
            log_entry=False,
            log_exit=False,
        ):
            columns = [
                "user_id",
                "creatomate_template_id",
                "name",
                "description",
                "thumbnail_url",
                "is_global",
                "tier_required",
                "category",
                "output_width",
                "output_height",
                "output_format",
                "modifiable_fields",
            ]

            # Serialize modifiable_fields to JSON
            modifiable_fields_json = json.dumps(
                [field.model_dump() for field in template.modifiable_fields]
            )

            values = (
                template.user_id,
                template.creatomate_template_id,
                template.name,
                template.description,
                template.thumbnail_url,
                template.is_global,
                template.tier_required.value,
                template.category,
                template.output_width,
                template.output_height,
                template.output_format,
                modifiable_fields_json,
            )

            new_id = await self.db.insert("video_templates", columns, values)
            if not new_id:
                raise RuntimeError("Failed to create video template")

            logger.info(
                "Created video template",
                extra={
                    "template_id": new_id,
                    "template_name": template.name,
                    "is_global": template.is_global,
                },
            )

            result = await self.get_template_by_id(new_id)
            if not result:
                raise RuntimeError("Failed to fetch created template")
            return result

    async def update_template(
        self, template_id: int, update: VideoTemplateUpdate
    ) -> Optional[VideoTemplate]:
        """Update an existing template."""
        with span(
            logger,
            "vt_update",
            {"template_id": template_id},
            log_entry=False,
            log_exit=False,
        ):
            update_dict = update.model_dump(exclude_none=True)
            if not update_dict:
                return await self.get_template_by_id(template_id)

            # Handle modifiable_fields serialization
            if "modifiable_fields" in update_dict:
                fields = update_dict["modifiable_fields"]
                # Handle both dict (from model_dump) and ModifiableField objects
                serialized_fields = [
                    field if isinstance(field, dict) else field.model_dump()
                    for field in fields
                ]
                update_dict["modifiable_fields"] = json.dumps(serialized_fields)

            # Handle tier_required enum
            if "tier_required" in update_dict:
                update_dict["tier_required"] = update_dict["tier_required"].value

            set_clauses = list(update_dict.keys())
            set_values = list(update_dict.values())

            await self.db.update(
                table="video_templates",
                set_columns=set_clauses,
                set_values=set_values,
                where_clause="id = %s AND deleted_at IS NULL",
                where_params=(template_id,),
            )

            logger.info(
                "Updated video template",
                extra={"template_id": template_id, "fields": list(update_dict.keys())},
            )

            return await self.get_template_by_id(template_id)

    async def delete_template(self, template_id: int) -> None:
        """Soft delete a template."""
        with span(
            logger,
            "vt_delete",
            {"template_id": template_id},
            log_entry=False,
            log_exit=False,
        ):
            await self.db.execute_commit(
                """
                UPDATE video_templates
                SET deleted_at = NOW(), updated_at = NOW()
                WHERE id = %s AND deleted_at IS NULL
                """,
                (template_id,),
            )
            logger.info(
                "Soft deleted video template", extra={"template_id": template_id}
            )

    async def increment_use_count(self, template_id: int) -> None:
        """Increment the use count for a template."""
        with span(
            logger,
            "vt_increment_use",
            {"template_id": template_id},
            log_entry=False,
            log_exit=False,
        ):
            await self.db.execute_commit(
                """
                UPDATE video_templates
                SET use_count = use_count + 1, updated_at = NOW()
                WHERE id = %s AND deleted_at IS NULL
                """,
                (template_id,),
            )

    async def can_user_access_template(
        self, template_id: int, user_id: int, user_tier: str
    ) -> bool:
        """Check if user can access a specific template."""
        with span(
            logger,
            "vt_check_access",
            {"template_id": template_id, "user_id": user_id, "tier": user_tier},
            log_entry=False,
            log_exit=False,
        ):
            template = await self.get_template_by_id(template_id)
            if not template:
                return False

            # User owns the template
            if template.user_id == user_id:
                return True

            # Global template - check tier
            if template.is_global:
                try:
                    user_tier_level = TIER_HIERARCHY.get(TierLevel(user_tier), 0)
                except ValueError:
                    user_tier_level = 0
                required_tier_level = TIER_HIERARCHY.get(template.tier_required, 0)
                return user_tier_level >= required_tier_level

            return False

    # User Template Preferences Methods

    async def get_user_preferences(
        self, user_id: int
    ) -> Optional[UserTemplatePreferences]:
        """Get user's template preferences."""
        with span(
            logger,
            "vt_get_prefs",
            {"user_id": user_id},
            log_entry=False,
            log_exit=False,
        ):
            row = await self.db.fetch_one(
                SELECT_PREFERENCES_BASE + "WHERE user_id = %s",
                (user_id,),
            )
            if not row:
                return None
            return self._row_to_preferences(row)

    async def upsert_user_preferences(
        self, user_id: int, preferences: UserTemplatePreferencesUpdate
    ) -> UserTemplatePreferences:
        """Create or update user's template preferences."""
        with span(
            logger,
            "vt_upsert_prefs",
            {"user_id": user_id},
            log_entry=False,
            log_exit=False,
        ):
            update_dict = preferences.model_dump(exclude_none=True)

            # Check if preferences exist
            existing = await self.get_user_preferences(user_id)

            if existing:
                # Update existing
                if update_dict:
                    set_clauses = list(update_dict.keys())
                    set_values = list(update_dict.values())

                    await self.db.update(
                        table="user_template_preferences",
                        set_columns=set_clauses,
                        set_values=set_values,
                        where_clause="user_id = %s",
                        where_params=(user_id,),
                    )
            else:
                # Insert new
                columns = ["user_id"] + list(update_dict.keys())
                values = tuple([user_id] + list(update_dict.values()))

                await self.db.insert("user_template_preferences", columns, values)

            logger.info(
                "Upserted user template preferences",
                extra={"user_id": user_id, "fields": list(update_dict.keys())},
            )

            result = await self.get_user_preferences(user_id)
            if not result:
                raise RuntimeError("Failed to fetch upserted preferences")
            return result

    def _row_to_template(self, row) -> VideoTemplate:
        """Convert database row to VideoTemplate model."""
        # Parse JSONB modifiable_fields
        modifiable_fields_raw = row[12]
        if isinstance(modifiable_fields_raw, str):
            modifiable_fields_raw = json.loads(modifiable_fields_raw)
        modifiable_fields = [
            ModifiableField(**field) for field in (modifiable_fields_raw or [])
        ]

        return VideoTemplate(
            id=row[0],
            user_id=row[1],
            creatomate_template_id=row[2],
            name=row[3],
            description=row[4],
            thumbnail_url=row[5],
            is_global=row[6],
            tier_required=TierLevel(row[7]),
            category=row[8],
            output_width=row[9],
            output_height=row[10],
            output_format=row[11],
            modifiable_fields=modifiable_fields,
            use_count=row[13],
            created_at=row[14],
            updated_at=row[15],
            deleted_at=row[16],
        )

    def _row_to_preferences(self, row) -> UserTemplatePreferences:
        """Convert database row to UserTemplatePreferences model."""
        return UserTemplatePreferences(
            id=row[0],
            user_id=row[1],
            default_clip_template_id=row[2],
            default_highlight_template_id=row[3],
            default_story_template_id=row[4],
            created_at=row[5],
            updated_at=row[6],
        )
