import subprocess
import time
from dataclasses import dataclass
from typing import Any

from home_agent.core.logging import get_logger, get_logging_config


@dataclass
class LoggedSubprocessError(RuntimeError):
    command: list[str]
    returncode: int
    stdout: str
    stderr: str
    timeout_seconds: float | None = None

    def __post_init__(self) -> None:
        super().__init__(self._build_message())

    def _build_message(self) -> str:
        if self.timeout_seconds is not None:
            parts = [
                f"command timed out after {self.timeout_seconds:g} seconds: {' '.join(self.command)}"
            ]
        else:
            parts = [f"command failed with exit code {self.returncode}: {' '.join(self.command)}"]
        if self.stderr.strip():
            parts.append(self.stderr.strip())
        elif self.stdout.strip():
            parts.append(self.stdout.strip())
        return " | ".join(parts)


def run_logged_subprocess(
    command: list[str],
    *,
    logger_name: str,
    event: str,
    check: bool = False,
    timeout: float | None = None,
    raw_output: bool | None = None,
) -> subprocess.CompletedProcess[str]:
    logger = get_logger(logger_name)
    config = get_logging_config()
    capture_raw = config.capture_raw_payloads if raw_output is None else raw_output
    command_fields = _command_fields(command, capture_raw)

    event_start = f"{event}.start"
    logger.debug(
        event_start,
        subprocess_name=command[0] if command else "unknown",
        **command_fields,
    )
    started = time.perf_counter()
    try:
        proc = subprocess.run(
            command,
            check=False,
            capture_output=True,
            text=True,
            timeout=timeout,
        )
    except subprocess.TimeoutExpired as exc:
        duration_ms = round((time.perf_counter() - started) * 1000, 3)
        stdout = _coerce_output_text(exc.stdout)
        stderr = _coerce_output_text(exc.stderr)
        payload: dict[str, Any] = {
            "subprocess_name": command[0] if command else "unknown",
            "returncode": -1,
            "duration_ms": duration_ms,
            "timeout_seconds": timeout,
            "stdout_chars": len(stdout),
            "stderr_chars": len(stderr),
            **command_fields,
        }
        if capture_raw:
            payload["stdout"] = _preview(stdout, config.subprocess_preview_chars)
            payload["stderr"] = _preview(stderr, config.subprocess_preview_chars)
        event_timeout = f"{event}.timeout"
        logger.error(event_timeout, **payload)
        raise LoggedSubprocessError(
            command=command,
            returncode=-1,
            stdout=stdout,
            stderr=stderr,
            timeout_seconds=timeout,
        ) from exc
    duration_ms = round((time.perf_counter() - started) * 1000, 3)

    result_payload: dict[str, Any] = {
        "subprocess_name": command[0] if command else "unknown",
        "returncode": proc.returncode,
        "duration_ms": duration_ms,
        "stdout_chars": len(proc.stdout),
        "stderr_chars": len(proc.stderr),
        **command_fields,
    }
    if capture_raw:
        result_payload["stdout"] = _preview(proc.stdout, config.subprocess_preview_chars)
        result_payload["stderr"] = _preview(proc.stderr, config.subprocess_preview_chars)

    log_method = logger.error if proc.returncode != 0 else logger.debug
    event_completed = f"{event}.completed"
    log_method(event_completed, **result_payload)

    if check and proc.returncode != 0:
        raise LoggedSubprocessError(
            command=command,
            returncode=proc.returncode,
            stdout=proc.stdout,
            stderr=proc.stderr,
        )
    return proc


def _preview(value: str, limit: int) -> str:
    if len(value) <= limit:
        return value
    return value[:limit] + "...<truncated>"


def _command_fields(command: list[str], capture_raw: bool) -> dict[str, Any]:
    if capture_raw:
        return {"argv": command}
    return {
        "argv_summary": {
            "executable": command[0] if command else "unknown",
            "arg_count": max(len(command) - 1, 0),
        }
    }


def _coerce_output_text(value: object) -> str:
    if value is None:
        return ""
    if isinstance(value, bytes):
        return value.decode(errors="replace")
    return str(value)
