#
# n23 - data acquisition and processing framework
#
# Copyright (C) 2013-2025 by Artur Wroblewski <wrobell@riseup.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

"""
API definitions for N23 framework data storage.

The API strives to be SQL/RDBMS independent.

Requirements for a storage backend

- if storage is an external service, like database server, the storage
  write method should block or enqueue data
- even if database server is down forever, a storage implementation must
  not cause an application to run out of memory
"""

import abc
from collections.abc import Awaitable
import types
import typing as tp
from contextlib import AbstractAsyncContextManager
from urllib.parse import urlparse

from ..error import ApplicationError
from ..scheduler import TaskResult

DEFAULT_MAX_LEN = 300

T_co = tp.TypeVar('T_co', covariant=True)

# a collection of fields of a data record, i.e. table column names
Fields: tp.TypeAlias = tp.Sequence[str]

class Storage(AbstractAsyncContextManager[Awaitable[tp.Any] | None], tp.Protocol):
    """
    N23 framework storage protocol.
    """
    @abc.abstractmethod
    def add_entity(
            self,
            name: str,
            fields: Fields,
            *,
            batch: bool=False,
            max_len: int=DEFAULT_MAX_LEN,
    ) -> None:
        """
        Add and configure storage entity.

        An example of storage entity is a database table.
        """

    @abc.abstractmethod
    async def write(self, result: TaskResult[T_co]) -> None:
        """
        Write result of N23 task into storage.

        Storage needs to be configured via :py:meth:`Database.add_entity`
        method.

        :param result: Task result generated by N23 scheduler.
        """

    @abc.abstractmethod
    async def flush(self) -> None:
        """
        Flush data buffered for write.
        """

    async def __aenter__(self) -> None:
        return None

    async def __aexit__(
        self,
        ex_type: type[BaseException] | None,
        ex: BaseException | None,
        traceback: types.TracebackType | None,
    ) -> None:
        return None

class StorageAbyss(Storage):
    """
    Storage entity, which does nothing.
    """
    def add_entity(
            self,
            name: str,
            fields: Fields,
            *,
            batch: bool=False,
            max_len: int=DEFAULT_MAX_LEN,
    ) -> None:
        pass

    async def write(self, result: TaskResult[T_co]) -> None:
        pass

    async def flush(self) -> None:
        pass


def storage_from_uri(uri: str) -> Storage:
    """
    Create storage object or connection for N23 framework.
    """
    v = urlparse(uri)
    f = STORAGE.get(v.scheme)
    if f is None:
        raise ApplicationError('Unknown storage: {}'.format(v.scheme))

    return f(uri)

def _pg_from_uri(uri: str) -> Storage:
    """
    Create PostgreSQL or TimescaleDB storage object using URI.
    """
    # pylint: disable=import-outside-toplevel
    import n23.storage.pg
    return n23.storage.pg.Database(uri)

STORAGE = {
    'postgresql': _pg_from_uri,
}

# vim: sw=4:et:ai
