from __future__ import annotations

from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Literal


def _as_tuple_of_strings(value: tuple[str, ...] | list[str], field_name: str) -> tuple[str, ...]:
    if isinstance(value, str):
        raise TypeError(f"{field_name} must be a sequence of strings, not a string")
    return tuple(value)


def _as_tuple_of_output_chunks(
    value: tuple[ExecOutputChunk, ...] | list[ExecOutputChunk],
    field_name: str,
) -> tuple[ExecOutputChunk, ...]:
    if isinstance(value, str):
        raise TypeError(f"{field_name} must be a sequence of ExecOutputChunk values")
    normalized = tuple(value)
    if not all(isinstance(chunk, ExecOutputChunk) for chunk in normalized):
        raise TypeError(f"{field_name} must contain ExecOutputChunk values")
    return normalized


def _as_tuple_of_string_pairs(
    value: tuple[tuple[str, str], ...] | list[list[str] | tuple[str, str]],
    field_name: str,
) -> tuple[tuple[str, str], ...]:
    if isinstance(value, str):
        raise TypeError(f"{field_name} must be a sequence of string pairs, not a string")
    normalized: list[tuple[str, str]] = []
    for item in value:
        if isinstance(item, str):
            raise TypeError(f"{field_name} must contain 2-item string pairs")
        pair = tuple(item)
        if len(pair) != 2:
            raise TypeError(f"{field_name} must contain 2-item string pairs")
        if not all(isinstance(part, str) for part in pair):
            raise TypeError(f"{field_name} must contain 2-item string pairs")
        normalized.append((pair[0], pair[1]))
    return tuple(normalized)


@dataclass(frozen=True)
class ExecRequest:
    command: tuple[str, ...]
    cwd: str | None = None
    env: tuple[tuple[str, str], ...] = field(default_factory=tuple)
    timeout_seconds: float | None = None
    stdin: str | None = None
    preview_max_lines: int = 2000
    preview_max_bytes: int = 50 * 1024
    artifact_dir: str | None = None
    capture_full_output: bool = True
    rolling_max_bytes: int = 100 * 1024

    def __post_init__(self) -> None:
        object.__setattr__(self, "command", _as_tuple_of_strings(self.command, "command"))
        object.__setattr__(self, "env", _as_tuple_of_string_pairs(self.env, "env"))
        if not isinstance(self.rolling_max_bytes, int):
            raise TypeError("rolling_max_bytes must be an integer")
        if self.rolling_max_bytes < 1:
            raise ValueError("rolling_max_bytes must be >= 1")


@dataclass(frozen=True)
class ExecOutputChunk:
    stream: Literal["stdout", "stderr"]
    text: str


ExecUpdateCallback = Callable[[ExecOutputChunk], Awaitable[None] | None]


@dataclass(frozen=True)
class ExecResult:
    exit_code: int
    stdout: str = ""
    stderr: str = ""
    timed_out: bool = False
    cancelled: bool = False
    stdout_chunks: tuple[str, ...] = field(default_factory=tuple)
    stderr_chunks: tuple[str, ...] = field(default_factory=tuple)
    output_chunks: tuple[ExecOutputChunk, ...] = field(default_factory=tuple)
    stdout_preview: str = ""
    stderr_preview: str = ""
    stdout_truncated: bool = False
    stdout_truncated_by: str | None = None
    stderr_truncated: bool = False
    stderr_truncated_by: str | None = None
    stdout_artifact_path: str | None = None
    stderr_artifact_path: str | None = None
    stdout_total_lines: int | None = None
    stdout_total_bytes: int | None = None
    stderr_total_lines: int | None = None
    stderr_total_bytes: int | None = None

    def __post_init__(self) -> None:
        object.__setattr__(self, "stdout_chunks", _as_tuple_of_strings(self.stdout_chunks, "stdout_chunks"))
        object.__setattr__(self, "stderr_chunks", _as_tuple_of_strings(self.stderr_chunks, "stderr_chunks"))
        object.__setattr__(self, "output_chunks", _as_tuple_of_output_chunks(self.output_chunks, "output_chunks"))
