#
# Stream adapter for VictoriaMetrics using OpenTSDB telnet protocol
from __future__ import annotations

import anyio
import heapq
import math
import time
from contextlib import asynccontextmanager
from datetime import datetime

from pytz import UTC

from .buffered import BufferedReader, IncompleteReadError
from .model import Entry, EntryDelta
from .sanitize import sanitize_label_name, sanitize_metric_name

from collections.abc import AsyncGenerator, Iterable, Mapping
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import httpx

EOL = b"\n"

_url = "http://localhost:8428/api/v1/query"


class NoCodeError(RuntimeError):
    """Could not encode this"""

    def __init__(self, data):
        super().__init__()
        self.data = data

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__}:{self.data!r}>"

    def __str__(self) -> str:
        return f"Cannot encode {self.data.__class__.__name__}"


class RespError(RuntimeError):
    """Received an error"""

    def __str__(self):
        return "VictoriaMetrics: error: " + " ".join(
            str(x) for x in self.args  # pylint: disable=not-an-iterable
        )


class RespUnknownError(RespError):
    """Received an unknown line"""

    pass


RespType = str | int | float | Iterable["RespType"]
RespTags = Mapping[str, str]
ExtRespType = BaseException | str | int | float | Iterable[RespType]


async def get_max_ts(
    httpx_session: httpx.AsyncClient,
    series: str,
    tags: dict[str, str] | None = None,
    url: str = _url,
) -> float:
    """
    Read the timestamp of the last entry using VictoriaMetrics instant query API.

    Automatically sanitizes metric and label names to comply with Prometheus naming rules.
    """
    if tags is None:
        tags = {}

    # Sanitize metric and label names
    series = sanitize_metric_name(series)
    tags = {sanitize_label_name(k): v for k, v in tags.items()}

    # Build the query with label filters
    label_filters = ",".join(f'{k}="{v}"' for k, v in tags.items())
    if label_filters:
        query = f"{series}{{{label_filters}}}"
    else:
        query = series

    # Use instant query to get the last value
    r = await httpx_session.get(url, params={"query": query})
    if r.status_code != 200:
        return -math.inf

    try:
        data = r.json()
        if data.get("status") != "success":
            return -math.inf

        result = data.get("data", {}).get("result", [])
        if not result:
            return -math.inf

        # Get the timestamp from the first result
        value_data = result[0].get("value", [])
        if len(value_data) >= 2:
            return float(value_data[0])

        return -math.inf
    except (KeyError, IndexError, ValueError):
        return -math.inf


async def get_data(
    httpx_session: httpx.AsyncClient,
    series: str,
    tags: dict[str, str],
    t_start: float | datetime | None = None,
    t_end: float | datetime | None = None,
    url: str = _url,
    aggregate: str | bool = False,
    group: str | list[str] | None = None,
    pivot: str | list[str] | None = None,  # noqa: ARG001 - Reserved for future use
) -> AsyncGenerator[Entry, None]:
    """
    Read some data using VictoriaMetrics range query API.

    Automatically sanitizes metric and label names to comply with Prometheus naming rules.
    """
    # Sanitize metric and label names
    series = sanitize_metric_name(series)
    tags = {sanitize_label_name(k): v for k, v in tags.items()}

    # Build the query with label filters
    label_filters = ",".join(f'{k}="{v}"' for k, v in tags.items())
    if label_filters:
        query = f"{series}{{{label_filters}}}"
    else:
        query = series

    if aggregate:
        # Apply aggregation function
        if group:
            if isinstance(group, str):
                group = [group]
            group_by = ",".join(group)
            query = f"{aggregate}({query}) by ({group_by})"
        else:
            query = f"{aggregate}({query})"

    params = {"query": query}

    # Convert timestamps to RFC3339 format for VictoriaMetrics
    if t_start:
        if isinstance(t_start, (int, float)):
            t_start_dt = datetime.fromtimestamp(t_start, tz=UTC)
        else:
            t_start_dt = t_start
        params["start"] = t_start_dt.isoformat()

    if t_end:
        if isinstance(t_end, (int, float)):
            t_end_dt = datetime.fromtimestamp(t_end, tz=UTC)
        else:
            t_end_dt = t_end
        params["end"] = t_end_dt.isoformat()

    # For range queries, we need to use query_range endpoint
    range_url = url.replace("/query", "/query_range")
    if "start" in params:
        # Add step parameter for range queries (default 5m)
        params.setdefault("step", "60s")
        r = await httpx_session.get(range_url, params=params)
    else:
        r = await httpx_session.get(url, params=params)

    if r.status_code != 200:
        error_msg = r.reason_phrase if hasattr(r, "reason_phrase") else str(r.status_code)
        try:
            error_data = r.json()
            if "error" in error_data:
                error_msg = f"{error_msg}: {error_data['error']}"
            elif "errorType" in error_data:
                error_type = error_data.get("errorType", "")
                error_detail = error_data.get("error", "")
                error_msg = f"{error_msg}: {error_type}: {error_detail}"
        except Exception:
            # If we can't parse JSON, try to get text
            try:
                error_msg = f"{error_msg}: {r.text}"
            except Exception:  # noqa: S110 - Fallback to basic error message
                pass
        raise RespError(error_msg)

    try:
        data = r.json()
        if data.get("status") != "success":
            raise RespError(data.get("error", "Unknown error"))

        result = data.get("data", {}).get("result", [])
        for series_data in result:
            metric = series_data.get("metric", {})
            series_name = metric.pop("__name__", series)

            # Get values - could be single value (instant) or multiple (range)
            values = series_data.get("values") or [series_data.get("value")]
            if not values or values[0] is None:
                continue

            for value_pair in values:
                if len(value_pair) >= 2:
                    timestamp, value_str = value_pair[0], value_pair[1]
                    try:
                        value = float(value_str)
                    except (ValueError, TypeError):
                        continue

                    tm = datetime.fromtimestamp(float(timestamp), tz=UTC)
                    yield Entry(value, series_name, metric, tm)
    except (KeyError, ValueError, TypeError) as e:
        raise RespError(f"Failed to parse response: {e}") from e


class Resp:
    """
    This class implements VictoriaMetrics' OpenTSDB telnet protocol for submission.

    If "delta" is True, uses an EntryDelta filter to skip runs of
    consecutive values if appropriate.

    If "size" is set, queue submission is halted when more entries than
    that are queued.

    If "delay" is set, submission of entries is deferred until they're at
    least this #seconds old.
    """

    _t = None

    def __init__(self, stream, delay=0, size=0, delta=False):
        if not isinstance(stream, BufferedReader):
            stream = BufferedReader(stream)
        self.stream = stream
        self.buf = []
        self._dict = {}
        self._dt = 0
        self._heap = []
        self._same = EntryDelta() if delta else lambda x: x
        self._delay = delay
        self._heap_large: anyio.abc.Event = None  # waits for heap emptying
        self._heap_item: anyio.abc.Event = None  # waits for heappush
        self._done: anyio.abc.Event = None  # flushing done
        self._ending: anyio.abc.Event = anyio.Event()  # flush
        self._heap_max = size

    def preload(self, series: str, tags: RespTags):
        """
        Preload this series+tags entry - not needed for OpenTSDB protocol
        """
        pass

    async def flush_dict(self):
        """
        Flush preload entries - not needed for OpenTSDB protocol
        """
        pass

    async def aclose(self):
        await self.flush()
        await self.stream.aclose()

    def _encode_opentsdb(self, pkt: Entry):
        """
        Encode a packet in OpenTSDB telnet format:
        put <metric> <timestamp> <value> <tagk1=tagv1[ tagk2=tagv2 ...tagkN=tagvN]>

        Automatically sanitizes metric and label names to comply with Prometheus naming rules.
        """
        # Convert nanosecond timestamp to seconds
        timestamp = pkt.ns_time // 1000000000

        # Sanitize metric name
        metric = sanitize_metric_name(pkt.series)

        # Build tags string with sanitized tag names
        tags_str = " ".join(f"{sanitize_label_name(k)}={v}" for k, v in sorted(pkt.tags.items()))

        # Format: put <metric> <timestamp> <value> <tags>
        line = f"put {metric} {timestamp} {pkt.value} {tags_str}\n"
        return line.encode("utf-8")

    async def write(self, pkt: Entry):
        """Send the contents of this packet"""
        self.buf.append(self._encode_opentsdb(pkt))
        if len(self.buf) > 1000:
            await self.flush_buf()

    async def put(self, pkt):
        """Store this packet, for eventual sending."""
        heapq.heappush(self._heap, pkt)
        if self._heap_item is not None:
            self._heap_item.set()
            self._heap_item = None

        if self._heap_max > 0:
            if self._heap_large is None and len(self._heap) >= self._heap_max:
                self._heap_large = anyio.Event()
            if self._heap_large is not None:
                await self._heap_large.wait()

    async def _next_to_send(self):
        """
        Returns the next message on the heap
        """
        while True:
            while self._heap:
                if self._heap_large is not None and len(self._heap) < self._heap_max / 2:
                    self._heap_large.set()
                    self._heap_large = None

                if self._ending.is_set() or self._heap[0].time <= self._t - self._delay:
                    return heapq.heappop(self._heap)
                self._t = time.time()
                if self._heap[0].time <= self._t - self._delay:
                    return heapq.heappop(self._heap)
                with anyio.move_on_after(max(self._delay + self._heap[0].time - self._t, 0)):
                    await self._ending.wait()

            await self.flush_buf()
            if self._done is not None:
                self._done.set()
            self._heap_item = anyio.Event()
            await self._heap_item.wait()

    async def send(self, evt: anyio.abc.Event = None):
        """
        Send loop.
        """
        self._t = time.time()
        if evt:
            evt.set()

        while True:
            e = await self._next_to_send()
            if e is None:
                return
            e = self._same(e)
            if e is not None:
                await self.write(e)

    async def flush(self):
        """
        Send our write-buffered data.
        """
        if self._heap:
            self._done = anyio.Event()
            self._ending.set()
            await self._done.wait()
            self._ending = anyio.Event()
            self._done = None

    async def flush_buf(self):
        buf = b"".join(self.buf)
        self.buf = []
        if buf:
            await self.stream.send(buf)

    async def receive(self):
        """
        Read a response line from VictoriaMetrics (usually not used in telnet protocol)
        """
        try:
            line = await self.stream.readuntil(EOL)
        except IncompleteReadError as err:
            return err.chunk
        if not line:
            return None
        return line

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self.aclose()

    def __aiter__(self):
        return self

    async def __anext__(self):
        try:
            r = await self.receive()
        except anyio.ClosedResourceError:
            raise StopAsyncIteration from None
        if r is None:
            raise StopAsyncIteration
        return r


async def reader(s):
    """
    Read messages from the stream and raise an error when any arrive.
    """
    async for m in s:
        if not m:
            return
        # VictoriaMetrics doesn't send responses in OpenTSDB telnet protocol
        # unless there's an error
        if m and m.strip():
            raise RuntimeError(m.decode("utf-8", errors="replace"))


@asynccontextmanager
async def connect(host="127.0.0.1", port=4242, **kw):
    """A context manager for managing the Resp object.

    Default port is 4242 for OpenTSDB protocol.
    """
    async with anyio.create_task_group() as tg, await anyio.connect_tcp(host, port) as st:
        s = Resp(st, **kw)
        evt = anyio.Event()
        tg.start_soon(s.send, evt)
        await evt.wait()
        tg.start_soon(reader, s)
        try:
            yield s

        finally:
            with anyio.move_on_after(5, shield=True):
                await s.flush()
                await anyio.sleep(0.3)  # let's hope that's enough to read errors
            tg.cancel_scope.cancel()
