"""
Method decorator for execution latency collection
"""

import functools
import logging
from datetime import datetime
from typing import Callable, Optional, TypeVar, Union, overload

from typing_extensions import ParamSpec

from samtranslator.metrics.metrics import DummyMetricsPublisher, Metrics
from samtranslator.model import Resource

LOG = logging.getLogger(__name__)

_PT = ParamSpec("_PT")  # parameters
_RT = TypeVar("_RT")  # return value


class MetricsMethodWrapperSingleton:
    """
    Keeps the instance of Metrics object.
    This singleton will be alive until lambda receives shutdown event
    """

    _DUMMY_INSTANCE = Metrics("ServerlessTransform", DummyMetricsPublisher())
    _METRICS_INSTANCE = _DUMMY_INSTANCE

    @staticmethod
    def set_instance(metrics: Metrics) -> None:
        MetricsMethodWrapperSingleton._METRICS_INSTANCE = metrics

    @staticmethod
    def get_instance() -> Metrics:
        """
        Return the instance, if nothing is set return a dummy one
        """
        return MetricsMethodWrapperSingleton._METRICS_INSTANCE


def _get_metric_name(prefix, name, func, args):  # type: ignore[no-untyped-def]
    """
    Returns the metric name depending on the parameters

    Parameters
    ----------
    prefix : str
        A string that will always be added in the beginning of metric name.
    name : str
        The name of the metric. If None is given, it will try to read from function and argument details.
    func : Function
        The function that is decorated. This will be used as metric name if name is not provided and caller is not an
        instance of Resource object.
    args : args
        Arguments that is originally passed to the caller. This function will check if first element in this function
        is a Resource then it reads the 'resource_type' property out of it to generate the metric name.
    """
    if name:
        metric_name = name
    elif args and isinstance(args[0], Resource):
        metric_name = args[0].resource_type
    else:
        metric_name = func.__name__

    if prefix:
        return f"{prefix}-{metric_name}"

    return metric_name


def _send_cw_metric(prefix, name, execution_time_ms, func, args):  # type: ignore[no-untyped-def]
    """
    Gets metric name from 'prefix', 'name', 'func' and 'args' parameters, then calls metrics instance from its
    singleton object to record the latency.
    """
    try:
        metric_name = _get_metric_name(prefix, name, func, args)  # type: ignore[no-untyped-call]
        LOG.debug("Execution took %sms for %s", execution_time_ms, metric_name)
        MetricsMethodWrapperSingleton.get_instance().record_latency(metric_name, execution_time_ms)
    except Exception as e:
        LOG.warning("Failed to add metrics", exc_info=e)


@overload
def cw_timer(
    *, name: Optional[str] = None, prefix: Optional[str] = None
) -> Callable[[Callable[_PT, _RT]], Callable[_PT, _RT]]: ...


@overload
def cw_timer(
    _func: Callable[_PT, _RT], name: Optional[str] = None, prefix: Optional[str] = None
) -> Callable[_PT, _RT]: ...


def cw_timer(
    _func: Optional[Callable[_PT, _RT]] = None, name: Optional[str] = None, prefix: Optional[str] = None
) -> Union[Callable[_PT, _RT], Callable[[Callable[_PT, _RT]], Callable[_PT, _RT]]]:
    """
    A method decorator, that will calculate execution time of the decorated method, and store this information as a
    metric in CloudWatch by calling the metrics singleton instance.

    The metric name is calculated with parameters.
    - If 'name' is provided then it will be the metrics name.
    - If 'name' is not provided and caller method is an instance of 'Resource' object, then 'resource_type' will be used
    - If 'name' is not provided and caller is not instance of 'Resource' then it will be the name of the function

    If prefix is defined, it will be added in the beginning of what is been generated above
    """

    def cw_timer_decorator(func: Callable[_PT, _RT]) -> Callable[_PT, _RT]:
        @functools.wraps(func)
        def wrapper_cw_timer(*args, **kwargs) -> _RT:  # type: ignore[no-untyped-def]
            start_time = datetime.now()

            exec_result = func(*args, **kwargs)

            execution_time = datetime.now() - start_time
            execution_time_ms = execution_time.total_seconds() * 1000
            _send_cw_metric(prefix, name, execution_time_ms, func, args)  # type: ignore[no-untyped-call]

            return exec_result

        return wrapper_cw_timer

    return cw_timer_decorator if _func is None else cw_timer_decorator(_func)
