from __future__ import annotations

import base64
import hashlib
import json
from datetime import date, datetime
from decimal import Decimal
from typing import Any, Mapping

KEYSET_CURSOR_PREFIX = "ks1:"


class KeysetCursorError(ValueError):
    """Raised when a keyset cursor cannot be decoded for the current query."""


def keyset_query_fingerprint(scope: str, params: Mapping[str, Any]) -> str:
    payload = {"scope": scope, "params": _jsonable(params)}
    raw = json.dumps(payload, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]


def encode_keyset_cursor(scope: str, *, fingerprint: str, values: Mapping[str, Any]) -> str:
    payload = {
        "v": 1,
        "scope": scope,
        "fingerprint": fingerprint,
        "values": _jsonable(values),
    }
    raw = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
    encoded = base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
    return f"{KEYSET_CURSOR_PREFIX}{encoded}"


def decode_keyset_cursor(scope: str, cursor: str | None, *, fingerprint: str | None = None) -> dict[str, Any] | None:
    if cursor is None or not cursor.strip():
        return None
    raw_cursor = cursor.strip()
    if not raw_cursor.startswith(KEYSET_CURSOR_PREFIX):
        raise KeysetCursorError("Invalid pagination cursor")
    encoded = raw_cursor[len(KEYSET_CURSOR_PREFIX):]
    try:
        padded = encoded + ("=" * (-len(encoded) % 4))
        payload = json.loads(base64.urlsafe_b64decode(padded.encode("ascii")).decode("utf-8"))
    except (UnicodeDecodeError, ValueError, json.JSONDecodeError) as exc:
        raise KeysetCursorError("Invalid pagination cursor") from exc
    if not isinstance(payload, dict) or payload.get("v") != 1:
        raise KeysetCursorError("Invalid pagination cursor")
    if payload.get("scope") != scope:
        raise KeysetCursorError("Pagination cursor does not match this endpoint")
    if fingerprint is not None and payload.get("fingerprint") != fingerprint:
        raise KeysetCursorError("Pagination cursor does not match the current query")
    values = payload.get("values")
    if not isinstance(values, dict):
        raise KeysetCursorError("Invalid pagination cursor")
    return values


def _jsonable(value: Any) -> Any:
    if isinstance(value, datetime):
        return value.isoformat()
    if isinstance(value, date):
        return value.isoformat()
    if isinstance(value, Decimal):
        return str(value)
    if isinstance(value, Mapping):
        return {str(key): _jsonable(item) for key, item in sorted(value.items(), key=lambda pair: str(pair[0]))}
    if isinstance(value, (list, tuple, set)):
        return [_jsonable(item) for item in value]
    return value
