from typing import Optional, List

from common.database import DBConfig, Db
from common.logging import get_logger, span

from ..models.stream_deck_device import StreamDeckDevice

logger = get_logger(__name__)


class StreamDeckDeviceError(Exception):
    """Base exception for stream deck device operations."""


class StreamDeckDeviceValidationError(StreamDeckDeviceError):
    """Invalid payload or precondition for stream deck device operations."""


class StreamDeckDeviceNotFoundError(StreamDeckDeviceError):
    """Stream deck device or related subject was not found."""


class StreamDeckDeviceForbiddenError(StreamDeckDeviceError):
    """Current user cannot perform the requested stream deck action."""


class StreamDeckDeviceConflictError(StreamDeckDeviceError):
    """Requested stream deck action conflicts with existing state."""


SELECT_BASE = """
    SELECT id, user_id, device_id, created_at, deleted_at
    FROM stream_deck_devices
"""

SELECT_ACTIVE_BY_USER_QUERY = (
    SELECT_BASE
    + """
    WHERE user_id = %s
      AND deleted_at IS NULL
    ORDER BY created_at DESC, id DESC
"""
)

SELECT_BY_DEVICE_ID_QUERY = (
    SELECT_BASE
    + """
    WHERE device_id = %s
    ORDER BY id DESC
    LIMIT 1
"""
)

SOFT_DELETE_BY_DEVICE_ID_QUERY = """
    UPDATE stream_deck_devices
    SET deleted_at = NOW()
    WHERE device_id = %s
      AND deleted_at IS NULL
"""

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

INSERT_DEVICE_QUERY = """
    INSERT INTO stream_deck_devices (user_id, device_id)
    VALUES (%s, %s)
    RETURNING id, user_id, device_id, created_at, deleted_at
"""


class StreamDeckDeviceDAO:
    """DAO for stream_deck_devices."""

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

    async def get_by_user(self, user_id: int) -> List[StreamDeckDevice]:
        """Return all ACTIVE devices for a user (deleted_at IS NULL)."""
        with span(logger, "sd_get_by_user", {"user_id": user_id}):
            rows = await self.db.fetch_all(SELECT_ACTIVE_BY_USER_QUERY, (user_id,))
            return [self._row_to_model(r) for r in rows]

    async def delete(self, device_id: str) -> None:
        """Soft delete a device by device_id."""
        with span(logger, "sd_delete", {"device_id": device_id}):
            d = (device_id or "").strip()
            if not d:
                raise StreamDeckDeviceValidationError("device_id cannot be empty")

            await self.db.execute_commit(SOFT_DELETE_BY_DEVICE_ID_QUERY, (d,))
            logger.info("Soft-deleted stream deck device", extra={"device_id": d})

    async def create(self, user_id: int, device_id: str) -> StreamDeckDevice:
        """
        Insert a new device row.
        - Requires users.tier = 'pro'
        - Fails if device_id already exists (because UNIQUE(device_id))
        """
        with span(logger, "sd_create", {"user_id": user_id}):
            d = (device_id or "").strip()
            if not d:
                raise StreamDeckDeviceValidationError("device_id cannot be empty")

            # Validate user is Pro
            tier_row = await self.db.fetch_one(GET_USER_TIER_QUERY, (user_id,))
            if not tier_row:
                raise StreamDeckDeviceNotFoundError(f"User {user_id} not found")

            tier = tier_row[0]
            if tier != "pro":
                raise StreamDeckDeviceForbiddenError(
                    "Stream Deck integration is available for Pro tier only"
                )

            # Insert-only behavior: do not revive/reassign. Fail if exists.
            existing = await self.db.fetch_one(SELECT_BY_DEVICE_ID_QUERY, (d,))
            if existing:
                # If you ever soft-delete devices, this will still block inserts due to UNIQUE(device_id).
                raise StreamDeckDeviceConflictError(
                    "This Stream Deck device is already linked"
                )

            row = await self.db.fetch_one(INSERT_DEVICE_QUERY, (user_id, d))
            if not row:
                raise RuntimeError("Insert returned no row for stream_deck_devices")

            model = self._row_to_model(row)
            logger.info(
                "Created stream deck device",
                extra={
                    "id": model.id,
                    "user_id": model.user_id,
                    "device_id": model.device_id,
                },
            )
            return model

    def _row_to_model(self, row) -> StreamDeckDevice:
        try:
            return StreamDeckDevice(
                id=row[0],
                user_id=row[1],
                device_id=row[2],
                created_at=row[3],
                deleted_at=row[4],
            )
        except (TypeError, KeyError):
            raise
