# SPDX-FileCopyrightText: 2023 Genome Research Ltd.
#
# SPDX-License-Identifier: MIT

from __future__ import annotations

import typing
from abc import ABC, abstractmethod
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any, Protocol

if typing.TYPE_CHECKING:
    from .operator import Relational
    from .datasource import DataSource


class _AnyKeyProtocol(Protocol):
    """
    Type hints that every key is valid for an attribute.

    Useful for `DataObject`.
    """

    def __getattr__(self, name: str) -> Any | None:
        ...


DataDict = dict[str, Any]


class DataObject(_AnyKeyProtocol, ABC):
    """
    The ABC for the unit of data on which a DataSource instance
    operates - representing the lingua franca of DataSource instances,
    and declaring all abstract properties that are needed.
    """

    @property
    @abstractmethod
    def type(self) -> str:  # noqa
        """
        The type of this object (e.g. species/specimens/samples).
        """

    @property
    @abstractmethod
    def id(self) -> str | None:  # noqa
        """
        A unique ID by which to identify this object within
        its type.
        """

    @property
    @abstractmethod
    def provenance(self) -> str | None:
        """
        The original source of this object (used in Elastic where the same data can be stored
        in multiple places. In this case, source priority is used to know which to return)
        """

    @property
    @abstractmethod
    def attributes(self) -> dict[str, Any]:
        """
        A dictionary of key:attribute pairs, where an attribute
        is any entry on the object that is none of an ID, type,
        or relationship.
        """

    @property
    @abstractmethod
    def to_one_relationships(self) -> dict[str, DataObject | None]:
        """
        A dictionary of relationships, where this object refers to
        precisely one other.
        """

    @property
    @abstractmethod
    def to_many_relationships(self) -> dict[str, Iterable[DataObject]]:
        """
        A dictionary of relationships, where many objects refer to
        precisely this object.
        """

    @property
    @abstractmethod
    def _host(self) -> DataSource | Relational:
        """
        The DataSource instance that manages DataObject instances of this type
        """

    @property
    @abstractmethod
    def _to_one_objects(self) -> dict[str, DataObject | None]:
        """
        The name: attribute mapping for to-one `DataObject`s set on this
        instance.

        Can used to inspect which relations are set on the object without
        triggering auto-fetching of to-one related objects from the `_host`
        `DataSource`.

        Most users should use `to_one_relationships` instead.
        """

    @property
    @abstractmethod
    def _to_many_objects(self) -> dict[str, Iterable[DataObject]]:
        """
        The name: attribute mapping for to-many `DataObject`s set on this
        instance.

        Can used to inspect which relations are set on the object without
        triggering auto-fetching of to-many related objects from the `_host`
        `DataSource`.

        Most users should use `to_many_relationships` instead.
        """

    def get_field_by_name(self, field_name: str) -> Any:
        """
        Get a field by name, or return `None` if the field does not exist.
        """
        # Split by dots to allow for nested fields
        field_names = field_name.split('.')
        current_obj = self
        for name in field_names:
            current_obj = getattr(current_obj, name)
            if current_obj is None:
                return None
        return current_obj


@dataclass(frozen=True)
class ErrorObject:
    """
    Returned by write `Operator` methods, in place of a valid `DataObject`,
    if there was an error with "writing" an individual input `DataObject` in
    the `Iterable`.
    """

    details: dict[str, Any]
    """Additional detail on this error"""
    object_type: str
    """The `type` of the object that this write concerns"""

    error_id: str | None = None
    """An optional ID for this error"""
    object_id: str | None = None
    """The `id` of the object that this write concerns, if provided"""
    object_: DataObject | None = None
    """The object that this write concerns, if provided"""
    http_code: int | None = None
    """An optional HTTP Status Code for this error"""


WriteObject = DataObject | ErrorObject
