from __future__ import annotations

from collections.abc import Mapping, Sequence


def build_query(
    *,
    bucket: str,
    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,
) -> str:
    """Build a Flux query string.

    Parameters
    ----------
    bucket : str
        InfluxDB bucket name.
    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. If ``None``, all measurements are queried.
    fields : Sequence[str] | None, default=None
        Optional field names to include.
    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
    -------
    str
        Flux query string.

    Raises
    ------
    ValueError
        If ``limit`` is not greater than 0.
    """
    lines = [
        f'from(bucket: "{_escape_flux_string(bucket)}")',
        f"  |> range(start: {_flux_time_expr(start)}"
        + (f", stop: {_flux_time_expr(stop)}" if stop is not None else "")
        + ")",
    ]

    if measurement is not None:
        lines.append(
            f'  |> filter(fn: (r) => r._measurement == "{_escape_flux_string(measurement)}")'
        )

    if fields:
        fields_list = ", ".join(f'"{_escape_flux_string(field)}"' for field in fields)
        lines.append(
            f"  |> filter(fn: (r) => contains(value: r._field, set: [{fields_list}]))"
        )

    if tags:
        for key, value in sorted(tags.items()):
            escaped_key = _escape_flux_string(key)
            escaped_value = _escape_flux_string(value)
            lines.append(
                f'  |> filter(fn: (r) => r["{escaped_key}"] == "{escaped_value}")'
            )

    if columns:
        cols_list = ", ".join(f'"{_escape_flux_string(col)}"' for col in columns)
        lines.append(f"  |> keep(columns: [{cols_list}])")

    if sort_desc:
        lines.append('  |> sort(columns: ["_time"], desc: true)')

    if limit is not None:
        if limit <= 0:
            raise ValueError("'limit' must be greater than 0.")
        lines.append(f"  |> limit(n: {limit})")

    return "\n".join(lines)


def _escape_flux_string(value: str) -> str:
    """Escape a string value for safe Flux string literals.

    Parameters
    ----------
    value : str
        Raw string value.

    Returns
    -------
    str
        Escaped Flux string content.
    """
    return value.replace("\\", "\\\\").replace('"', '\\"')


def _flux_time_expr(value: str) -> str:
    """Normalize time input into a Flux time expression.

    Parameters
    ----------
    value : str
        Input time expression.

    Returns
    -------
    str
        Flux-ready time expression.
    """
    expr = value.strip()
    if expr.startswith(("-", "time(", "now(")):
        return expr
    return f'time(v: "{_escape_flux_string(expr)}")'
