"""Doctor command implementation for MindRoom CLI."""

from __future__ import annotations

import ipaddress
import os
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING
from urllib.parse import urlparse

import httpx
import typer

from mindroom import constants
from mindroom.constants import RuntimePaths, env_key_for_provider, runtime_env_path
from mindroom.credentials_sync import sync_env_to_credentials
from mindroom.embedder_health import probe_embedder, semantic_embedder_configured
from mindroom.embedding_errors import EMBEDDER_UNREACHABLE_DETAIL
from mindroom.embeddings import create_sentence_transformers_embedder
from mindroom.matrix.health import (
    MSC4186_UNSTABLE_FEATURE,
    matrix_versions_url,
    response_advertises_sliding_sync,
    response_has_matrix_versions,
)
from mindroom.model_defaults import OLLAMA_HOST_DEFAULT
from mindroom.runtime_env_policy import VERTEXAI_CLAUDE_ENV_BY_KEY
from mindroom.startup_errors import PermanentStartupError

from .config import activate_cli_runtime, console, load_config_quiet

if TYPE_CHECKING:
    from collections.abc import Callable

    from anthropic import APIStatusError
    from google.auth.exceptions import DefaultCredentialsError, RefreshError

    from mindroom.config.models import ModelConfig

from mindroom.config.main import CONFIG_LOAD_USER_ERROR_TYPES, Config, iter_config_validation_messages


def doctor(config_path: Path | None = None, storage_path: Path | None = None) -> None:
    """Check your environment for common issues.

    Runs connectivity, configuration, and credential checks in a single pass
    so you can fix everything before running `mindroom run`.
    """
    console.print("[bold]MindRoom Doctor[/bold]\n")

    passed = 0
    failed = 0
    warnings = 0

    runtime_paths = activate_cli_runtime(path=config_path, storage_path=storage_path)
    config_path = runtime_paths.config_path
    console.print(f"[dim]Config directory: {runtime_paths.config_dir}[/dim]")

    # Resolve credentials the way `mindroom run` will: seed/update env-backed
    # services (EMBEDDER_API_KEY, provider keys, ...) into the shared store so
    # preflight probes validate the keys the runtime will actually use.
    # Credential-store failures must not abort the remaining diagnostics, but
    # they are counted because the general storage check may target another path.
    try:
        sync_env_to_credentials(runtime_paths=runtime_paths)
    except (OSError, ValueError) as exc:
        console.print(f"[yellow]![/yellow] Could not sync env credentials into the store ({exc})")
        warnings += 1

    # 1. Config file exists
    p, f, w = _run_doctor_step("Checking config file...", lambda: _check_config_exists(config_path))
    passed += p
    failed += f
    warnings += w

    # 2+. Config validity + provider API key validation (skip if file missing)
    config: Config | None = None
    if config_path.is_file():
        config, p, f, w = _run_doctor_step(
            "Validating configuration...",
            lambda: _check_config_valid(runtime_paths),
        )
        passed += p
        failed += f
        warnings += w
        if config is not None:
            p, f, w = _run_doctor_step(
                "Checking providers...",
                lambda: _check_providers(config, runtime_paths=runtime_paths),
            )
            passed += p
            failed += f
            warnings += w

            # 4. Memory LLM & embedder
            p, f, w = _run_doctor_step(
                "Checking memory config...",
                lambda: _check_memory_config(config, runtime_paths=runtime_paths),
            )
            passed += p
            failed += f
            warnings += w

    # 5. Matrix homeserver reachable
    p, f, w = _run_doctor_step(
        "Checking Matrix homeserver...",
        lambda: _check_matrix_homeserver(runtime_paths=runtime_paths, config=config),
    )
    passed += p
    failed += f
    warnings += w

    # 6. Storage directory writable
    p, f, w = _run_doctor_step("Checking storage...", lambda: _check_storage_writable(runtime_paths))
    passed += p
    failed += f
    warnings += w

    # 7. Matrix encryption stores match persisted device identities
    p, f, w = _run_doctor_step("Checking encryption stores...", lambda: _check_e2ee_stores(runtime_paths))
    passed += p
    failed += f
    warnings += w

    # Summary
    console.print(f"\n{passed} passed, {failed} failed, {warnings} warning{'s' if warnings != 1 else ''}")

    if failed > 0:
        raise typer.Exit(1)


def _check_e2ee_stores(runtime_paths: RuntimePaths) -> tuple[int, int, int]:
    """Check persisted Matrix devices have their encryption stores on disk."""
    from mindroom.matrix.client_session import olm_store_exists  # noqa: PLC0415
    from mindroom.matrix.state import MatrixState  # noqa: PLC0415

    state = MatrixState.load(runtime_paths=runtime_paths)
    accounts_with_devices = [
        (key, account) for key, account in state.accounts.items() if account.device_id and account.domain
    ]
    if not accounts_with_devices:
        console.print("[green]✓[/green] Encryption stores: no persisted Matrix devices yet")
        return 1, 0, 0

    missing = [
        (key, account)
        for key, account in accounts_with_devices
        if not olm_store_exists(f"@{account.username}:{account.domain}", account.device_id or "", runtime_paths)
    ]
    if not missing:
        count = len(accounts_with_devices)
        console.print(f"[green]✓[/green] Encryption stores: {count} device store{'s' if count != 1 else ''} present")
        return 1, 0, 0

    for key, account in missing:
        console.print(
            f"[yellow]⚠[/yellow] Encryption store missing for {key} "
            f"(@{account.username}:{account.domain}, device {account.device_id}); "
            "MindRoom will log in as a fresh device on next start and old encrypted messages stay unreadable",
        )
    return 0, 0, len(missing)


def _run_doctor_step[T](message: str, check: Callable[[], T]) -> T:
    """Run one doctor step with a minimal terminal spinner."""
    with console.status(f"[dim]{message}[/dim]", spinner="dots"):
        return check()


def _check_config_exists(config_path: Path) -> tuple[int, int, int]:
    """Check config file exists. Returns (passed, failed, warnings)."""
    if config_path.is_file():
        console.print(f"[green]✓[/green] Config file: {config_path}")
        return 1, 0, 0
    if config_path.exists():
        console.print(f"[red]✗[/red] Config path is not a file: {config_path}")
        return 0, 1, 0
    console.print(f"[red]✗[/red] Config file not found: {config_path}")
    return 0, 1, 0


def _check_config_valid(runtime_paths: RuntimePaths) -> tuple[Config | None, int, int, int]:
    """Validate config file. Returns (config_or_none, passed, failed, warnings)."""
    try:
        config = load_config_quiet(runtime_paths=runtime_paths)
    except CONFIG_LOAD_USER_ERROR_TYPES as exc:
        issues = "; ".join(f"{location}: {message}" for location, message in iter_config_validation_messages(exc))
        console.print(f"[red]✗[/red] Config invalid: {issues}")
        return None, 0, 1, 0
    agents = len(config.agents)
    teams = len(config.teams)
    models = len(config.models)
    rooms = len(config.get_all_configured_rooms())
    console.print(
        f"[green]✓[/green] Config valid"
        f" ({agents} agent{'s' if agents != 1 else ''},"
        f" {teams} team{'s' if teams != 1 else ''},"
        f" {models} model{'s' if models != 1 else ''},"
        f" {rooms} room{'s' if rooms != 1 else ''})",
    )
    return config, 1, 0, 0


_PROVIDER_VALIDATE_URLS: dict[str, str] = {
    "anthropic": "https://api.anthropic.com/v1/models",
    "openai": "https://api.openai.com/v1/models",
    "google": "https://generativelanguage.googleapis.com/v1beta/models",
    "openrouter": "https://openrouter.ai/api/v1/models",
    "deepseek": "https://api.deepseek.com/v1/models",
    "cerebras": "https://api.cerebras.ai/v1/models",
    "groq": "https://api.groq.com/openai/v1/models",
    # No "zai" entry: Z.ai's paas/v4 OpenAPI spec exposes no GET /models
    # listing endpoint, so a URL probe would misreport valid keys as broken.
}


def _get_custom_base_url(config: Config, provider: str) -> str | None:
    """Get custom base_url for a provider from model extra_kwargs, if any."""
    for model in config.models.values():
        if model.provider == provider and model.extra_kwargs:
            base_url = model.extra_kwargs.get("base_url")
            if base_url:
                return base_url
    return None


def _http_check(
    url: str,
    headers: dict[str, str] | None = None,
    *,
    verify: bool = True,
) -> tuple[bool | None, str]:
    """Make a lightweight GET request and return (True, ""), (False, reason), or (None, reason)."""
    try:
        resp = httpx.get(url, headers=headers or {}, timeout=5, verify=verify)
    except httpx.HTTPError as exc:
        return None, str(exc)
    if resp.is_success:
        return True, ""
    return False, f"HTTP {resp.status_code}"


def _is_local_network_host(host: str) -> bool:
    """Return True for .local, loopback, and private-link hosts."""
    normalized_host = host.strip().strip("[]").lower()
    if normalized_host in {"localhost", "127.0.0.1", "::1"} or normalized_host.endswith(".local"):
        return True
    try:
        ip = ipaddress.ip_address(normalized_host)
    except ValueError:
        return False
    return ip.is_private or ip.is_loopback or ip.is_link_local


def _with_local_network_hint(detail: str, base_url: str | None) -> str:
    """Append a targeted hint for local host routing failures."""
    if not detail or not base_url:
        return detail

    parsed = urlparse(base_url)
    host = parsed.hostname
    if host is None or not _is_local_network_host(host):
        return detail

    lowered = detail.lower()
    route_signals = (
        "no route to host",
        "connection refused",
        "name or service not known",
        "nodename nor servname provided",
        EMBEDDER_UNREACHABLE_DETAIL,
    )
    if not any(signal in lowered for signal in route_signals):
        return detail

    return (
        f"{detail}; local host '{host}' may be unreachable from this Python runtime"
        " (try a reachable LAN IP instead of .local)"
    )


def _validate_provider_key(
    provider: str,
    api_key: str,
    base_url: str | None = None,
) -> tuple[bool | None, str]:
    """Validate an API key with a lightweight models-list request.

    Returns (True, "") if valid, (False, reason) if invalid,
    (None, reason) if inconclusive (e.g. connection error).
    """
    # Normalize aliases so we look up a single URL and auth style
    canonical = "google" if provider == "gemini" else provider

    if base_url:
        url = base_url.rstrip("/") + "/models"
    elif canonical in _PROVIDER_VALIDATE_URLS:
        url = _PROVIDER_VALIDATE_URLS[canonical]
    else:
        return None, "unknown provider"

    headers: dict[str, str] = {}
    if canonical == "anthropic":
        headers = {"x-api-key": api_key, "anthropic-version": "2023-06-01"}
    elif canonical == "google":
        url = f"{url}?key={api_key}"
    else:
        headers = {"Authorization": f"Bearer {api_key}"}

    return _http_check(url, headers)


def _classify_vertexai_claude_error(
    exc: APIStatusError
    | DefaultCredentialsError
    | RefreshError
    | RuntimeError
    | TypeError
    | ValueError
    | httpx.HTTPError,
) -> tuple[bool | None, str]:
    """Classify one Vertex AI Claude validation failure for doctor output."""
    from anthropic import APIStatusError  # noqa: PLC0415
    from google.auth.exceptions import DefaultCredentialsError, RefreshError  # noqa: PLC0415

    if isinstance(exc, APIStatusError):
        return False, _vertexai_claude_api_error_detail(exc)
    if isinstance(exc, DefaultCredentialsError):
        return None, str(exc)
    if isinstance(exc, RefreshError):
        return False, str(exc)
    return None, str(exc)


def _vertexai_claude_api_error_detail(exc: APIStatusError) -> str:
    """Turn a Vertex Claude API error into an actionable doctor message.

    Model availability on Vertex is per project and region: a project can have
    the Vertex AI API enabled yet still 404 on a Claude model when Anthropic
    models are not enabled in its Model Garden or when the model id form is
    wrong for Vertex (current-generation ids are bare, e.g. the anthropic
    default; older ones are dated snapshots with an @ separator).
    """
    if exc.status_code == httpx.codes.NOT_FOUND:
        return (
            "HTTP 404: model not available in this project/region — verify the Vertex model id form "
            "(bare current-generation id or dated @ snapshot) and that Anthropic models are enabled "
            f"in the project's Model Garden ({exc.message})"
        )
    if exc.status_code == httpx.codes.FORBIDDEN:
        if "SERVICE_DISABLED" in exc.message:
            return "HTTP 403: the Vertex AI API (aiplatform.googleapis.com) is not enabled in this project"
        return "HTTP 403: permission denied — check the credentials' IAM access to Vertex AI in this project"
    return f"HTTP {exc.status_code}"


def _validate_vertexai_claude_connection(
    model_config: ModelConfig,
    runtime_paths: RuntimePaths,
) -> tuple[bool | None, str]:
    """Validate the configured Vertex AI Claude model with the runtime request path."""
    extra_kwargs = dict(model_config.extra_kwargs or {})
    project_env = VERTEXAI_CLAUDE_ENV_BY_KEY["project_id"]
    region_env = VERTEXAI_CLAUDE_ENV_BY_KEY["region"]
    project_id = extra_kwargs.get("project_id") or runtime_paths.env_value(project_env)
    region = extra_kwargs.get("region") or runtime_paths.env_value(region_env)
    missing = []
    if not project_id:
        missing.append(project_env)
    if not region:
        missing.append(region_env)
    if missing:
        return None, f"missing {', '.join(missing)}"

    client_params = dict(extra_kwargs.get("client_params") or {})
    google_application_credentials = runtime_env_path(runtime_paths, "GOOGLE_APPLICATION_CREDENTIALS")
    if "credentials" not in client_params and google_application_credentials is not None:
        from mindroom.google_adc import load_google_application_credentials  # noqa: PLC0415

        try:
            client_params["credentials"] = load_google_application_credentials(str(google_application_credentials))
        except PermanentStartupError as exc:
            return False, str(exc)
    if client_params:
        extra_kwargs["client_params"] = client_params

    extra_kwargs.setdefault("project_id", project_id)
    extra_kwargs.setdefault("region", region)
    extra_kwargs.setdefault("timeout", 10)

    from agno.models.vertexai.claude import Claude as VertexAIClaude  # noqa: PLC0415
    from anthropic import APIStatusError  # noqa: PLC0415
    from google.auth.exceptions import DefaultCredentialsError, RefreshError  # noqa: PLC0415

    try:
        model = VertexAIClaude(id=model_config.id, **extra_kwargs)
        request_kwargs = model.get_request_params().copy()
        request_kwargs["model"] = model_config.id
        request_kwargs["messages"] = [{"role": "user", "content": "Reply with OK."}]
        request_kwargs.setdefault("max_tokens", 1)
        request_kwargs["timeout"] = request_kwargs.get("timeout", 10)
        client = model.get_client()
        client.messages.create(
            **request_kwargs,
        )
    except (
        APIStatusError,
        DefaultCredentialsError,
        RefreshError,
        RuntimeError,
        TypeError,
        ValueError,
        httpx.HTTPError,
    ) as exc:
        return _classify_vertexai_claude_error(exc)

    return True, ""


def _get_ollama_host(config: Config, runtime_paths: RuntimePaths) -> str:
    """Get the Ollama host from config or environment."""
    for model in config.models.values():
        if model.provider == "ollama" and model.host:
            return model.host
    return runtime_paths.env_value("OLLAMA_HOST", default=OLLAMA_HOST_DEFAULT) or OLLAMA_HOST_DEFAULT


def _check_providers(config: Config, runtime_paths: RuntimePaths) -> tuple[int, int, int]:
    """Print provider summary and validate API keys. Returns (passed, failed, warnings)."""
    provider_models: dict[str, list[str]] = {}
    for name, model in config.models.items():
        provider_models.setdefault(model.provider, []).append(name)

    if not provider_models:
        return 0, 0, 0

    # Print provider summary
    parts = []
    for provider in sorted(provider_models):
        n = len(provider_models[provider])
        parts.append(f"{provider} ({n} model{'s' if n != 1 else ''})")
    console.print(f"  Providers: {', '.join(parts)}")

    passed = 0
    failed = 0
    warnings = 0
    validated_keys: set[str] = set()

    for provider in sorted(provider_models):
        p, f, w = _check_single_provider(provider, config, validated_keys, runtime_paths)
        passed += p
        failed += f
        warnings += w

    return passed, failed, warnings


def _print_validation(
    valid: bool | None,
    detail: str,
    pass_msg: str,
    fail_msg: str,
    warn_msg: str,
) -> tuple[int, int, int]:
    """Print a tri-state validation result. Returns (passed, failed, warnings)."""
    if valid is True:
        console.print(f"[green]✓[/green] {pass_msg}")
        return 1, 0, 0
    if valid is False:
        console.print(f"[red]✗[/red] {fail_msg} ({detail})")
        return 0, 1, 0
    console.print(f"[yellow]![/yellow] {warn_msg} ({detail})")
    return 0, 0, 1


def _check_single_provider(
    provider: str,
    config: Config,
    validated_keys: set[str],
    runtime_paths: RuntimePaths,
) -> tuple[int, int, int]:
    """Validate a single provider. Returns (passed, failed, warnings)."""
    if provider == "vertexai_claude":
        passed = 0
        failed = 0
        warnings = 0
        for model_config in config.models.values():
            if model_config.provider != provider:
                continue
            valid, detail = _validate_vertexai_claude_connection(model_config, runtime_paths)
            p, f, w = _print_validation(
                valid,
                detail,
                f"{provider} connection valid for {model_config.id}",
                f"{provider} connection failed for {model_config.id}",
                f"{provider}: could not validate connection for {model_config.id}",
            )
            passed += p
            failed += f
            warnings += w
        return passed, failed, warnings

    if provider == "ollama":
        host = _get_ollama_host(config, runtime_paths=runtime_paths)
        url = f"{host.rstrip('/')}/api/tags"
        valid, detail = _http_check(url)
        return _print_validation(
            valid,
            detail,
            f"{provider} reachable ({host})",
            f"{provider} unreachable: {host}",
            f"{provider}: could not reach {host}",
        )

    env_key = env_key_for_provider(provider)
    if not env_key:
        return 0, 0, 0

    # google and gemini share GOOGLE_API_KEY — validate once
    if env_key in validated_keys:
        return 0, 0, 0
    validated_keys.add(env_key)

    api_key = runtime_paths.env_value(env_key)
    if not api_key:
        console.print(f"[yellow]![/yellow] {provider}: {env_key} not set")
        return 0, 0, 1

    base_url = _get_custom_base_url(config, provider)
    valid, detail = _validate_provider_key(provider, api_key, base_url)
    return _print_validation(
        valid,
        detail,
        f"{provider} API key valid",
        f"{provider} API key invalid",
        f"{provider}: could not validate key",
    )


def _check_memory_config(config: Config, runtime_paths: RuntimePaths) -> tuple[int, int, int]:
    """Check memory LLM and embedder configuration. Returns (passed, failed, warnings)."""
    backends = (
        {config.memory.backend}
        if not config.agents
        else {config.resolve_entity(agent_name).memory_backend for agent_name in config.agents}
    )
    if "mem0" not in backends:
        if backends == {"none"}:
            console.print("[green]✓[/green] Memory backend: disabled")
        elif backends == {"file"}:
            console.print("[green]✓[/green] Memory backend: file (markdown)")
        else:
            labels = "/".join("disabled" if backend == "none" else backend for backend in sorted(backends))
            console.print(f"[green]✓[/green] Memory backend: mixed (per-agent {labels})")
        # Semantic knowledge bases and file-backend semantic memory search
        # need the shared embedder even without mem0.
        if semantic_embedder_configured(config):
            p, f, w = _check_memory_embedder(config, runtime_paths=runtime_paths)
            return 1 + p, f, w
        return 1, 0, 0

    if len(backends) > 1:
        labels = "/".join("disabled" if backend == "none" else backend for backend in sorted(backends))
        console.print(f"[green]✓[/green] Memory backend: mixed (per-agent {labels})")

    p1, f1, w1 = _check_memory_llm(config, runtime_paths=runtime_paths)
    p2, f2, w2 = _check_memory_embedder(config, runtime_paths=runtime_paths)
    return p1 + p2, f1 + f2, w1 + w2


def _check_memory_llm(config: Config, runtime_paths: RuntimePaths) -> tuple[int, int, int]:
    """Check memory LLM configuration. Returns (passed, failed, warnings)."""
    if config.memory.llm is None:
        ollama_host = _get_ollama_host(config, runtime_paths=runtime_paths)
        console.print(
            "[yellow]![/yellow] Memory LLM not configured"
            f" (defaults to ollama at {ollama_host};"
            " see memory/config.py fallback)",
        )
        # Check if default Ollama is reachable
        valid, detail = _http_check(f"{ollama_host.rstrip('/')}/api/tags")
        if valid is not True:
            console.print(
                f"[red]✗[/red] Default Ollama for memory LLM unreachable ({ollama_host}: {detail})",
            )
            return 0, 1, 0
        return 0, 0, 1

    llm_provider = config.memory.llm.provider
    llm_host = (
        config.memory.llm.config.get("host")
        or config.memory.llm.config.get("openai_base_url")
        or config.memory.llm.config.get("base_url")
    )
    if llm_provider == "ollama":
        host = llm_host or _get_ollama_host(config, runtime_paths=runtime_paths)
        valid, detail = _http_check(f"{host.rstrip('/')}/api/tags")
        return _print_validation(
            valid,
            detail,
            f"Memory LLM: ollama reachable ({host})",
            f"Memory LLM: ollama unreachable ({host})",
            f"Memory LLM: could not reach ollama ({host})",
        )

    llm_model = config.memory.llm.config.get("model", "default")
    env_key = env_key_for_provider(llm_provider)
    api_key = runtime_paths.env_value(env_key) if env_key else None
    if env_key and not api_key:
        console.print(
            f"[yellow]![/yellow] Memory LLM ({llm_provider}): {env_key} not set",
        )
        return 0, 0, 1
    base_url = llm_host
    valid, detail = _validate_provider_key(llm_provider, api_key or "", base_url)
    return _print_validation(
        valid,
        detail,
        f"Memory LLM: {llm_provider}/{llm_model} API key valid",
        f"Memory LLM: {llm_provider}/{llm_model} API key invalid",
        f"Memory LLM: {llm_provider}/{llm_model} could not validate",
    )


def _check_memory_embedder(config: Config, runtime_paths: RuntimePaths) -> tuple[int, int, int]:
    """Check memory embedder configuration. Returns (passed, failed, warnings)."""
    emb = config.memory.embedder
    if emb.provider == "ollama":
        host = emb.config.host or _get_ollama_host(config, runtime_paths=runtime_paths)
        valid, detail = _http_check(f"{host.rstrip('/')}/api/tags")
        return _print_validation(
            valid,
            detail,
            f"Memory embedder: ollama reachable ({host})",
            f"Memory embedder: ollama unreachable ({host})",
            f"Memory embedder: could not reach ollama ({host})",
        )

    if emb.provider == "sentence_transformers":
        valid, detail = _validate_sentence_transformers_embedder(runtime_paths, emb.config.model)
        return _print_validation(
            valid,
            detail,
            f"Memory embedder: sentence_transformers/{emb.config.model} local model loaded",
            f"Memory embedder: sentence_transformers/{emb.config.model} local model failed",
            f"Memory embedder: sentence_transformers/{emb.config.model} could not validate",
        )

    if emb.provider == "openai":
        # One real embedding round-trip through the shared probe, which builds
        # the configured embedder and resolves the key via get_embedder_api_key
        # (explicit config key > 'embedder' credential > openai fallback).
        error = probe_embedder(config, runtime_paths)
        host_suffix = f" ({emb.config.host})" if emb.config.host else ""
        valid: bool | None = True if error is None else None if error == EMBEDDER_UNREACHABLE_DETAIL else False
        detail = error or ""
        if error is not None and emb.config.host:
            detail = _with_local_network_hint(detail, emb.config.host)
        return _print_validation(
            valid,
            detail,
            f"Memory embedder: openai/{emb.config.model} embedding round-trip succeeded{host_suffix}",
            f"Memory embedder: openai/{emb.config.model} embedding request failed{host_suffix}",
            f"Memory embedder: openai/{emb.config.model} could not reach embeddings endpoint{host_suffix}",
        )

    env_key = env_key_for_provider(emb.provider)
    api_key = runtime_paths.env_value(env_key) if env_key else None
    if env_key and not api_key:
        console.print(
            f"[yellow]![/yellow] Memory embedder ({emb.provider}): {env_key} not set",
        )
        return 0, 0, 1

    base_url = emb.config.host
    valid, detail = _validate_provider_key(emb.provider, api_key or "", base_url)
    return _print_validation(
        valid,
        detail,
        f"Memory embedder: {emb.provider}/{emb.config.model} API key valid",
        f"Memory embedder: {emb.provider}/{emb.config.model} API key invalid",
        f"Memory embedder: {emb.provider}/{emb.config.model} could not validate",
    )


def _validate_sentence_transformers_embedder(runtime_paths: RuntimePaths, model: str) -> tuple[bool, str]:
    """Validate a local sentence-transformers model with a tiny embedding request."""
    try:
        embedder = create_sentence_transformers_embedder(runtime_paths, model)
        embedding = embedder.get_embedding("mindroom doctor embedder check")
    except Exception as exc:
        return False, str(exc)

    if not isinstance(embedding, list) or not embedding:
        return False, "empty embedding vector"
    return True, ""


def _check_matrix_homeserver(runtime_paths: RuntimePaths, config: Config | None = None) -> tuple[int, int, int]:
    """Check Matrix homeserver reachability. Returns (passed, failed, warnings)."""
    homeserver = constants.runtime_matrix_homeserver(runtime_paths=runtime_paths)
    url = matrix_versions_url(homeserver)
    try:
        response = httpx.get(url, timeout=5, verify=constants.runtime_matrix_ssl_verify(runtime_paths=runtime_paths))
    except httpx.TransportError as exc:
        console.print(f"[red]✗[/red] Matrix homeserver unreachable: {homeserver} ({exc})")
        return 0, 1, 0
    if response_has_matrix_versions(response):
        sliding_configured = config is not None and config.matrix_sync.mode == "sliding"
        if sliding_configured and not response_advertises_sliding_sync(response):
            console.print(
                f"[red]✗[/red] Matrix homeserver does not advertise MSC4186 Simplified Sliding Sync"
                f" ({MSC4186_UNSTABLE_FEATURE}): {homeserver}."
                " Set matrix_sync.mode: classic or upgrade the homeserver.",
            )
            return 0, 1, 0
        console.print(f"[green]✓[/green] Matrix homeserver: {homeserver}")
        return 1, 0, 0
    detail = f"HTTP {response.status_code}" if not response.is_success else "returned invalid /versions payload"
    console.print(f"[red]✗[/red] Matrix homeserver {detail}: {homeserver}")
    return 0, 1, 0


def _check_storage_writable(runtime_paths: RuntimePaths) -> tuple[int, int, int]:
    """Check storage directory is writable. Returns (passed, failed, warnings)."""
    storage = runtime_paths.storage_root
    try:
        storage.mkdir(parents=True, exist_ok=True)
        fd, tmp = tempfile.mkstemp(dir=storage)
        os.close(fd)
        Path(tmp).unlink()
    except OSError as exc:
        console.print(f"[red]✗[/red] Storage not writable: {storage} ({exc})")
        return 0, 1, 0
    console.print(f"[green]✓[/green] Storage writable: {storage}/")
    return 1, 0, 0
