from datetime import datetime
from typing import Any, Literal

from bson import ObjectId
from bson.errors import InvalidId
from motor.motor_asyncio import AsyncIOMotorCollection

from app.errors import ApiError
from app.schemas.clip import Clip

SortOrder = Literal["asc", "desc"]


class ClipService:
    def __init__(self, collection: AsyncIOMotorCollection[dict[str, Any]]) -> None:
        self.collection = collection

    def _build_filter(
        self,
        camera_id: int | None = None,
        detected_object: str | None = None,
        start: datetime | None = None,
        end: datetime | None = None,
    ) -> dict[str, Any]:
        query: dict[str, Any] = {}
        if camera_id is not None:
            query["camera_id"] = camera_id
        if detected_object is not None:
            query["detected_object"] = detected_object
        if start is not None or end is not None:
            range_query: dict[str, Any] = {}
            if start is not None:
                range_query["$gte"] = start
            if end is not None:
                range_query["$lte"] = end
            query["timestamp"] = range_query
        return query

    def _to_clip(self, document: dict[str, Any]) -> Clip:
        return Clip(
            id=str(document["_id"]),
            camera_id=document["camera_id"],
            filename=document.get("filename"),
            s3_path=document["s3_path"],
            timestamp=document["timestamp"],
            uploaded_at=document.get("uploaded_at"),
            detected_object=document["detected_object"],
            duration_seconds=document.get("duration_seconds"),
            size_bytes=document.get("size_bytes"),
            thumbnail_path=document.get("thumbnail_path"),
        )

    async def search_clips(
        self,
        camera_id: int | None,
        detected_object: str | None,
        start: datetime | None,
        end: datetime | None,
        page: int,
        limit: int,
        sort: SortOrder,
    ) -> tuple[list[Clip], int]:
        query = self._build_filter(camera_id, detected_object, start, end)
        direction = 1 if sort == "asc" else -1
        skip = (page - 1) * limit
        total = await self.count_clips(query)
        cursor = self.collection.find(query).sort("timestamp", direction).skip(skip).limit(limit)
        return [self._to_clip(document) async for document in cursor], total

    async def count_clips(self, query: dict[str, Any]) -> int:
        return await self.collection.count_documents(query)

    async def get_clip(self, clip_id: str) -> Clip:
        document = await self._get_clip_document(clip_id)
        return self._to_clip(document)

    async def _get_clip_document(self, clip_id: str) -> dict[str, Any]:
        try:
            object_id = ObjectId(clip_id)
        except InvalidId as exc:
            raise ApiError("invalid_clip_id", "Clip id is not valid", 422) from exc
        document = await self.collection.find_one({"_id": object_id})
        if document is None:
            raise ApiError("clip_not_found", "Clip does not exist", 404)
        return document
