"""Deterministic opt-in threaded tooling helpers (stdlib-only).

Default behavior remains single-threaded. Callers only opt into threaded
execution when they pass ``workers > 1``.
"""

from __future__ import annotations

from collections.abc import Callable, Iterable, Mapping, Sequence
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from functools import partial
from pathlib import Path
from typing import Any

from icsd.core.batch import BatchReport, BatchStateResult, validate_states
from icsd.core.invariants import InvariantSet

__all__ = ["ParallelLintResult", "lint_targets_parallel", "validate_states_parallel"]

LintFileCallable = Callable[[Path], Sequence[Any]]


@dataclass(frozen=True, slots=True)
class ParallelLintResult:
    """Deterministic per-target lint result envelope."""

    index: int
    path: Path
    findings: tuple[Any, ...] = ()
    error: str | None = None

    def __post_init__(self) -> None:
        if not isinstance(self.index, int) or self.index < 0:
            msg = "index must be an integer greater than or equal to zero."
            raise ValueError(msg)
        if not isinstance(self.path, Path):
            msg = "path must be a pathlib.Path instance."
            raise TypeError(msg)
        if self.error is not None and (not isinstance(self.error, str) or not self.error.strip()):
            msg = "error must be a non-empty string when provided."
            raise TypeError(msg)
        if self.error is not None and self.findings:
            msg = "error results must not include findings."
            raise ValueError(msg)


def validate_states_parallel(
    invariant_set: InvariantSet,
    states: Iterable[Any],
    *,
    context: Mapping[str, Any] | None = None,
    workers: int = 1,
) -> BatchReport:
    """Validate states with deterministic ordering and optional threaded execution.

    ``workers=1`` preserves the existing sequential ``validate_states(...)``
    path. ``workers>1`` uses ``ThreadPoolExecutor`` while preserving the input
    order of ``BatchStateResult`` items.
    """
    if not isinstance(invariant_set, InvariantSet):
        msg = "invariant_set must be an InvariantSet instance."
        raise TypeError(msg)
    if isinstance(states, (str, bytes, bytearray)):
        msg = "states must be an iterable of state values."
        raise TypeError(msg)
    if context is not None and not isinstance(context, Mapping):
        msg = "context must be a mapping when provided."
        raise TypeError(msg)
    _validate_worker_count(workers)

    if workers == 1:
        return validate_states(invariant_set, states, context=context)

    indexed_states = list(enumerate(states))
    validator = partial(_validate_indexed_state, invariant_set=invariant_set)
    with ThreadPoolExecutor(max_workers=workers) as executor:
        results = tuple(executor.map(validator, indexed_states))
    return BatchReport(results=results, context=context)


def lint_targets_parallel(
    targets: Sequence[Path],
    *,
    lint_file: LintFileCallable,
    workers: int = 1,
) -> tuple[ParallelLintResult, ...]:
    """Run lint target processing with deterministic ordering.

    ``workers=1`` stays on the current single-threaded path. ``workers>1`` uses
    ``ThreadPoolExecutor`` while preserving target order and the existing
    stable error envelope shape.
    """
    if not isinstance(targets, Sequence):
        msg = "targets must be a sequence of Path values."
        raise TypeError(msg)
    if not callable(lint_file):
        msg = "lint_file must be callable."
        raise TypeError(msg)
    _validate_worker_count(workers)

    indexed_targets: list[tuple[int, Path]] = []
    for index, target in enumerate(targets):
        if not isinstance(target, Path):
            msg = "targets must contain only Path values."
            raise TypeError(msg)
        indexed_targets.append((index, target))

    runner = partial(_lint_indexed_target, lint_file=lint_file)
    if workers == 1:
        return tuple(runner(item) for item in indexed_targets)
    with ThreadPoolExecutor(max_workers=workers) as executor:
        return tuple(executor.map(runner, indexed_targets))


def _validate_indexed_state(
    indexed_state: tuple[int, Any],
    *,
    invariant_set: InvariantSet,
) -> BatchStateResult:
    index, state = indexed_state
    report = validate_states(invariant_set, [state])
    item_result = report.results[0]
    return BatchStateResult(
        index=index,
        valid=item_result.valid,
        failures=item_result.failures,
    )


def _lint_indexed_target(
    indexed_target: tuple[int, Path],
    *,
    lint_file: LintFileCallable,
) -> ParallelLintResult:
    index, path = indexed_target
    try:
        findings = tuple(lint_file(path))
    except (OSError, UnicodeDecodeError) as exc:
        return ParallelLintResult(
            index=index,
            path=path,
            error=f"unable to read {path}: {exc}.",
        )
    return ParallelLintResult(index=index, path=path, findings=findings)


def _validate_worker_count(workers: int) -> None:
    if isinstance(workers, bool) or not isinstance(workers, int) or workers < 1:
        msg = "workers must be an integer greater than or equal to 1."
        raise ValueError(msg)
