"""Data model for VictoriaMetrics entries."""

from __future__ import annotations

from datetime import datetime
from enum import IntEnum
from time import time_ns

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Generator

__all__ = ["DS", "Entry"]


class DS(IntEnum):
    """short for DataSource"""

    invalid = -1
    counter = 0
    gauge = 1
    derive = 2
    absolute = 3
    delta = 4  # only skipped when zero, intended to be summed up


class Entry:
    """A data entry. It consists of a series, a number of tags, a time, and
    of course a value.
    """

    _tags: dict[str, str] | None = None
    mode: DS = DS.invalid
    interval: float | None = None
    ns_time: int | None = None

    def __init__(
        self,
        value: int,
        series: str,
        tags: dict[str, str] | str | None = None,
        time: datetime | float | None = None,
        mode: DS | None = None,
    ):
        """
        Time may be a datetime instance, a number of seconds, or nanoseconds
        """
        if time is None:
            time = time_ns()
        elif isinstance(time, datetime):
            time = int(time.timestamp()) * 1000000000 + time.microsecond * 1000
        elif time < 1000000000000:  # seconds
            time = int(time * 1000000000)  # nanoseconds

        self.value = value
        self.ns_time = time
        self.series = series
        if isinstance(tags, str):
            tags = str2tags(tags)
        self.tags = tags
        if mode is not None:
            self.mode = mode

    @property
    def time(self) -> float:
        """Return time as seconds since epoch."""
        return self.ns_time / 1000000000

    @property
    def date(self) -> datetime:
        """Return time as datetime object with UTC timezone."""
        from pytz import UTC  # noqa: PLC0415 - Avoid circular import at module level

        return datetime.fromtimestamp(self.time, UTC)

    @property
    def tags(self) -> dict[str, str]:
        """Return tags dictionary."""
        return self._tags

    @tags.setter
    def tags(self, tags: dict[str, str] | str | None) -> None:
        """Set tags from dict or string."""
        if tags is None:
            tags = {}
        elif isinstance(tags, str):
            tags = str2tags(tags)
        self._tags = tags

    def __lt__(self, other):
        if self.ns_time != other.ns_time:
            return self.ns_time < other.ns_time
        if self.series != other.series:
            return self.series < other.series
        if self.tags != other.tags:
            return self.tags_str < other.tags_str
        return self.value < other.value

    def __eq__(self, other):
        if self.ns_time != other.ns_time:
            return False
        if self.series != other.series:
            return False
        if self.tags != other.tags:
            return False
        return True

    @property
    def key(self) -> tuple[str, str]:
        """Key for hashing."""
        return (self.series, self.tags_str)

    @property
    def tags_str(self) -> str:
        """Return tags as string."""
        return tags2str(self.tags)

    def __repr__(self) -> str:
        """Return string representation."""
        return (
            f"<{self.__class__.__name__}:{self.mode}:{self.ns_time} "
            f"{self.value} {self.series} {self.tags_str}>"
        )

    def __str__(self) -> str:
        """Return human-readable string."""
        return f"{self.value} {self.mode}@{self.ns_time} {self.series} {self.tags_str}"


def str2tags(tags: dict[str, str] | str) -> dict[str, str | int | float]:
    """Convert tags string to dictionary."""
    if isinstance(tags, dict):
        return tags
    t: dict[str, str | int | float] = {}
    for kv in tags.split(" "):
        if not kv:
            continue
        k, v_str = kv.split("=", 1)
        v: str | int | float = v_str
        try:
            v = int(v_str)
        except ValueError:
            try:
                v = float(v_str)
            except ValueError:
                pass
        t[k] = v
    return t


def tags2str(tags: dict[str, str | int | float]) -> str:
    """Convert tags dictionary to string."""
    return " ".join(f"{k}={v}" for k, v in sorted(tags.items()))


class EntryDelta:
    """
    This class accepts an entry which it may or may not return later.

    All non-raw data you feed to this are converted to gauges, i.e. rate
    per second.

    Use::
        f = EntryDelta()
        async for e in read_entries():
            e = f(e)
            if e is None:
                continue
            await store_entry(e)
    """

    def __init__(self) -> None:
        """Initialize entry delta tracker."""
        self._last: dict[tuple[str, str], tuple[int | float, int]] = {}
        self._prev: dict[tuple[str, str], Entry] = {}
        self._dup: dict[tuple[str, str], bool] = {}  # Track duplicate values

    def __call__(self, entry: Entry) -> Entry | None:
        k = entry.key
        if entry.mode == DS.invalid:
            raise RuntimeError("mode needs to be set")
        if entry.mode in {DS.counter, DS.derive, DS.absolute}:
            # First step: calculate delta.
            v = entry.value
            try:
                ov, ot = self._last[k]
            except KeyError:
                self._last[k] = (entry.value, entry.ns_time)
                return
            else:
                t = entry.ns_time
            if ot >= t:
                return
            r = (t - ot) / 1000000000  # nsec > sec
            self._last[k] = (entry.value, t)

            if entry.mode == DS.derive:
                entry.value = (v - ov) / r
            elif entry.mode == DS.counter:
                if v < ov:
                    if ov < 2**32:
                        v += 2**32
                    else:
                        v += 2**64
                entry.value = (v - ov) / r
            elif entry.mode == DS.absolute:
                entry.value /= r

            # We have successfully removed the time based element.

        # Second step: Return the previous value if there either is or was
        # a difference. Invariant: an entry stored in self._prev has a
        # duplicate flag in self._dup which is True iff the entry before that, already
        # stored in the database, has the same value.
        try:
            prev = self._prev[k]
        except KeyError:
            self._dup[k] = False
            return None
        else:
            if prev.value == entry.value and (entry.mode != DS.delta or entry.value == 0):
                # We cannot skip identical Delta values unless they're zero.
                self._dup[k] = True
                if self._dup.get(k, False):
                    return None
            else:
                self._dup[k] = False
            if prev.mode < DS.delta:
                prev.mode = DS.gauge
            return prev
        finally:
            self._prev[k] = entry

    def flush(self) -> Generator[Entry, None, None]:
        """Iterate stored entries."""
        for k, v in self._prev.items():
            try:
                lv, lt = self._last[k]
            except KeyError:
                pass
            else:
                v.abs_value = lv  # type: ignore[attr-defined]  # Dynamic attribute for restoration
                v.ns_time = lt
            yield v
        self._prev = {}
        self._dup = {}

    def prep(self, entry: Entry) -> None:
        """Restore saved entry from previous run.
        Requires "abs_value" to be restored!
        """
        k = entry.key
        if k in self._prev:
            raise RuntimeError("known", k)
        self._dup[k] = False
        self._prev[k] = entry
        if entry.mode in {DS.counter, DS.derive, DS.absolute}:
            abs_value = getattr(entry, "abs_value", entry.value)
            self._last[k] = (abs_value, entry.ns_time)
