from __future__ import annotations

from collections.abc import Iterable
from typing import Any


class UopyNotConfiguredError(RuntimeError):
    pass


def _coerce_record_id(record_id: Any) -> str:
    if record_id is None:
        raise ValueError("record_id is None")

    if isinstance(record_id, str):
        return record_id
    if isinstance(record_id, bytes):
        return record_id.decode("utf-8", errors="replace")

    # uopy.List.next() returns a DynArray representing a record id.
    v = getattr(record_id, "list", None)
    if isinstance(v, str):
        return v
    if isinstance(v, list) and len(v) == 1 and isinstance(v[0], str):
        return v[0]

    return str(record_id)


def _coerce_record_value(record: Any) -> Any:
    """Make record JSON/str friendly while retaining structure when possible."""

    if record is None:
        return None
    if isinstance(record, (str, int, float, bool)):
        return record
    if isinstance(record, bytes):
        return record.decode("utf-8", errors="replace")

    # uopy.File.read returns DynArray; prefer its Python list representation.
    v = getattr(record, "list", None)
    if v is not None:
        return v

    return str(record)


def _get_or_open_file(session: Any, file_name: str) -> Any:
    """Get a cached uopy.File handle bound to this session.

    This avoids the overhead of opening the UniVerse file on every read.
    Safe because each process owns its own session.
    """

    try:
        import uopy  # type: ignore
    except Exception as exc:  # pragma: no cover
        raise UopyNotConfiguredError(
            "Failed to import 'uopy'. Install it in this environment first."
        ) from exc

    cache = getattr(session, "_universe_reads_file_cache", None)
    if cache is None:
        cache = {}
        setattr(session, "_universe_reads_file_cache", cache)

    file_obj = cache.get(file_name)
    if file_obj is None:
        file_obj = uopy.File(file_name, session=session)
        file_obj.open()
        cache[file_name] = file_obj

    return file_obj


def iter_keys(session: Any, file_name: str) -> Iterable[str]:
    """Yield record IDs/keys for a UniVerse file.

    Goal: stream keys without loading all 300k into memory.

    Replace this body with the correct `uopy` calls for your environment.
    """

    try:
        import uopy  # type: ignore
    except Exception as exc:  # pragma: no cover
        raise UopyNotConfiguredError(
            "Failed to import 'uopy'. Install it in this environment first."
        ) from exc

    # Stream keys without pulling the entire list back.
    with uopy.File(file_name, session=session) as file_obj:
        select_list = uopy.List(session=session)
        select_list.select(file_obj)

        while True:
            rid = select_list.next()
            if rid is None:
                break
            yield _coerce_record_id(rid)


def iter_keys_filtered(
    session: Any,
    file_name: str,
    *,
    where_clause: str | None = None,
    select_override: str | None = None,
) -> Iterable[str]:
    """Yield record IDs matching a UniVerse SELECT criteria.

    Args:
        session: An open uopy session.
        file_name: The UniVerse file to select from.
        where_clause: A UniVerse WHERE/WITH clause appended to the default
            ``SELECT <file_name>``. Example: ``"WITH STATUS = 'ACTIVE'"``.
        select_override: A full SELECT statement that replaces the default.
            Example: ``"SELECT STUDENTS WITH GPA >= '3.0'"``.

    The WHERE clause is ignored if ``select_override`` is provided.
    """

    try:
        import uopy  # type: ignore
    except Exception as exc:  # pragma: no cover
        raise UopyNotConfiguredError(
            "Failed to import 'uopy'. Install it in this environment first."
        ) from exc

    if select_override:
        cmd = select_override
    elif where_clause:
        cmd = f"SELECT {file_name} {where_clause}"
    else:
        cmd = f"SELECT {file_name}"

    # Execute the SELECT command — this populates the default select list.
    resp = uopy.Command(session=session)
    resp.run(cmd)

    select_list = uopy.List(session=session)
    # Read from the active (default) select list populated by the command.

    while True:
        rid = select_list.next()
        if rid is None:
            break
        yield _coerce_record_id(rid)


def read_record(session: Any, file_name: str, key: str) -> str:
    """Read and return a record by key.

    Return value should be the raw record body (string). Your comparison/parsing can be done elsewhere.
    """

    file_obj = _get_or_open_file(session, file_name)
    record = file_obj.read(key)
    # Keep return type stable for callers (string-ish). If you want structured
    # data, use uopy.DynArray downstream or switch sink to accept Any.
    coerced = _coerce_record_value(record)
    return coerced if isinstance(coerced, str) else str(coerced)


def read_records(session: Any, file_name: str, keys: list[str]) -> tuple[list[str], int]:
    """Batch read records (single RPC) for throughput.

    Returns: (records_as_strings, errors_count)
    The record list aligns with keys in order.
    """

    file_obj = _get_or_open_file(session, file_name)
    errors = 0

    try:
        response_codes, _status_codes, id_list, record_list = file_obj.read_records(keys)
    except Exception:
        # If uopy raises UOFileError, we still consider all keys failed here.
        return ([""] * len(keys), len(keys))

    # Some servers can reorder/normalize IDs. Prefer returned id_list if present.
    # However, we must keep alignment with input keys for downstream logic.
    # We'll map by index and ignore mismatches.
    out: list[str] = []
    for i in range(min(len(keys), len(record_list))):
        code = response_codes[i] if i < len(response_codes) else "0"
        if str(code) != "0":
            errors += 1
        coerced = _coerce_record_value(record_list[i])
        out.append(coerced if isinstance(coerced, str) else str(coerced))

    # If uopy returned fewer records than keys, pad.
    if len(out) < len(keys):
        errors += len(keys) - len(out)
        out.extend([""] * (len(keys) - len(out)))

    return out, errors
