"""Run a parameterized SELECT over an open pymssql (SQL Server / Navision) connection."""

from __future__ import annotations


def mssql_query(conn, sql: str, params=None, max_rows: int | None = None) -> dict:
    """Execute a SELECT on an already-open connection and map rows to dicts.

    The connection is supplied by the caller (typically from `mssql_connect`),
    so a single connection can be opened once and reused for many queries. This
    function never opens or closes the connection — it only borrows it. It is
    impure I/O: it touches the database over an existing connection.

    Parameter binding is delegated to the driver: `params` is passed straight to
    `cursor.execute(sql, params)`. NEVER interpolate values into `sql` with
    f-strings or `%` formatting — that opens the door to SQL injection. Use the
    pymssql placeholders `%s` (positional) or `%(name)s` (named) in `sql` and
    let the driver bind safely. When `params is None`, the SQL is executed with
    no bound parameters.

    The query runs read-only: no commit is issued. The cursor opened here is
    always closed before returning (try/finally), even on error.

    Args:
        conn: An open connection object (e.g. the one returned by
            `mssql_connect`). Used by duck typing via `conn.cursor()`, so the
            concrete driver does not matter and the function stays testable.
        sql: The SELECT statement, using pymssql placeholders `%s` (positional)
            or `%(name)s` (named) for any bound values.
        params: A tuple/list for positional placeholders, a dict for named
            placeholders, or None for a query with no parameters. Passed to
            `cursor.execute(sql, params)` for safe driver-side binding.
        max_rows: If a positive int, only the first `max_rows` rows are fetched
            (via `cursor.fetchmany(max_rows)`). If None, all rows are fetched
            (via `cursor.fetchall()`).

    Returns:
        A dict with three keys:
        - "columns": list of column names in result order (empty list if the
          statement produced no result set, i.e. `cursor.description is None`).
        - "rows": list of dicts, one per row, mapping each column name to its
          value. Empty list when the query returned no rows.
        - "row_count": int, equal to `len(rows)`.

    Raises:
        RuntimeError: If executing or fetching the query fails. The message is
            deliberately generic (it does not include the SQL or the params,
            which may carry sensitive data) and the original exception is
            chained for debugging.
    """
    cur = conn.cursor()
    try:
        try:
            if params is None:
                cur.execute(sql)
            else:
                cur.execute(sql, params)

            description = cur.description
            if description is None:
                columns: list = []
                raw_rows: list = []
            else:
                columns = [d[0] for d in description]
                if max_rows is not None and max_rows > 0:
                    raw_rows = cur.fetchmany(max_rows)
                else:
                    raw_rows = cur.fetchall()
        except Exception as exc:
            raise RuntimeError(
                f"mssql_query failed executing query: {exc}"
            ) from exc
    finally:
        cur.close()

    rows = [dict(zip(columns, row)) for row in raw_rows]
    return {"columns": columns, "rows": rows, "row_count": len(rows)}
