"""Fixed launcher for the managed Blender scene-recipe adapter."""

from __future__ import annotations

import json
import os
import re
import shutil
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

ADAPTER_VERSION = "1.0.0"
BLENDER_ENV = "ZCBOT_BLENDER_EXE"


def _atomic_json(path: Path, value: Any) -> None:
    temporary = path.with_name(path.name + ".tmp-" + os.urandom(8).hex())
    try:
        with temporary.open("w", encoding="utf-8", newline="\n") as handle:
            json.dump(value, handle, ensure_ascii=False, indent=2)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, path)
    finally:
        temporary.unlink(missing_ok=True)


def _candidate_paths() -> list[Path]:
    candidates: list[Path] = []
    configured = os.environ.get(BLENDER_ENV)
    if configured:
        candidates.append(Path(configured))
    discovered = shutil.which("blender.exe") or shutil.which("blender")
    if discovered:
        candidates.append(Path(discovered))
    for variable in ("ProgramFiles", "ProgramW6432"):
        root = os.environ.get(variable)
        if root:
            candidates.extend(
                sorted(
                    Path(root).glob("Blender Foundation/Blender */blender.exe"),
                    reverse=True,
                )
            )
    result: list[Path] = []
    seen: set[str] = set()
    for candidate in candidates:
        try:
            resolved = candidate.expanduser().resolve(strict=True)
        except (OSError, RuntimeError):
            continue
        key = str(resolved).casefold()
        if key not in seen and resolved.is_file():
            seen.add(key)
            result.append(resolved)
    return result


def _version(executable: Path) -> str:
    result = subprocess.run(
        [str(executable), "--version"],
        check=False,
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
        timeout=7,
    )
    output = result.stdout + "\n" + result.stderr
    match = re.search(r"Blender\s+([0-9]+(?:\.[0-9]+){1,2})", output)
    if result.returncode != 0 or match is None:
        raise RuntimeError("Blender version probe failed")
    return match.group(1)


def _find_blender() -> tuple[Path, str]:
    failures: list[str] = []
    for candidate in _candidate_paths():
        try:
            return candidate, _version(candidate)
        except (OSError, subprocess.SubprocessError, RuntimeError) as exc:
            failures.append(f"{candidate.name}: {exc}")
    detail = "; ".join(failures) if failures else "Blender installation was not found"
    raise FileNotFoundError(detail)


def _record(job_dir: Path) -> dict[str, Any]:
    try:
        value = json.loads(
            (job_dir / "request" / "request.json").read_text(encoding="utf-8")
        )
        return value if isinstance(value, dict) else {}
    except (OSError, json.JSONDecodeError):
        return {}


def _failure(job_dir: Path, code: str, detail: str) -> None:
    record = _record(job_dir)
    _atomic_json(
        job_dir / "terminal.json",
        {
            "job_id": record.get("job_id", ""),
            "lease_id": record.get("lease_id", ""),
            "request_digest": record.get("request_digest", ""),
            "status": "failed",
            "error": {"code": code, "detail": detail[:500]},
            "artifact_manifest": [],
            "terminal_at": datetime.now(timezone.utc).isoformat(),
        },
    )


def probe() -> int:
    try:
        executable, software_version = _find_blender()
        health = "ready"
        detail = f"Blender background executable detected: {executable}"
    except (FileNotFoundError, OSError, subprocess.SubprocessError) as exc:
        software_version = None
        health = "unavailable"
        detail = str(exc)
    print(
        json.dumps(
            {
                "adapter_version": ADAPTER_VERSION,
                "software": "Blender",
                "software_version": software_version,
                "health": health,
                "detail": detail,
            },
            ensure_ascii=False,
        )
    )
    return 0


def run(job_dir: Path) -> int:
    job_dir = job_dir.resolve(strict=True)
    script = Path(__file__).with_name("blender_worker.py").resolve(strict=True)
    try:
        executable, _ = _find_blender()
        result = subprocess.run(
            [
                str(executable),
                "--background",
                "--factory-startup",
                "--disable-autoexec",
                "--python-exit-code",
                "1",
                "--python",
                str(script),
                "--",
                str(job_dir),
            ],
            check=False,
        )
        if not (job_dir / "terminal.json").is_file():
            _failure(
                job_dir,
                "BLENDER_NO_TERMINAL",
                f"Blender exited with code {result.returncode} without terminal.json",
            )
        return result.returncode
    except (FileNotFoundError, OSError, subprocess.SubprocessError) as exc:
        _failure(job_dir, "BLENDER_START_FAILED", str(exc))
        print(f"[ERR] {type(exc).__name__}: {exc}", file=sys.stderr)
        return 1


def main() -> int:
    if sys.argv[1:] == ["--probe"]:
        return probe()
    if len(sys.argv) != 2:
        print("[ERR] Usage: worker.py <job-directory>", file=sys.stderr)
        return 2
    return run(Path(sys.argv[1]))


if __name__ == "__main__":
    raise SystemExit(main())
