"""Safe SQL parameter binding for ADO queries."""

from __future__ import annotations

import re
from string import ascii_letters, digits

_PLACEHOLDER_RE = re.compile(r"%([sdfwu%])")

_IDENTIFIER_CHARS = frozenset(ascii_letters + digits + "_@$#")
_WORD_FORBIDDEN = (" ", "\t", "\n", "\r", "/*", "*/", "--")


def _quote_string(value: str) -> str:
    return "'" + value.replace("'", "''") + "'"


def _check_word(value: str) -> str:
    for forbidden in _WORD_FORBIDDEN:
        if forbidden in value:
            raise ValueError(
                f"Placeholder %w does not allow whitespace or comment markers, "
                f"got {value!r}."
            )
    return _quote_string(value)


def _check_identifier(value: str) -> str:
    if not value:
        raise ValueError("Placeholder %u must not be empty.")
    invalid = [c for c in value if c not in _IDENTIFIER_CHARS]
    if invalid:
        raise ValueError(
            f"Invalid characters in placeholder %%u: {invalid!r}. "
            "Only letters, digits, '_', '@', '$', and '#' are allowed."
        )
    return value


def bind_sql_params(sql: str, *params: int | str | float) -> str:
    """Substitute positional placeholders in a SQL string with safely escaped values.

    Placeholders are processed left-to-right; each consumes one value from *params*.

    Supported placeholders:

    - ``%s``: String value. Single quotes are escaped by doubling (``'`` → ``''``).
      The result is enclosed in single quotes.
    - ``%w``: Word string. Like ``%s``, but additionally forbids whitespace and SQL
      comment markers (``--``, ``/*``, ``*/``). Use when only a single token is expected.
    - ``%d``: Integer value. The parameter is cast to ``int``; non-numeric values
      raise ``ValueError``.
    - ``%f``: Float value. The parameter is cast to ``float``; non-numeric values
      raise ``ValueError``.
    - ``%u``: Unquoted identifier (table or column name). Only letters, digits, ``_``,
      ``@``, ``$``, and ``#`` are allowed; the value is inserted without quotes.
    - ``%%``: Literal percent sign — no parameter consumed.

    Args:
        sql: SQL template string containing placeholders.
        *params: Values to substitute, one per non-``%%`` placeholder.

    Returns:
        The SQL string with all placeholders replaced by their escaped values.

    Raises:
        ValueError: When a placeholder type does not match the supplied value,
            a word or identifier contains forbidden characters, or the number of
            params does not match the number of placeholders.

    Example::

        sql = bind_sql_params(
            "SELECT * FROM %u WHERE name = %s AND age > %d",
            "benutzer", "O'Brien", 18,
        )
        # → "SELECT * FROM benutzer WHERE name = 'O''Brien' AND age > 18"
    """
    params_iter = iter(params)
    consumed = 0
    total = len(params)

    def _replace(match: re.Match) -> str:
        nonlocal consumed
        kind = match.group(1)

        if kind == "%":
            return "%"

        try:
            value = next(params_iter)
        except StopIteration as exc:
            raise ValueError(
                f"Not enough parameters: placeholder %{kind} at position {match.start()} "
                f"has no corresponding value ({consumed} consumed, {total} provided)."
            ) from exc

        consumed += 1

        if kind == "s":
            return _quote_string(str(value))
        if kind == "w":
            return _check_word(str(value))
        if kind == "d":
            try:
                return str(int(value))  # type: ignore[arg-type]
            except (TypeError, ValueError) as exc:
                raise ValueError(
                    f"Placeholder %d requires an integer, got {value!r}."
                ) from exc
        if kind == "f":
            try:
                return str(float(value))  # type: ignore[arg-type]
            except (TypeError, ValueError) as exc:
                raise ValueError(
                    f"Placeholder %f requires a float, got {value!r}."
                ) from exc
        if kind == "u":
            return _check_identifier(str(value))

        raise ValueError(f"Unknown placeholder type: %{kind}")  # pragma: no cover

    result = _PLACEHOLDER_RE.sub(_replace, sql)

    remaining = total - consumed
    if remaining > 0:
        raise ValueError(
            f"Too many parameters: {total} provided but only {consumed} placeholder(s) found."
        )

    return result
