import logging
from functools import wraps

import hikaru
from hikaru.model import *

from ...core.model.k8s_operation_type import K8sOperationType
from ..base_handler import handle_event
from ...core.model.cloud_event import *
from ...core.model.playbook_hash import playbook_hash
from ...core.model.runner_config import *
from ...core.active_playbooks import register_playbook, activate_playbook
from pydantic import BaseModel

from ...core.model.events import EventType
from ...core.triggers import TriggerParams
from .autogenerated.events import KIND_TO_EVENT_CLASS, KubernetesAnyEvent
from .autogenerated.models import get_api_version
from .base_event import K8sBaseEvent
from .model_not_found_exception import ModelNotFoundException


class IncomingK8sEventPayload(BaseModel):
    """
    The format of incoming CloudEvent payloads containing kubernetes events. This is mostly used for deserialization.
    """

    operation: str
    kind: str
    apiVersion: str = ""
    clusterUid: str
    description: str
    obj: Dict[Any, Any]
    oldObj: Optional[Dict[Any, Any]]


def parse_kubernetes_objs(k8s_payload: IncomingK8sEventPayload):
    model_class = get_api_version(k8s_payload.apiVersion).get(k8s_payload.kind)
    if model_class is None:
        msg = f"classes for kind {k8s_payload.kind} cannot be found. skipping. description {k8s_payload.description}"
        logging.error(msg)
        raise ModelNotFoundException(msg)

    obj = hikaru.from_dict(k8s_payload.obj, cls=model_class)
    old_obj = None
    if k8s_payload.oldObj is not None:
        old_obj = hikaru.from_dict(k8s_payload.oldObj, cls=model_class)
    return (obj, old_obj)


def parse_incoming_kubernetes_event(
    k8s_payload: IncomingK8sEventPayload,
) -> Optional[K8sBaseEvent]:
    (obj, old_obj) = parse_kubernetes_objs(k8s_payload)
    event_class = KIND_TO_EVENT_CLASS.get(k8s_payload.kind)
    if event_class is None:
        logging.error(
            f"classes for kind {k8s_payload.kind} cannot be found. skipping. description {k8s_payload.description}"
        )
        return None
    operation_type = K8sOperationType(k8s_payload.operation)
    return event_class(
        operation=operation_type,
        description=k8s_payload.description.replace("\n", ""),
        obj=obj,
        old_obj=old_obj,
    )


def parse_incoming_kubernetes_any_event(
    k8s_payload: IncomingK8sEventPayload,
) -> Optional[K8sBaseEvent]:
    (obj, old_obj) = parse_kubernetes_objs(k8s_payload)
    operation_type = K8sOperationType(k8s_payload.operation)
    return KubernetesAnyEvent(
        operation=operation_type,
        description=k8s_payload.description.replace("\n", ""),
        obj=obj,
        old_obj=old_obj,
    )


def register_k8s_playbook(
    func,
    kind,
    operation: Optional[K8sOperationType],
    name_prefix="",
    namespace_prefix="",
):
    register_playbook(
        func,
        deploy_on_k8s_topology,
        TriggerParams(
            name_prefix=name_prefix,
            namespace_prefix=namespace_prefix,
            kind=kind,
            operation=operation,
        ),
    )
    return func


def register_k8s_any_playbook(
    func, operation: Optional[K8sOperationType], name_prefix="", namespace_prefix=""
):
    register_playbook(
        func,
        deploy_on_any_k8s_topology,
        TriggerParams(
            name_prefix=name_prefix,
            namespace_prefix=namespace_prefix,
            operation=operation,
        ),
    )
    return func


def prefix_match(prefix, field_value) -> bool:
    if prefix == "":
        return True
    if (
        field_value is None
    ):  # we have a prefix requirement, but field doesn't exist. no match
        return False
    return field_value.startswith(prefix)


def k8s_topology_handler(
    cloud_event: CloudEvent,
    event_parser,
    func,
    trigger_params: TriggerParams,
    named_sinks: List[str],
    action_params=None,
):
    logging.debug(f"checking if we should run {func} on k8s event")
    k8s_payload = IncomingK8sEventPayload(**cloud_event.data)

    if not k8s_topology_event_matched(k8s_payload, trigger_params):
        return "SKIP"

    try:
        concrete_event = event_parser(k8s_payload)
    except ModelNotFoundException:
        return "SKIP"

    return handle_event(func, concrete_event, action_params, "kubernetes", named_sinks)


def deploy_on_k8s_topology(
    func, trigger_params: TriggerParams, named_sinks: List[str], action_params=None
):
    @wraps(func)
    def wrapper(cloud_event: CloudEvent):
        return k8s_topology_handler(
            cloud_event,
            parse_incoming_kubernetes_event,
            func,
            trigger_params,
            named_sinks,
            action_params,
        )

    playbook_id = playbook_hash(func, trigger_params, action_params)
    activate_playbook(EventType.KUBERNETES_TOPOLOGY_CHANGE, wrapper, func, playbook_id)
    return wrapper


def deploy_on_any_k8s_topology(
    func, trigger_params: TriggerParams, named_sinks: List[str], action_params=None
):
    @wraps(func)
    def wrapper(cloud_event: CloudEvent):
        return k8s_topology_handler(
            cloud_event,
            parse_incoming_kubernetes_any_event,
            func,
            trigger_params,
            named_sinks,
            action_params,
        )

    playbook_id = playbook_hash(func, trigger_params, action_params)
    activate_playbook(EventType.KUBERNETES_TOPOLOGY_CHANGE, wrapper, func, playbook_id)
    return wrapper


def k8s_topology_event_matched(
    event: IncomingK8sEventPayload, trigger_params: TriggerParams
):
    if (
        trigger_params.operation is not None
        and trigger_params.operation.value != event.operation
    ):
        logging.debug(f"operation {event.operation} != {trigger_params.operation}")
        return False

    if trigger_params.kind is not None and trigger_params.kind != event.kind:
        logging.debug(f"kind {event.kind} != {trigger_params.kind}")
        return False

    metadata = event.obj.get("metadata", {})
    if trigger_params.name_prefix != "":
        obj_name = metadata.get("name", "")
        if not obj_name.startswith(trigger_params.name_prefix):
            logging.debug("name doesn't match")
            return False

    if trigger_params.namespace_prefix != "":
        obj_namespace = metadata.get("namespace", "")
        if not obj_namespace.startswith(trigger_params.namespace_prefix):
            logging.debug("namespace doesn't match")
            return False

    return True
