"""Bounded desktop screenshot capture for Avatar vision (0.2.2)."""

from __future__ import annotations

import hashlib
import os
import shutil
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Callable

MAX_WIDTH = 1920
MAX_HEIGHT = 1080
MAX_FILE_BYTES = 2 * 1024 * 1024
DEFAULT_EVIDENCE_ROOT = Path.home() / ".local" / "share" / "iuve-connect" / "evidence"


@dataclass(frozen=True)
class ScreenshotArtifact:
    kind: str
    path: str
    sha256: str
    size_bytes: int
    mime: str
    redacted: bool
    display_server: str
    capture_backend: str

    def as_dict(self) -> dict[str, object]:
        return {
            "kind": self.kind,
            "path": self.path,
            "sha256": self.sha256,
            "size_bytes": self.size_bytes,
            "mime": self.mime,
            "redacted": self.redacted,
            "display_server": self.display_server,
            "capture_backend": self.capture_backend,
        }


def detect_display_server() -> str:
    if os.environ.get("WAYLAND_DISPLAY"):
        return "wayland"
    if os.environ.get("DISPLAY"):
        return "x11"
    return "unknown"


def evidence_dir_for_step(*, plan_id: str, step_id: str, root: Path | None = None) -> Path:
    base = root or DEFAULT_EVIDENCE_ROOT
    safe_plan = _safe_segment(plan_id or "unknown-plan")
    safe_step = _safe_segment(step_id or "step")
    return base / safe_plan / safe_step


def capture_screenshot(
    *,
    plan_id: str,
    step_id: str,
    region: str = "full",
    delay_ms: int = 0,
    evidence_root: Path | None = None,
    run_subprocess: Callable[..., subprocess.CompletedProcess[str]] | None = None,
) -> ScreenshotArtifact:
    """Capture one bounded JPEG frame for a plan step."""
    del region  # active_window deferred to 0.2.3
    if delay_ms > 0:
        time.sleep(min(delay_ms, 3000) / 1000.0)

    display_server = detect_display_server()
    target_dir = evidence_dir_for_step(plan_id=plan_id, step_id=step_id, root=evidence_root)
    target_dir.mkdir(parents=True, exist_ok=True)
    output_path = target_dir / f"{_safe_segment(step_id)}.jpg"

    runner = run_subprocess or subprocess.run
    backend, raw_path = _capture_raw_image(display_server=display_server, output_path=output_path, runner=runner)
    final_path = _normalize_image(raw_path, output_path=output_path, runner=runner)
    data = final_path.read_bytes()
    if len(data) > MAX_FILE_BYTES:
        raise ValueError("screenshot_exceeds_max_size")

    digest = hashlib.sha256(data).hexdigest()
    return ScreenshotArtifact(
        kind="screenshot",
        path=str(final_path),
        sha256=digest,
        size_bytes=len(data),
        mime="image/jpeg",
        redacted=False,
        display_server=display_server,
        capture_backend=backend,
    )


def _safe_segment(value: str) -> str:
    cleaned = "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "_" for ch in str(value).strip())
    return cleaned or "step"


def _capture_raw_image(
    *,
    display_server: str,
    output_path: Path,
    runner: Callable[..., subprocess.CompletedProcess[str]],
) -> tuple[str, Path]:
    errors: list[str] = []

    # Phase B: prefer OS-native backends on Windows / macOS.
    try:
        from ..platform_adapters.screenshot_backends import capture_raw_for_platform

        native = capture_raw_for_platform(output_path, runner=runner)
        if native is not None:
            return native
    except Exception as exc:  # noqa: BLE001
        errors.append(f"platform:{exc}")

    for name, fn in (
        ("mss", lambda: _capture_mss(output_path)),
        ("grim", lambda: _capture_grim(output_path, runner=runner)),
        ("gnome-screenshot", lambda: _capture_gnome_screenshot(output_path, runner=runner)),
        ("import", lambda: _capture_import(output_path, runner=runner)),
    ):
        if name == "grim" and display_server != "wayland" and not shutil.which("grim"):
            continue
        if name == "import" and display_server != "x11":
            continue
        try:
            path = fn()
            if path.is_file() and path.stat().st_size > 0:
                return name, path
        except Exception as exc:  # noqa: BLE001 — collect adapter errors
            errors.append(f"{name}:{exc}")
    detail = "; ".join(errors) if errors else "no_capture_backend"
    raise RuntimeError(f"screenshot_capture_failed:{detail}")


def _capture_mss(output_path: Path) -> Path:
    import mss  # optional dependency

    png_path = output_path.with_suffix(".png")
    with mss.mss() as grabber:
        monitor = grabber.monitors[0]
        shot = grabber.grab(monitor)
        mss.tools.to_png(shot.rgb, shot.size, output=str(png_path))
    return png_path


def _capture_grim(output_path: Path, *, runner: Callable[..., subprocess.CompletedProcess[str]]) -> Path:
    grim = shutil.which("grim")
    if not grim:
        raise FileNotFoundError("grim_not_found")
    png_path = output_path.with_suffix(".png")
    completed = runner([grim, str(png_path)], capture_output=True, text=True, check=False)
    if completed.returncode != 0:
        raise RuntimeError(completed.stderr.strip() or "grim_failed")
    return png_path


def _capture_gnome_screenshot(
    output_path: Path,
    *,
    runner: Callable[..., subprocess.CompletedProcess[str]],
) -> Path:
    tool = shutil.which("gnome-screenshot")
    if not tool:
        raise FileNotFoundError("gnome-screenshot_not_found")
    png_path = output_path.with_suffix(".png")
    completed = runner([tool, "-f", str(png_path)], capture_output=True, text=True, check=False)
    if completed.returncode != 0:
        raise RuntimeError(completed.stderr.strip() or "gnome_screenshot_failed")
    return png_path


def _capture_import(output_path: Path, *, runner: Callable[..., subprocess.CompletedProcess[str]]) -> Path:
    tool = shutil.which("import")
    if not tool:
        raise FileNotFoundError("import_not_found")
    png_path = output_path.with_suffix(".png")
    completed = runner([tool, "-window", "root", str(png_path)], capture_output=True, text=True, check=False)
    if completed.returncode != 0:
        raise RuntimeError(completed.stderr.strip() or "import_failed")
    return png_path


def _normalize_image(
    source_path: Path,
    *,
    output_path: Path,
    runner: Callable[..., subprocess.CompletedProcess[str]],
) -> Path:
    convert = shutil.which("convert")
    if convert:
        completed = runner(
            [
                convert,
                str(source_path),
                "-resize",
                f"{MAX_WIDTH}x{MAX_HEIGHT}>",
                "-quality",
                "85",
                str(output_path),
            ],
            capture_output=True,
            text=True,
            check=False,
        )
        if completed.returncode == 0 and output_path.is_file():
            if source_path != output_path and source_path.exists():
                source_path.unlink(missing_ok=True)
            return output_path

    if source_path.suffix.lower() == ".jpg":
        if source_path != output_path:
            output_path.write_bytes(source_path.read_bytes())
        return output_path

    if source_path.suffix.lower() == ".png" and shutil.which("ffmpeg"):
        completed = runner(
            [
                shutil.which("ffmpeg") or "ffmpeg",
                "-y",
                "-i",
                str(source_path),
                "-vf",
                f"scale='min({MAX_WIDTH},iw)':'min({MAX_HEIGHT},ih)':force_original_aspect_ratio=decrease",
                str(output_path),
            ],
            capture_output=True,
            text=True,
            check=False,
        )
        if completed.returncode == 0 and output_path.is_file():
            source_path.unlink(missing_ok=True)
            return output_path

    # Last resort: copy bytes (tests / headless stub).
    data = source_path.read_bytes()
    output_path.write_bytes(data)
    return output_path
