from collections.abc import AsyncIterator
import hashlib
from typing import cast, override
from uuid import uuid4

from fastapi import UploadFile
import neosqlite
from neosqlite.connection import Connection
from neosqlite.gridfs import NoFile
from pydantic import SecretStr

from phederation.models.media import MediaMetadata
from phederation.storage.nosql import NoSQLStorageBackend
from phederation.storage.nosqlitecollection import (
    AsyncFileIterator,
    AsyncNoSQLiteCollection,
)
from phederation.utils import UrlType
from phederation.utils.base import ObjectId, assemble_id_url
from phederation.utils.exceptions import NotFoundError, StorageError, catch_exceptions
from phederation.utils.settings import PhedSettings


class NoSQLiteStorageBackend(NoSQLStorageBackend):
    GRID_FS_COLLECTION: str = "__gridfs_files"

    def __init__(self, settings: PhedSettings, database_url: SecretStr):
        super().__init__(settings, database_url)

        self.client: Connection = neosqlite.Connection(database_url.get_secret_value())
        self.collections: dict[str, AsyncNoSQLiteCollection] = {}

    @override
    async def get_nosql_collection(self, table_name: str) -> AsyncNoSQLiteCollection:
        if not table_name in self.collections:
            collection = self.client.create_collection(table_name)  # pyright: ignore[reportUnknownMemberType]
            self.collections[table_name] = AsyncNoSQLiteCollection(table_name, collection)
        return self.collections[table_name]

    @catch_exceptions(StorageError, "NoSqlite: Failed to reconnect to database")
    @override
    async def reconnect(self) -> None:
        pass

    @override
    async def save(self, document_id: ObjectId, content: UploadFile) -> ObjectId:
        bucket = neosqlite.GridFSBucket(self.client.db)  # pyright: ignore[reportUnknownMemberType]
        chunk_size_bytes = 1 * 1024 * 1024
        file_primary = str(uuid4()) + str(hash(document_id))

        file_id = assemble_id_url(
            type=UrlType.Media,
            base_url=self.settings.domain.hostname,
            primary=file_primary,
        )

        file_content_hash = hashlib.sha256()
        metadata = {"contentType": "application/octet-stream", "filename": file_primary, "documentId": document_id}
        with bucket.open_upload_stream_with_id(
            file_id=hash(file_id),
            filename=file_primary,
            metadata=metadata,
        ) as grid_in:
            while True:
                chunk = await content.read(size=chunk_size_bytes)
                if not chunk:
                    break
                _ = grid_in.write(chunk)
                file_content_hash.update(chunk)

        # set the metadata of the file with the hex digest
        hash_digest = file_content_hash.hexdigest()
        metadata["sha256"] = hash_digest

        files_collection = await self.get_nosql_collection(self.GRID_FS_COLLECTION)
        _ = await files_collection.insert_one({"_id": file_id, "metadata": metadata})

        return file_id

    @override
    async def load(self, file_id: ObjectId) -> AsyncIterator[bytes]:
        bucket = neosqlite.GridFSBucket(self.client.db)  # pyright: ignore[reportUnknownMemberType]

        try:
            file = bucket.open_download_stream(file_id=hash(file_id))
        except NoFile:
            raise NotFoundError(f"Could not find file with id {file_id}", user_facing_message="File not found")
        return AsyncFileIterator(file.read())

    @override
    async def metadata(self, file_id: ObjectId) -> MediaMetadata:
        files_collection = await self.get_nosql_collection(self.GRID_FS_COLLECTION)
        file_data = await files_collection.find_one({"_id": file_id})
        if not file_data or "metadata" not in file_data:
            raise NotFoundError(f"Could not find metadata of file with id {file_id}", user_facing_message="File metadata not found")
        metadata = cast(dict[str, str], file_data["metadata"])

        filename = metadata.get("filename", file_id)
        content_type: str = "application/json"
        hash_sha256: str | None = None
        if metadata and "contentType" in metadata:
            content_type = metadata.get("contentType", "")
        if metadata and "sha256" in metadata:
            hash_sha256 = metadata.get("sha256", "")
        if metadata and "documentId" in metadata:
            document_id = metadata.get("documentId", "")
        else:
            raise StorageError(f"documentId not in metadata of file {file_id}")
        return MediaMetadata(content_type=content_type, filename=filename, hash_sha256=hash_sha256, document_id=document_id)

    @override
    async def flush(self) -> None:
        """Flush storage to file, if possible."""
        pass
