#!/usr/bin/env python3
"""SearXNG / Colima install sanity check (onboarding local-search path).

Pins the docker.sock permission-denied race that surfaced during first-run
setup when Colima's socket was not ready yet and the bundled Docker CLI fell
back to /var/run/docker.sock.

Runs three layers:

1. Simulated race — socket appears late; docker.sock permission denied once,
   then succeeds on retry (the wife's-machine failure mode).
2. Live ``run_setup()`` — full Colima + SearXNG bring-up.
3. HTTP probe — JSON search against the local instance.

Optional ``--cold`` stops Colima first (closest to a never-run-before Mac).

Apple Silicon only. Requires network for image pull on a truly fresh machine.

Usage:
    python scripts/searxng_sanity.py
    python scripts/searxng_sanity.py --cold

Exit 0 + ``SEARXNG_SANITY_OK`` on success; non-zero with a short cause on failure.
"""

from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request
from unittest.mock import patch

REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if REPO_ROOT not in sys.path:
    sys.path.insert(0, REPO_ROOT)


def _fail(stage: str, msg: str) -> int:
    print(f"FAIL: {stage}: {msg}", file=sys.stderr)
    return 1


def _check_simulated_race() -> int:
    from tools import searxng_docker

    denied = subprocess.CompletedProcess(
        args=["docker", "info"],
        returncode=1,
        stdout="",
        stderr=(
            "permission denied while trying to connect to the Docker daemon socket "
            "at unix:///var/run/docker.sock: dial unix /var/run/docker.sock: "
            "connect: permission denied"
        ),
    )
    ready = subprocess.CompletedProcess(
        args=["docker", "info"],
        returncode=0,
        stdout="Server Version: 27.0.0",
        stderr="",
    )
    fake_env = {"PATH": os.environ.get("PATH", "")}
    socket_checks = [False, False, True]

    def exists_side_effect(path):
        if path == searxng_docker._COLIMA_DOCKER_SOCKET:
            return socket_checks.pop(0) if socket_checks else True
        return False

    with patch("tools.searxng_docker.get_docker_path", return_value="/tmp/docker"), \
         patch("tools.searxng_docker._docker_env_for_colima", return_value=fake_env), \
         patch("tools.searxng_docker.os.path.exists", side_effect=exists_side_effect), \
         patch("tools.searxng_docker.subprocess.run", side_effect=[denied, ready]) as run_mock, \
         patch("time.sleep"):
        ok, msg = searxng_docker.ensure_docker_access_via_colima(max_attempts=5, poll_seconds=0)

    if not ok:
        return _fail("simulated_race", msg)
    if run_mock.call_count != 2:
        return _fail("simulated_race", f"expected 2 docker probes, got {run_mock.call_count}")
    print("PASS: simulated docker.sock permission race retried successfully")
    return 0


def _check_socket_wait() -> int:
    from tools import searxng_docker

    with patch("tools.searxng_docker.os.path.exists", side_effect=[False, False, True]), \
         patch("time.sleep"):
        ok, msg = searxng_docker._wait_for_colima_socket(timeout_seconds=5, poll_seconds=0.01)
    if not ok:
        return _fail("socket_wait", msg)
    print(f"PASS: {msg}")
    return 0


def _maybe_cold_stop() -> None:
    from tools.searxng_docker import SEARXNG_CONTAINER_NAME, _run_colima, _run_docker

    print("[searxng-sanity] cold: stopping SearXNG container and Colima...")
    _run_docker(["stop", SEARXNG_CONTAINER_NAME], timeout=30)
    _run_colima(["stop"], timeout=90)
    from tools.searxng_docker import _COLIMA_DOCKER_SOCKET

    for _ in range(20):
        if not os.path.exists(_COLIMA_DOCKER_SOCKET):
            break
        time.sleep(1)
    print(f"[searxng-sanity] cold: colima_socket_gone={not os.path.exists(_COLIMA_DOCKER_SOCKET)}")


def _check_live_setup(*, cold: bool) -> int:
    from tools.searxng_docker import _COLIMA_DOCKER_SOCKET, run_setup, searxng_container_status

    if cold:
        _maybe_cold_stop()

    t0 = time.time()
    ok, msg = run_setup()
    elapsed = time.time() - t0
    lower = (msg or "").lower()
    if not ok:
        return _fail("live_setup", msg)
    if "permission denied" in lower or (
        "docker.sock" in lower and "colima" not in lower
    ):
        return _fail(
            "live_setup",
            f"docker.sock permission issue still present after {elapsed:.1f}s: {msg}",
        )
    if searxng_container_status() != "running":
        return _fail("live_setup", f"container not running after setup ({elapsed:.1f}s)")
    if not os.path.exists(_COLIMA_DOCKER_SOCKET):
        return _fail("live_setup", "Colima socket missing after successful setup")
    print(f"PASS: run_setup ok in {elapsed:.1f}s — {msg}")
    return 0


def _check_http_search(port: int = 8888) -> int:
    url = f"http://127.0.0.1:{port}/search?q=mugi&format=json"
    try:
        with urllib.request.urlopen(url, timeout=15) as resp:
            data = json.loads(resp.read().decode("utf-8", errors="replace"))
    except urllib.error.URLError as exc:
        return _fail("http_search", str(exc))
    except Exception as exc:
        return _fail("http_search", str(exc))
    if not isinstance(data, dict) or "results" not in data:
        return _fail("http_search", "unexpected JSON response from SearXNG")
    print(f"PASS: HTTP search returned {len(data['results'])} results")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="SearXNG/Colima install sanity check.")
    parser.add_argument(
        "--cold",
        action="store_true",
        help="stop Colima first, then run setup (slower; closer to first onboarding)",
    )
    args = parser.parse_args()

    from tools.docker_binaries import require_apple_silicon

    ok, msg = require_apple_silicon()
    if not ok:
        return _fail("platform", msg)

    for check in (_check_simulated_race, _check_socket_wait):
        code = check()
        if code != 0:
            return code

    code = _check_live_setup(cold=args.cold)
    if code != 0:
        return code

    code = _check_http_search()
    if code != 0:
        return code

    print("SEARXNG_SANITY_OK")
    return 0


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