from contextlib import contextmanager
from functools import wraps

import statsd

from bakit import settings

_statsd_client = None


def get_statsd_client():
    global _statsd_client
    if _statsd_client is None:
        _statsd_client = statsd.StatsClient(
            settings.STATSD_HOST,
            settings.STATSD_PORT,
            settings.STATSD_PREFIX,
        )
    return _statsd_client


def multinetworktimerd(key):
    """
    Decorator function to set up a timer around a function call.
    This is a function only decorator!

    Example:
    >>> import time
    >>> @metrics.multinetworktimerd('eventprocessor.sync')
    >>> def sync(self):
    ...     time.sleep(1)

    When running:
    `EventProcessor(self.network, self.to_block).sync()
    it will generate the following key:
    - base.eventprocessor.sync
    - ethereum.eventprocessor.sync
    """

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if not key:
                raise Exception("Using an empty key name")

            network = None
            # Access the class instance (`self`) if the method is an instance method.
            cls = args[0] if args else None
            if cls:
                network = getattr(cls, "network", None)

            if not network:
                raise AttributeError(
                    f"The decorated method '{func.__name__}' must have a 'network' "
                    "attribute. Ensure the class or instance has a 'network' property "
                    "defined."
                )

            with timer(f"{network}.{key}"):
                return func(*args, **kwargs)

        return wrapper

    return decorator


def timerd(key):
    """
    Decorator function to set up a timer around a function call.
    This is a function only decorator!

    Example:
    >>> import time
    >>> @metrics.timerd('time_sleep_key')
    >>> def timed_function():
    ...     time.sleep(1)
    """

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if not key:
                raise Exception("Using an empty key name")
            with timer(key):
                return func(*args, **kwargs)

        return wrapper

    return decorator


@contextmanager
def timer(key):
    """Metrics wrapper for Statsd Timer Object

    >>> import time
    >>> with metrics.timer('unique_key'):
    ...     time.sleep(1)
    """
    statsd_timer = get_statsd_client().timer(str(key))
    statsd_timer.start()
    try:
        yield
    finally:
        statsd_timer.stop()


def raw_timer(key, value):
    """Send a timing directly to Graphite, without need to call start() and stop().

    :keyword value: The time in seconds, it must be an int or a float

    >>> # Got a timing from frontend!
    >>> metrics.raw_timer('unique_key', 31.3)
    """

    # Validating "value" to be an int or a float
    if not isinstance(value, int | float):
        return None

    return get_statsd_client().timing(str(key), value)


def increment(key, delta=1, subname=None):
    """Increment the counter identified with `key` and `subname` with `delta`

    >>> # After a user logs in....
    >>> metrics.increment('auth.successful_login', 1)

        :keyword delta: The delta to add to the counter, default is 1
        :keyword subname: The subname to report the data to (appended to the
            client name). Like "hits", or "sales".
    """
    name = f"counters.{key}"
    if subname:
        name += f".{subname}"

    return get_statsd_client().incr(name, delta)


def decrement(key, delta=1, subname=None):
    """Decrement the counter identified with `key` and `subname` with `delta`

    >>> # Users that log out...
    >>> metrics.decrement('auth.connected_users', 1)

        :keyword delta: The delta to substract from the counter, default is 1
        :keyword subname: The subname to report the data to (appended to the
            client name)
    """

    name = f"counters.{key}"
    if subname:
        name += f".{subname}"

    return get_statsd_client().decr(name, delta)


def gauge(key, value=1, subname=None):
    """Set the value of the gauge identified with `key` and `subname` with `value`

    :keyword value: The value to set the gauge at, default is 1
    :keyword subname: The subname to report the data to (appended to the
        client name)
    """

    name = key
    if subname:
        name += f".{subname}"

    # We never use the relative changes behaviour so attempt to always make it do the
    # set value behaviour instead.
    if value < 0:
        get_statsd_client().gauge(name, 0)
    return get_statsd_client().gauge(name, value)


def function_long_name(func, extra=None):
    if extra:
        return ".".join([func.__module__, func.__name__, extra])
    else:
        return ".".join([func.__module__, func.__name__])


def auto_named_statsd_timer(function_to_decorate):
    call_name = function_long_name(function_to_decorate, "call")

    @wraps(function_to_decorate)
    def incr_and_call(*args, **kwargs):
        get_statsd_client().incr(call_name)
        return function_to_decorate(*args, **kwargs)

    timer_name = function_long_name(function_to_decorate, "time")
    named_decorator = get_statsd_client().timer(timer_name)

    return named_decorator(incr_and_call)


@contextmanager
def view_metrics_context(endpoint_name=None, instance=None):
    """
    Context manager for view methods to measure hit count and response time.

    Args:
        endpoint_name: Optional custom name for the endpoint. If not provided,
                       uses the instance's class name and current context.
        instance: The instance (self) to get class name from.

    Example:
        async def get(self, request):
            with view_metrics_context(instance=self):
                # Your code here
                return Response({"data": result})

        with view_metrics_context("custom_endpoint"):
            # Your code here
            pass
    """
    if endpoint_name:
        metric_base = f"views.{endpoint_name}"
    else:
        if instance:
            cls_name = instance.__class__.__name__
            metric_base = f"views.{cls_name}.context"
        else:
            metric_base = "views.unknown.context"

    get_statsd_client().incr(f"{metric_base}.hits")

    with get_statsd_client().timer(f"{metric_base}.response_time"):
        yield


def view_metrics(endpoint_name=None):
    """
    Decorator for view methods to measure hit count and response time.

    Args:
        endpoint_name: Optional custom name for the endpoint. If not provided,
                       uses the class name and method name.

    Example:
        @view_metrics()
        async def get(self, request):
            ...

        @view_metrics("custom_endpoint")
        async def post(self, request):
            ...
    """

    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            if endpoint_name:
                metric_base = f"views.{endpoint_name}"
            else:
                cls_name = args[0].__class__.__name__ if args else "unknown"
                method_name = func.__name__
                metric_base = f"views.{cls_name}.{method_name}"

            get_statsd_client().incr(f"{metric_base}.hits")

            # Time the function execution
            with get_statsd_client().timer(f"{metric_base}.response_time"):
                return await func(*args, **kwargs)

        return wrapper

    return decorator
