from __future__ import annotations

from pathlib import Path
from typing import Any, Callable

from .parallel_read import RunOptions, WorkerResult, run_parallel_read


def _callable_to_spec(factory: Callable[..., Any]) -> str:
    """Convert a top-level callable to an import spec string.

    Args:
        factory: A top-level (module-scope) callable.

    Returns:
        A string in the form "module.sub:callable_name".

    Notes:
        This is required to work reliably with multiprocessing("spawn"). Nested
        or local functions (e.g., closures) cannot be imported in worker
        processes, so they must be passed as a spec string instead.
    """

    module = getattr(factory, "__module__", None)
    qualname = getattr(factory, "__qualname__", None)
    if not module or not qualname:
        raise ValueError("session_factory must be a top-level function or provide a spec string")
    if "<locals>" in qualname:
        raise ValueError(
            "session_factory cannot be a nested/local function when using multiprocessing; "
            "pass a 'module:callable' spec instead"
        )

    # Only support direct attribute names (no dotted qualname) for clarity.
    name = getattr(factory, "__name__", None)
    if not name:
        raise ValueError("session_factory must have a __name__")

    return f"{module}:{name}"


def run(
    *,
    file_name: str,
    session_factory: str | Callable[..., Any] | None = None,
    sessions: int = 5,
    count_only: bool = True,
    output_dir: str | Path | None = None,
    batch_size: int = 500,
    progress_every_seconds: float = 5.0,
    max_records: int | None = None,
    dry_run: int | None = None,
    dry_run_data: dict[str, Any] | None = None,
    pooling_on: bool | None = None,
    min_pool_size: int | None = None,
    max_pool_size: int | None = None,
    retries: int = 3,
    retry_backoff: float = 0.5,
    output_format: str = "ndjson",
    stream: bool = False,
    where_clause: str | None = None,
    select_override: str | None = None,
    partition: str | None = None,
) -> list[WorkerResult]:
    """Run a parallel UniVerse read.

    This is the importable equivalent of the CLI.

    Args:
        file_name:
            UniVerse file name to read keys/records from.
            Ignored when ``dry_run`` is set.

        session_factory:
            How to create a new ``uopy`` Session per process.

            Accepted forms:
            - ``"module.sub:make_session"`` (recommended; multiprocessing-safe)
            - a top-level Python callable (will be converted to a spec string)
            - ``None`` (allowed only when ``dry_run`` is set)

        sessions: Number of concurrent sessions/processes (default 5).
        count_only: If True, only count records — do not write them.
        output_dir: Write one file per worker into this directory.
        batch_size: Keys per IPC batch to each worker.
        progress_every_seconds: Seconds between throughput log lines.
        max_records: Debug cap (per main-process shard).
        dry_run: Generate N fake keys instead of connecting to UniVerse.
        dry_run_data: ``{key: record}`` fixtures for dry-run.
        pooling_on / min_pool_size / max_pool_size: Pool hints.
        retries: Retries per failed batch/key (default 3).
        retry_backoff: Base delay between retries (default 0.5s).
        output_format: ``"ndjson"`` | ``"jsonl"`` | ``"csv"`` | ``"parquet"``.
        stream: Stream records to stdout instead of writing files.
        where_clause: UniVerse WHERE/WITH clause, e.g. ``"WITH STATUS = 'A'"``.
        select_override: Full UniVerse SELECT statement override.
        partition: Key partitioning strategy: ``"prefix"`` | ``"hash"`` | ``None``.

    Returns:
        A list of :class:`WorkerResult`, one per session/process.
    """

    if dry_run is not None and session_factory is None:
        session_factory_spec = None
    else:
        if session_factory is None:
            raise ValueError("session_factory is required unless dry_run is set")

        if isinstance(session_factory, str):
            session_factory_spec = session_factory
        else:
            session_factory_spec = _callable_to_spec(session_factory)

    kwargs: dict[str, Any] = {}
    if pooling_on is not None:
        kwargs["pooling_on"] = pooling_on
    if min_pool_size is not None:
        kwargs["min_pool_size"] = int(min_pool_size)
    if max_pool_size is not None:
        kwargs["max_pool_size"] = int(max_pool_size)

    options = RunOptions(
        sessions=int(sessions),
        count_only=bool(count_only),
        output_dir=Path(output_dir) if output_dir is not None else None,
        batch_size=int(batch_size),
        progress_every_seconds=float(progress_every_seconds),
        max_records=max_records,
        dry_run=dry_run,
        session_factory_kwargs=kwargs,
        dry_run_data=dry_run_data or {},
        retries=int(retries),
        retry_backoff=float(retry_backoff),
        output_format=output_format,
        stream=stream,
        where_clause=where_clause,
        select_override=select_override,
        partition=partition,
    )

    return run_parallel_read(
        file_name=file_name,
        session_factory_spec=session_factory_spec,
        options=options,
    )


__all__ = ["run", "RunOptions", "WorkerResult"]
