"""
Base storage interface.
"""

from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator, AsyncIterator
from logging import Logger
import os
from typing import Any

from dotenv import dotenv_values
from fastapi.datastructures import UploadFile
from pydantic import SecretStr

from phederation.models import CRUD
from phederation.models.activities import APActivity, APFollow
from phederation.models.actors import APAccount, APActor
from phederation.models.collections import APOrderedCollectionPage
from phederation.models.keys import APPrivateKey
from phederation.models.media import MediaMetadata
from phederation.models.objects import APCollectionItem, APObject
from phederation.models.proofs import DataIntegrityProof
from phederation.utils import NodeInfo, ObjectId
from phederation.utils.base import AccessType, ActivityPubBaseWithId
from phederation.utils.exceptions import StorageError, catch_exceptions
from phederation.utils.settings import PhedSettings, StorageProviderType


def fill_database_url_from_env(url: SecretStr, dotenv_path: str | None = None) -> SecretStr:
    """Fill in .env variables into the url, if necessary.

    Args:
        url (str): The database string with potentially unfilled variables (POSTGRES_PASSWORD) etc.
        dotenv_path (str|None): the path to the .env file to load from. Default: None (current directory).
    """
    url_secret = url.get_secret_value()
    settings = dotenv_values(dotenv_path=dotenv_path)

    if "mongodb" in url_secret:
        keys = [
            "MONGO_PHED_USER",
            "MONGO_PHED_PASSWORD",
            "MONGO_PHED_DEV_USER",
            "MONGO_PHED_DEV_PASSWORD",
            "MONGO_PHED_DB",
            "MONGO_PHED_HOST",
            "MONGO_PHED_PORT",
        ]
        if keys[0] in os.environ:  # if the key is already in the environment, do not use dotenv
            settings = {key: os.environ[key] for key in keys if key in os.environ}
        for key in keys:
            if key not in settings:
                raise EnvironmentError(f"Key {key} not in .env, but is required")
            value = settings.get(key)
            if value:
                url_secret = url_secret.replace(f"${{{key}}}", value)
    if "temporalio" in url_secret:
        keys = [
            "TEMPORAL_HOST",
            "TEMPORAL_PORT",
        ]
        if keys[0] in os.environ:  # if the key is already in the environment, do not use dotenv
            settings = {key: os.environ[key] for key in keys if key in os.environ}
        for key in keys:
            if key not in settings:
                raise EnvironmentError(f"Key {key} not in .env, but is required")
            value = settings.get(key)
            if value:
                url_secret = url_secret.replace(f"${{{key}}}", value)
            url_secret = url_secret.replace("temporalio://", "")
    return SecretStr(url_secret)


class StorageBackend(ABC):
    """Abstract storage backend."""

    # Register storage backends
    _providers: dict[StorageProviderType, type["StorageBackend"]] = {}

    actor: CRUD[APActor]
    account: CRUD[APAccount]
    object: CRUD[APObject]
    collection: CRUD[APOrderedCollectionPage]
    collection_items: CRUD[APCollectionItem]
    activity: CRUD[APActivity]
    follow: CRUD[APFollow]
    key: CRUD[APPrivateKey]
    proof: CRUD[DataIntegrityProof]
    node_info: CRUD[NodeInfo]

    database_url: SecretStr
    settings: PhedSettings
    logger: Logger

    def __init__(self, database_url: SecretStr, settings: PhedSettings) -> None:
        super().__init__()
        self.database_url = database_url
        self.settings = settings

    @classmethod
    def register_provider(cls, provider_name: StorageProviderType, provider_class: type["StorageBackend"]):
        """Register a storage provider."""
        cls._providers[provider_name] = provider_class

    @classmethod
    async def create(cls, settings: PhedSettings) -> "StorageBackend":
        """Create storage backend instance."""
        provider = StorageProviderType(settings.storage.provider)
        database_url = SecretStr(settings.storage.database.url)

        if provider not in cls._providers:
            raise StorageError(f"Unsupported storage provider: {provider}")

        provider_class = cls._providers[provider]
        return provider_class(database_url=database_url, settings=settings)

    @catch_exceptions(StorageError, "Failed to initialize storage")
    @abstractmethod
    async def initialize(self) -> None:
        """Initialize storage."""
        pass

    @catch_exceptions(StorageError, "Failed to reconnect to database")
    @abstractmethod
    async def reconnect(self) -> None:
        """Reconnect to the database. Useful if the async event loop changed, e.g. in delivery workers in different processes."""
        pass

    @abstractmethod
    def table[T: ActivityPubBaseWithId](self, model_class: type[T], name: str) -> CRUD[T]:
        """Get the table associated with the ActivityPubBaseWithId type."""

    @abstractmethod
    async def save(self,  document_id: ObjectId, content: UploadFile) -> ObjectId:
        """Save byte array data. Useful for media storage."""

    @abstractmethod
    async def load(self, file_id: ObjectId) -> AsyncIterator[bytes]:
        """Load byte array data. Useful for media storage."""

    @abstractmethod
    async def metadata(self, file_id: ObjectId) -> MediaMetadata:
        """Load metadata of a file, e.g. the file hash or content type. Useful for media storage."""

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

    @abstractmethod
    async def clear(self):
        """Delete the database. Useful for testing."""

    @abstractmethod
    async def close(self) -> None:
        """Close storage connection."""
        pass

    @abstractmethod
    async def is_following(self, follower: ObjectId, following: ObjectId, check_accepted: bool = False) -> bool:
        pass

    @abstractmethod
    async def paginate(self, collection_id: ObjectId, access: AccessType = AccessType.PUBLIC) -> AsyncGenerator[list[APCollectionItem], Any]:
        yield []

    @abstractmethod
    async def get_collection_page_items(
        self, collection_id: ObjectId, page: int = 1, access: AccessType = AccessType.PUBLIC
    ) -> list[APCollectionItem] | None:
        pass
