from collections.abc import AsyncGenerator, AsyncIterator, Iterator
from contextlib import asynccontextmanager
from typing import Any, cast, override

from neosqlite import Collection, Cursor
from pymongo.results import InsertManyResult, InsertOneResult

from phederation.utils.base import ObjectId


class AsyncFileIterator(AsyncIterator[bytes]):
    """Patch class to get async/await working in neosqlite.
    This async iterator immediately returns the entire bytes array in one block, i.e., only has one iterate.
    """

    def __init__(self, data: bytes):
        self.data: bytes = data
        self.done: bool = False

    @override
    async def __anext__(self):
        if self.done:
            raise StopAsyncIteration
        self.done = True
        return self.data


class AsyncCursorNoSqlite:
    """An async wrapper for the neosqlite cursor (which is not async)."""

    def __init__(self, cursor: Cursor):
        self.cursor: Cursor = cursor
        self.iterator: None | Iterator[dict[str, Any]] = None

    def __aiter__(self):
        return self

    async def __anext__(self):
        if not self.iterator:
            self.iterator = self.cursor.__iter__()
        try:
            return self.iterator.__next__()
        except StopIteration:
            raise StopAsyncIteration

    def skip(self, skip: int):
        self.cursor = self.cursor.skip(skip)
        return self

    def limit(self, limit: int):
        self.cursor = self.cursor.limit(limit)
        return self


class AsyncNoSQLiteCollection:
    """An async wrapper for the neosqlite database (which is not async).

    This allows to use a simple sqlite database file and access it with pymongo, including async/await interfaces.
    The operators are asynchronous (but blocking), so it cannot be used efficiently.
    The main reason to implement this is so that the NoSqlBackend code can be used without setting up a mongodb server, e.g., when running tests in a CI pipeline.
    """

    def __init__(self, name: str, collection: Collection) -> None:
        self.name: str = name
        self.collection: Collection = collection

    async def insert_many(self, data: list[dict[str, Any]]):
        result = self.collection.insert_many(data)
        insert_many_result = InsertManyResult(inserted_ids=result.inserted_ids, acknowledged=True)
        return insert_many_result

    async def insert_one(self, data: dict[str, Any]):
        result = self.collection.insert_one(data)
        inserted_id = str(cast(str, result.inserted_id))
        insert_one_result = InsertOneResult(inserted_id=inserted_id, acknowledged=True)
        return insert_one_result

    async def update_one(self, filter: dict[str, ObjectId], update: dict[str, Any], upsert: bool = False):
        result = self.collection.update_one(filter=filter, update=update, upsert=upsert)
        return result

    async def find_one(self, filter: dict[str, Any]) -> dict[str, Any] | None:
        result = self.collection.find_one(filter=filter)
        return result

    @asynccontextmanager
    async def find(self, query: dict[str, list[dict[str, ObjectId] | dict[str, list[dict[str, str]]]]]) -> AsyncGenerator[AsyncCursorNoSqlite]:
        cursor = self.collection.find(filter=query)
        yield AsyncCursorNoSqlite(cursor)

    async def count_documents(self, filter: dict[str, Any]):
        return self.collection.count_documents(filter)

    async def delete_one(self, filter: dict[str, str]):
        result = self.collection.delete_one(filter=filter)
        return result.deleted_count

    async def delete_many(self, filter: dict[str, list[dict[str, str]]]):
        result = self.collection.delete_many(filter=filter)
        return result.deleted_count
