# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Redis backend for prometheus values."""

import datetime as dt
import os
from collections.abc import Callable, Sequence
from typing import Any, Literal, Protocol, cast, override

from prometheus_client.mmap_dict import mmap_key
from prometheus_client.samples import Exemplar

from debusine.server.prometheus_client.redis import (
    _keep_key_from_expiring,
    _key_expiry,
    redis_client,
)

MULTIPROCESS_MODE_T = Literal[
    "all",
    "liveall",
    "min",
    "livemin",
    "max",
    "livemax",
    "sum",
    "livesum",
    "mostrecent",
    "livemostrecent",
    "",
]


class Value(Protocol):
    """Prometheus Client Metric implementation."""

    _multiprocess: bool

    def __init__(
        self,
        typ: str,
        metric_name: str,
        name: str,
        labelnames: Sequence[str],
        labelvalues: Sequence[str],
        help_text: str,
        multiprocess_mode: MULTIPROCESS_MODE_T = "",
        **kwargs: Any,
    ) -> None:
        """Initialize a metric."""

    def inc(self, amount: float) -> None:
        """Increment the metric by amount."""

    def set(self, value: float, timestamp: float | None = None) -> None:
        """Set the metric to value."""

    def get(self) -> float:
        """Get the current metric value."""

    def set_exemplar(self, exemplar: Exemplar) -> None:
        """Set an exemplar value."""
        exemplar  # For vulture

    def get_exemplar(self) -> Exemplar | None:
        """Get any set exemplar value."""


def RedisValue(  # noqa: C901
    process_identifier: Callable[[], str | int] = os.getpid,
) -> type[Value]:
    """
    Value implementation that stores data in a redis/valkey database.

    Key scheme:
    * value:typ:MMAP_KEY

    When a live multiprocess_mode is used, we set the key to expire after
    PROMETHEUS_REDIS_REFRESH_TTL seconds. We launch a daemon thread that
    extends the expiry of all our process' keys every
    PROMETHEUS_REDIS_REFRESH_FREQUENCY.
    """

    class RedisValueImpl(Value):
        _multiprocess: bool = True

        _typ: str
        _metric_name: str
        _name: str
        _labelnames: list[str]
        _labelvalues: list[str]
        _help_text: str
        _multiprocess_mode: MULTIPROCESS_MODE_T
        _expiry: dt.timedelta | None

        _key: str

        def __init__(
            self,
            typ: str,
            metric_name: str,
            name: str,
            labelnames: Sequence[str],
            labelvalues: Sequence[str],
            help_text: str,
            multiprocess_mode: MULTIPROCESS_MODE_T = "",
            **kwargs: Any,
        ) -> None:
            self._typ = typ
            self._metric_name = metric_name
            self._name = name
            self._labelnames = list(labelnames)
            self._labelvalues = list(labelvalues)
            self._help_text = help_text
            self._multiprocess_mode = multiprocess_mode
            self._expiry = None
            if multiprocess_mode:
                if multiprocess_mode in ("mostrecent", "livemostrecent"):
                    raise NotImplementedError(
                        "The 'mostrecent' modes are not supported in RedisValue"
                    )
                assert typ in ("gauge", "gaugehistogram")
                self._labelnames.append("pid")
                self._labelvalues.append("")
                if multiprocess_mode.startswith("live"):
                    self._expiry = _key_expiry()
            self._update_key(True)
            redis_client().set(self._key, 0.0, ex=self._expiry, nx=True)

        def _update_key(self, update: bool = False) -> None:
            if self._multiprocess_mode:
                assert self._labelnames[-1] == "pid"
                new_id = str(process_identifier())
                if new_id != self._labelvalues[-1]:
                    self._labelvalues[-1] = new_id
                    update = True

            if update:
                key = mmap_key(
                    self._metric_name,
                    self._name,
                    self._labelnames,
                    self._labelvalues,
                    self._help_text,
                )
                self._key = f"value:{self._typ}:{self._multiprocess_mode}:{key}"

            if self._expiry and update:
                _keep_key_from_expiring(self._labelvalues[-1], self._key)

        @override
        def inc(self, amount: float) -> None:
            self._update_key()
            client = redis_client()
            client.incrbyfloat(self._key, amount)
            if self._expiry:
                client.expire(self._key, self._expiry)

        @override
        def set(self, value: float, timestamp: float | None = None) -> None:
            timestamp  # for vulture
            self._update_key()
            # TODO: Implement timestamps
            redis_client().set(self._key, value, ex=self._expiry)

        @override
        def get(self) -> float:
            self._update_key()
            # Type bug: https://github.com/redis/redis-py/issues/2399
            value = cast(float | None, redis_client().get(self._key))
            if value is None:
                return 0.0
            return float(value)

        @override
        def set_exemplar(self, exemplar: Exemplar) -> None:
            # TODO: Implement exemplars for redis.
            raise NotImplementedError("Exemplars are not implemented for Redis")

        @override
        def get_exemplar(self) -> Exemplar | None:
            # TODO: Implement exemplars for redis.
            return None

    return RedisValueImpl
