from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any

from minio import Minio
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection, AsyncIOMotorDatabase
from pymongo import ASCENDING, DESCENDING

from app.auth.passwords import hash_password
from app.config import Settings


class Database:
    """Holds external clients for dependency injection."""

    def __init__(self, settings: Settings) -> None:
        self.settings = settings
        self.mongo_client: AsyncIOMotorClient[dict[str, Any]] | None = None
        self.db: AsyncIOMotorDatabase[dict[str, Any]] | None = None
        self.clips: AsyncIOMotorCollection[dict[str, Any]] | None = None
        self.users: AsyncIOMotorCollection[dict[str, Any]] | None = None
        self.minio_client: Minio | None = None

    async def connect(self) -> None:
        self.mongo_client = AsyncIOMotorClient(self.settings.mongo_uri)
        self.db = self.mongo_client[self.settings.mongo_db]
        self.clips = self.db[self.settings.mongo_collection]
        self.users = self.db[self.settings.mongo_users_collection]
        self.minio_client = Minio(
            self.settings.minio_endpoint,
            access_key=self.settings.minio_access_key,
            secret_key=self.settings.minio_secret_key,
            secure=self.settings.minio_secure,
        )
        await self.ensure_indexes()
        await self.bootstrap_admin_user()

    async def close(self) -> None:
        if self.mongo_client is not None:
            self.mongo_client.close()

    async def ensure_indexes(self) -> None:
        collection = self.get_clip_collection()
        await collection.create_index(
            [("camera_id", ASCENDING), ("timestamp", DESCENDING)],
            name="camera_timestamp_idx",
        )
        await collection.create_index([("timestamp", DESCENDING)], name="timestamp_idx")
        await collection.create_index([("detected_object", ASCENDING)], name="detected_object_idx")
        await self.get_user_collection().create_index([("username", ASCENDING)], unique=True, name="username_idx")

    async def bootstrap_admin_user(self) -> None:
        password_hash = self.settings.admin_password_hash
        if password_hash is None and self.settings.admin_password is not None:
            password_hash = hash_password(self.settings.admin_password)
        if password_hash is None:
            return
        users = self.get_user_collection()
        await users.update_one(
            {"username": self.settings.admin_username},
            {
                "$setOnInsert": {
                    "username": self.settings.admin_username,
                    "password_hash": password_hash,
                    "is_active": True,
                }
            },
            upsert=True,
        )

    def get_clip_collection(self) -> AsyncIOMotorCollection[dict[str, Any]]:
        if self.clips is None:
            raise RuntimeError("Database is not connected")
        return self.clips

    def get_user_collection(self) -> AsyncIOMotorCollection[dict[str, Any]]:
        if self.users is None:
            raise RuntimeError("Database is not connected")
        return self.users

    def get_minio_client(self) -> Minio:
        if self.minio_client is None:
            raise RuntimeError("Storage is not connected")
        return self.minio_client


@asynccontextmanager
async def lifespan_context(settings: Settings) -> AsyncIterator[Database]:
    database = Database(settings)
    await database.connect()
    try:
        yield database
    finally:
        await database.close()
