"""
All the structures coming from/to the Kubernetes API.

The usage of these classes is spread over the codebase, so they are extracted
into a separate module of such type definitions.

For strict type-checking, they are detailed to the per-field level
(e.g. ``TypedDict`` instead of just ``Mapping[Any, Any]``) --
as used by the framework. The operators can use arbitrary fields at runtime,
which are not declared in the type definitions at type-checking time.

In case the operators are also type-checked, type casting can be used
(without ``cast()``, this code fails at type-checking, though works at runtime):

.. code-block:: python

    from typing import cast
    import kopf

    class MyMeta(kopf.Meta):
        unknownField: str

    @kopf.on.create('kopfexamples')
    def create_fn(*args, meta: kopf.Meta, **kwargs):
        meta = cast(MyMeta, meta)
        print(meta['unknownField'])

.. note::

    There is a strict separation of objects coming from/to the Kubernetes API
    and from (but not to) the users:

    The Kubernetes-originated objects are dicts or dict-like custom classes.
    The framework internally expects them to be such. Arbitrary 3rd-party
    classes are not supported and are not delivered to the handlers.

    The user-originated objects can be either one of the Kubernetes-originated
    framework-supported types (dicts/dict-like), or a 3rd-party class,
    such as from ``pykube-ng``, ``kubernetes`` client, etc --- as long as it is
    supported by the framework's object-processing functions.

    In the future, extra classes can be added for the user-originated objects
    and object-processing functions. The internal dicts will remain the same.
"""

from collections.abc import Mapping
from typing import Any, Literal, TypeAlias, TypedDict, cast

from kopf._cogs.structs import dicts, references

# Make sure every kwarg has a corresponding same-named type in the root package.
# Usually, they are "live views" into dicts, so they are not dicts themselves.
Labels: TypeAlias = Mapping[str, str]
Annotations: TypeAlias = Mapping[str, str]

#
# Everything marked "raw" is a plain unwrapped unprocessed data as JSON-decoded
# from Kubernetes API, usually as retrieved in watching or fetching API calls.
# "Input" is a parsed JSON as is, while "event" is an "input" without "errors".
# All non-used payload falls into `Any`, and is not type-checked.
#

# ``None`` is used for the listing, when the pseudo-watch-stream is simulated.
RawInputType = Literal[None, 'ADDED', 'MODIFIED', 'DELETED', 'BOOKMARK', 'ERROR']
RawEventType = Literal[None, 'ADDED', 'MODIFIED', 'DELETED', 'BOOKMARK']


class RawMeta(TypedDict, total=False):
    uid: str
    name: str
    namespace: str
    labels: dict[str, str]
    annotations: dict[str, str]
    finalizers: list[str]
    resourceVersion: str
    deletionTimestamp: str
    creationTimestamp: str
    selfLink: str


class RawBody(TypedDict, total=False):
    apiVersion: str
    kind: str
    metadata: RawMeta
    spec: dict[str, Any]
    status: dict[str, Any]


# A special payload for type==ERROR (this is not a connection or client error).
class RawError(TypedDict, total=False):
    apiVersion: str     # usually: Literal['v1']
    kind: str           # usually: Literal['Status']
    metadata: dict[Any, Any]
    code: int
    reason: str
    status: str
    message: str


# As received from the stream before processing the errors and special cases.
class RawInput(TypedDict, total=True):
    type: RawInputType
    object: RawBody | RawError


# As passed to the framework after processing the errors and special cases.
class RawEvent(TypedDict, total=True):
    type: RawEventType
    object: RawBody


#
# Body/Meta essences only contain the fields relevant for object diff tracking.
# They are presented to the user as part of the diff's `old`/`new` fields & kwargs.
# Added for stricter type checking, to differentiate from the actual Body/Meta.
#


class MetaEssence(TypedDict, total=False):
    labels: dict[str, str]
    annotations: dict[str, str]


class BodyEssence(TypedDict, total=False):
    metadata: MetaEssence
    spec: dict[str, Any]
    status: dict[str, Any]


#
# Enhanced dict-wrappers for easier typed access to well-known typed fields,
# with live view of updates and changes in the root body (for daemon's)
# Despite they are just MappingViews with no extensions, they are separated
# for stricter typing of arguments.
#


class Meta(dicts.MappingView[str, Any]):

    def __init__(self, __src: "Body") -> None:
        super().__init__(__src, 'metadata')
        self._labels: dicts.MappingView[str, str] = dicts.MappingView(self, 'labels')
        self._annotations: dicts.MappingView[str, str] = dicts.MappingView(self, 'annotations')

    @property
    def labels(self) -> Labels:
        return self._labels

    @property
    def annotations(self) -> Annotations:
        return self._annotations

    @property
    def uid(self) -> str | None:
        return cast(str | None, self.get('uid'))

    @property
    def name(self) -> str | None:
        return cast(str | None, self.get('name'))

    @property
    def namespace(self) -> references.Namespace:
        return cast(references.Namespace, self.get('namespace'))

    @property
    def creation_timestamp(self) -> str | None:
        return cast(str | None, self.get('creationTimestamp'))

    @property
    def deletion_timestamp(self) -> str | None:
        return cast(str | None, self.get('deletionTimestamp'))


class Spec(dicts.MappingView[str, Any]):
    def __init__(self, __src: "Body") -> None:
        super().__init__(__src, 'spec')


class Status(dicts.MappingView[str, Any]):
    def __init__(self, __src: "Body") -> None:
        super().__init__(__src, 'status')


class Body(dicts.ReplaceableMappingView[str, Any]):

    def __init__(self, __src: RawBody | BodyEssence) -> None:
        super().__init__(__src)
        self._meta = Meta(self)
        self._spec = Spec(self)
        self._status = Status(self)

    @property
    def metadata(self) -> Meta:
        return self._meta

    @property
    def meta(self) -> Meta:
        return self._meta

    @property
    def spec(self) -> Spec:
        return self._spec

    @property
    def status(self) -> Status:
        return self._status


#
# Other API types, which are not body parts.
#

class ObjectReference(TypedDict, total=False):
    apiVersion: str
    kind: str
    namespace: str | None
    name: str
    uid: str


class OwnerReference(TypedDict, total=False):
    controller: bool
    blockOwnerDeletion: bool
    apiVersion: str
    kind: str
    name: str
    uid: str


def build_object_reference(
        body: Body,
) -> ObjectReference:
    """
    Construct an object reference for the events.

    Keep in mind that some fields can be absent: e.g. ``namespace``
    for cluster resources, or e.g. ``apiVersion`` for ``kind: Node``, etc.
    """
    ref = dict(
        apiVersion=body.get('apiVersion'),
        kind=body.get('kind'),
        name=body.get('metadata', {}).get('name'),
        uid=body.get('metadata', {}).get('uid'),
        namespace=body.get('metadata', {}).get('namespace'),
    )
    return cast(ObjectReference, {key: val for key, val in ref.items() if val})


def build_owner_reference(
        body: Body,
        *,
        controller: bool | None = True,
        block_owner_deletion: bool | None = True,
) -> OwnerReference:
    """
    Construct an owner reference object for the parent-children relationships.

    The structure needed to link the children objects to the current object as a parent.
    See https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/

    Keep in mind that some fields can be absent: e.g. ``namespace``
    for cluster resources, or e.g. ``apiVersion`` for ``kind: Node``, etc.
    """
    ref = dict(
        controller=controller,
        blockOwnerDeletion=block_owner_deletion,
        apiVersion=body.get('apiVersion'),
        kind=body.get('kind'),
        name=body.get('metadata', {}).get('name'),
        uid=body.get('metadata', {}).get('uid'),
    )
    return cast(OwnerReference, {key: val for key, val in ref.items() if val is not None})
