"""Keyset cursor encoding and decoding.

Single-column cursors are plain ``str(value)`` — no encoding, easy to debug.
Compound cursors (multiple ORDER BY columns) use a pluggable ``CursorCodec``
(default: URL-safe base64 of a compact JSON object).
"""

from collections.abc import Sequence
from typing import Any

from sqlalchemy_relay_pagination.codec import CursorCodec
from sqlalchemy_relay_pagination.exceptions import InvalidCursorError, NullOrderingColumnError
from sqlalchemy_relay_pagination.keyset import ColWithDir


def encode_cursor(cols: Sequence[ColWithDir], row: Any, codec: CursorCodec) -> str:
    """Return a cursor string for the given row.

    Single-column: returns ``str(value)`` (no encoding).
    Compound: returns ``codec.encode({col_key: value, ...})``.

    Raises:
        NullOrderingColumnError: If any cursor column value is ``None``.
    """
    if len(cols) == 1:
        col, _ = cols[0]
        value = getattr(row, col.key)
        if value is None:
            raise NullOrderingColumnError("NULL values in the cursor column are not allowed")
        return str(value)

    values: dict[str, Any] = {}
    for col, _ in cols:
        value = getattr(row, col.key)
        if value is None:
            raise NullOrderingColumnError(f"NULL values in cursor column {col.key!r} are not allowed")
        values[col.key] = value
    return codec.encode(values)


def decode_cursor(cursor: str, cols: Sequence[ColWithDir], codec: CursorCodec) -> list[Any]:
    """Parse a cursor string back to a list of Python values (one per column).

    Single-column: coerces ``cursor`` via ``col.type.python_type``.
    Compound: decodes via ``codec``, then coerces each value.

    Raises:
        InvalidCursorError: If the cursor cannot be parsed or a key is missing.
    """
    if len(cols) == 1:
        col, _ = cols[0]
        try:
            return [col.type.python_type(cursor)]
        except Exception as e:
            raise InvalidCursorError(f"Invalid cursor {cursor!r} for column {col.key!r}: {e}") from e

    raw = codec.decode(cursor)  # raises InvalidCursorError internally
    result = []
    for col, _ in cols:
        if col.key not in raw:
            raise InvalidCursorError(f"Compound cursor {cursor!r} missing key {col.key!r}")
        try:
            result.append(col.type.python_type(raw[col.key]))
        except Exception as e:
            raise InvalidCursorError(f"Invalid value for column {col.key!r} in cursor {cursor!r}: {e}") from e
    return result
