from __future__ import annotations

from collections.abc import Iterator, Mapping, Sequence
from dataclasses import dataclass
from hashlib import sha256
from pathlib import Path
from time import perf_counter

from influxdb_client.client.influxdb_client import InfluxDBClient

from .query_build import build_query
from ..settings import InfluxSettings, load_settings


@dataclass(frozen=True, slots=True)
class QueryResultMeta:
    """Execution metadata for one query call."""

    elapsed_ms: int
    row_count: int
    query_sha256: str


def query_rows(
    *,
    settings_path: str | Path | None = None,
    bucket: str | None = None,
    start: str = "-1h",
    stop: str | None = None,
    measurement: str | None = None,
    fields: Sequence[str] | None = None,
    tags: Mapping[str, str] | None = None,
    columns: Sequence[str] | None = None,
    limit: int | None = None,
    sort_desc: bool = False,
) -> list[dict[str, object]]:
    """Run a structured query and return all rows.

    Parameters
    ----------
    settings_path : str | Path | None, default=None
        Optional path to settings TOML.
    bucket : str | None, default=None
        Optional bucket override. Defaults to value from settings.
    start : str, default="-1h"
        Flux start time expression.
    stop : str | None, default=None
        Optional Flux stop time expression.
    measurement : str | None, default=None
        Optional measurement filter.
    fields : Sequence[str] | None, default=None
        Optional field list filter.
    tags : Mapping[str, str] | None, default=None
        Optional tag equality filters.
    columns : Sequence[str] | None, default=None
        Optional keep-column list.
    limit : int | None, default=None
        Optional row limit.
    sort_desc : bool, default=False
        If ``True``, sort by ``_time`` descending.

    Returns
    -------
    list[dict[str, object]]
        Query result rows.
    """
    settings = load_settings(settings_path)
    query_text = build_query(
        bucket=bucket or settings.bucket,
        start=start,
        stop=stop,
        measurement=measurement,
        fields=fields,
        tags=tags,
        columns=columns,
        limit=limit,
        sort_desc=sort_desc,
    )
    return _collect_rows(settings=settings, query_text=query_text)


def query_flux_rows(
    *,
    query: str,
    settings_path: str | Path | None = None,
) -> list[dict[str, object]]:
    """Run a raw Flux query and return all rows."""
    settings = load_settings(settings_path)
    return _collect_rows(settings=settings, query_text=_normalize_query(query))


def query_stream(
    *,
    settings_path: str | Path | None = None,
    bucket: str | None = None,
    start: str = "-1h",
    stop: str | None = None,
    measurement: str | None = None,
    fields: Sequence[str] | None = None,
    tags: Mapping[str, str] | None = None,
    columns: Sequence[str] | None = None,
    limit: int | None = None,
    sort_desc: bool = False,
) -> Iterator[dict[str, object]]:
    """Stream rows for a structured query as an iterator."""
    settings = load_settings(settings_path)
    query_text = build_query(
        bucket=bucket or settings.bucket,
        start=start,
        stop=stop,
        measurement=measurement,
        fields=fields,
        tags=tags,
        columns=columns,
        limit=limit,
        sort_desc=sort_desc,
    )
    return _stream_rows(settings=settings, query_text=query_text)


def query_flux_stream(
    *,
    query: str,
    settings_path: str | Path | None = None,
) -> Iterator[dict[str, object]]:
    """Stream rows for a raw Flux query as an iterator."""
    settings = load_settings(settings_path)
    return _stream_rows(settings=settings, query_text=_normalize_query(query))


def query_rows_with_meta(
    *,
    settings_path: str | Path | None = None,
    bucket: str | None = None,
    start: str = "-1h",
    stop: str | None = None,
    measurement: str | None = None,
    fields: Sequence[str] | None = None,
    tags: Mapping[str, str] | None = None,
    columns: Sequence[str] | None = None,
    limit: int | None = None,
    sort_desc: bool = False,
) -> tuple[list[dict[str, object]], QueryResultMeta]:
    """Run a structured query and return rows plus query metadata."""
    settings = load_settings(settings_path)
    query_text = build_query(
        bucket=bucket or settings.bucket,
        start=start,
        stop=stop,
        measurement=measurement,
        fields=fields,
        tags=tags,
        columns=columns,
        limit=limit,
        sort_desc=sort_desc,
    )
    return _collect_rows_with_meta(settings=settings, query_text=query_text)


def query_flux_rows_with_meta(
    *,
    query: str,
    settings_path: str | Path | None = None,
) -> tuple[list[dict[str, object]], QueryResultMeta]:
    """Run a raw Flux query and return rows plus query metadata."""
    settings = load_settings(settings_path)
    return _collect_rows_with_meta(
        settings=settings,
        query_text=_normalize_query(query),
    )


def _normalize_query(query: str) -> str:
    if not isinstance(query, str):
        raise TypeError("'query' must be a string.")
    query_text = query.strip()
    if not query_text:
        raise ValueError("'query' must be a non-empty string.")
    return query_text


def _collect_rows(
    *,
    settings: InfluxSettings,
    query_text: str,
) -> list[dict[str, object]]:
    return list(_stream_rows(settings=settings, query_text=query_text))


def _collect_rows_with_meta(
    *,
    settings: InfluxSettings,
    query_text: str,
) -> tuple[list[dict[str, object]], QueryResultMeta]:
    started = perf_counter()
    rows = _collect_rows(settings=settings, query_text=query_text)
    elapsed_ms = int((perf_counter() - started) * 1000)
    meta = QueryResultMeta(
        elapsed_ms=max(elapsed_ms, 0),
        row_count=len(rows),
        query_sha256=sha256(query_text.encode("utf-8")).hexdigest(),
    )
    return rows, meta


def _stream_rows(
    *,
    settings: InfluxSettings,
    query_text: str,
) -> Iterator[dict[str, object]]:
    client = InfluxDBClient(url=settings.url, token=settings.token, org=settings.org)
    try:
        tables = client.query_api().query(query=query_text, org=settings.org)
        rows = [
            {str(key): value for key, value in record.values.items()}
            for table in tables
            for record in table.records
        ]
    finally:
        client.close()
    yield from rows
