import hashlib
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import threading
import time
import zipfile
from datetime import datetime
from pathlib import Path

import requests
from fastapi import APIRouter, HTTPException, Request
from starlette.responses import JSONResponse

from backend.config import (
    DEFAULT_SETTINGS,
    GEMINI_URL,
    LLAMACPP_URL,
    NVIDIA_URL,
    OLLAMA_URL,
    OPENAI_URL,
    UPDATE_BASE_URL,
    VERSION_FILE,
    _SECRET_KEYS,
    _apply_update_from_extracted_dir,
    _candidate_manifest_urls,
    _default_manifest_url,
    _fetch_update_manifest,
    _get_local_version,
    _is_cloud_model,
    _is_dns_resolution_error,
    _is_remote_newer,
    _load_settings,
    _save_settings,
)
from backend.integrations import _restart_slack_bot, _restart_telegram_bot
from backend.local_time import utc_now_iso_z
from backend.mcp_runtime import _mcp_discover_all
from backend.models import _model_cache
from backend.websocket_manager import _reset_runtime_state


router = APIRouter()
ROOT_DIR = Path(__file__).resolve().parent.parent

_MODEL_KEY_FIELD_BY_PROVIDER = {
    "ollama_cloud": "ollama_cloud_api_key",
    "openai": "openai_api_key",
    "gemini": "gemini_api_key",
    "nvidia": "nvidia_api_key",
}


def _provider_error_detail(provider_label: str, response: requests.Response | None = None, exc: Exception | None = None) -> str:
    if response is not None:
        detail = ""
        try:
            payload = response.json()
            if isinstance(payload, dict):
                error_obj = payload.get("error")
                if isinstance(error_obj, dict):
                    detail = str(error_obj.get("message") or "").strip()
                elif error_obj:
                    detail = str(error_obj).strip()
                if not detail:
                    detail = str(payload.get("detail") or payload.get("message") or "").strip()
        except Exception:
            detail = ""
        if not detail:
            detail = (response.text or "").strip()[:240]
        suffix = f": {detail}" if detail else ""
        return f"{provider_label} API key test failed ({response.status_code}){suffix}"
    if exc is not None:
        return f"{provider_label} API key test failed: {exc}"
    return f"{provider_label} API key test failed."


def _resolve_model_api_test_key(provider: str, candidate_key: str | None, settings: dict) -> str:
    field = _MODEL_KEY_FIELD_BY_PROVIDER.get(provider, "")
    candidate = str(candidate_key or "").strip()
    if candidate and not candidate.startswith("***"):
        return candidate
    return str(settings.get(field) or "").strip()


def _test_openai_api_key(api_key: str) -> dict:
    response = None
    try:
        response = requests.get(
            "https://api.openai.com/v1/models",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=15,
        )
        if response.status_code >= 400:
            return {"ok": False, "verified": True, "detail": _provider_error_detail("OpenAI", response=response)}
        model_count = len((response.json() or {}).get("data") or [])
        return {
            "ok": True,
            "verified": True,
            "detail": f"OpenAI API key works. Retrieved {model_count} model(s).",
        }
    except Exception as exc:
        return {"ok": False, "verified": True, "detail": _provider_error_detail("OpenAI", response=response, exc=exc)}


def _test_gemini_api_key(api_key: str) -> dict:
    response = None
    try:
        response = requests.get(f"{GEMINI_URL}?key={api_key}", timeout=15)
        if response.status_code >= 400:
            return {"ok": False, "verified": True, "detail": _provider_error_detail("Gemini", response=response)}
        model_count = len((response.json() or {}).get("models") or [])
        return {
            "ok": True,
            "verified": True,
            "detail": f"Gemini API key works. Retrieved {model_count} model(s).",
        }
    except Exception as exc:
        return {"ok": False, "verified": True, "detail": _provider_error_detail("Gemini", response=response, exc=exc)}


def _test_nvidia_api_key(api_key: str) -> dict:
    response = None
    payload = {
        "model": "moonshotai/kimi-k2.5",
        "messages": [{"role": "user", "content": "Reply with OK"}],
        "max_tokens": 1,
        "temperature": 0,
        "stream": False,
    }
    try:
        response = requests.post(
            NVIDIA_URL,
            headers={"Authorization": f"Bearer {api_key}", "Accept": "application/json"},
            json=payload,
            timeout=20,
        )
        if response.status_code >= 400:
            return {"ok": False, "verified": True, "detail": _provider_error_detail("NVIDIA", response=response)}
        return {
            "ok": True,
            "verified": True,
            "detail": "NVIDIA API key works. Test completion request succeeded.",
        }
    except Exception as exc:
        return {"ok": False, "verified": True, "detail": _provider_error_detail("NVIDIA", response=response, exc=exc)}


def _ollama_cloud_probe_candidates(settings: dict, selected_model: str = "") -> list[str]:
    candidates: list[str] = []
    explicit_model = str(selected_model or "").strip()
    if explicit_model:
        if _is_cloud_model(explicit_model):
            return [explicit_model]
        return []
    for candidate in (
        settings.get("main_model", ""),
        settings.get("fast_model", ""),
        settings.get("fallback_model", ""),
        "gpt-oss:20b-cloud",
        "glm-5.1:cloud",
        "deepseek-v4-flash:cloud",
        "kimi-k2.6:cloud",
        "minimax-m2.7:cloud",
    ):
        value = str(candidate or "").strip()
        if value and _is_cloud_model(value) and value not in candidates:
            candidates.append(value)
    return candidates


def _test_local_ollama_cloud_runtime(settings: dict, selected_model: str = "") -> dict:
    explicit_model = str(selected_model or "").strip()
    if explicit_model and not _is_cloud_model(explicit_model):
        return {
            "ok": False,
            "verified": False,
            "detail": f"Ollama Cloud test failed: selected model `{explicit_model}` is not an Ollama Cloud model.",
        }
    candidates = _ollama_cloud_probe_candidates(settings, selected_model=selected_model)
    if not candidates:
        return {
            "ok": False,
            "verified": False,
            "detail": "Ollama Cloud test failed: no cloud model is configured. Choose a `:cloud` model or keep the recommended profile.",
        }

    last_detail = ""
    for model_name in candidates:
        try:
            response = requests.post(
                f"{OLLAMA_URL}/chat",
                json={
                    "model": model_name,
                    "messages": [{"role": "user", "content": "Reply with OK"}],
                    "stream": False,
                    "options": {"temperature": 0, "num_predict": 8},
                    "keep_alive": "0s",
                },
                timeout=45,
            )
        except requests.ConnectionError:
            return {
                "ok": False,
                "verified": False,
                "detail": "Ollama Cloud test failed: local Ollama is not reachable on http://127.0.0.1:11434. Start Ollama first.",
            }
        except Exception as exc:
            last_detail = f"Ollama Cloud runtime probe failed for {model_name}: {exc}"
            continue

        if response.status_code >= 400:
            body = (response.text or "").strip()
            last_detail = f"Ollama Cloud runtime probe failed for {model_name} ({response.status_code})"
            if body:
                last_detail += f": {body[:240]}"
            continue

        resolved_model = ""
        try:
            resolved_model = str((response.json() or {}).get("model") or "").strip()
        except Exception:
            resolved_model = ""
        detail = f"Ollama Cloud works. Local Ollama completed a cloud-model probe with {model_name}."
        if resolved_model and resolved_model != model_name:
            detail += f" Runtime resolved it as {resolved_model}."
        return {
            "ok": True,
            "verified": True,
            "detail": detail,
        }

    return {
        "ok": False,
        "verified": False,
        "detail": last_detail or "Ollama Cloud runtime probe failed. Make sure you ran `ollama signin` locally and that a cloud model is available.",
    }


def _test_ollama_cloud_api_key(api_key: str, settings: dict, selected_model: str = "") -> dict:
    runtime_result = _test_local_ollama_cloud_runtime(settings, selected_model=selected_model)
    if runtime_result.get("ok"):
        return runtime_result
    return {
        "ok": False,
        "verified": False,
        "detail": str(runtime_result.get("detail") or "Ollama Cloud test failed.").strip(),
    }


def _run_model_api_key_test(provider: str, api_key: str, settings: dict | None = None, selected_model: str = "") -> dict:
    if provider == "openai":
        return _test_openai_api_key(api_key)
    if provider == "gemini":
        return _test_gemini_api_key(api_key)
    if provider == "nvidia":
        return _test_nvidia_api_key(api_key)
    if provider == "ollama_cloud":
        return _test_ollama_cloud_api_key(api_key, settings or {}, selected_model=selected_model)
    return {"ok": False, "verified": False, "detail": "Unsupported provider."}


def _schedule_self_restart(delay_seconds: float = 0.6) -> None:
    current_pid = os.getpid()
    cmd = [
        sys.executable,
        "-m",
        "uvicorn",
        "backend.main:app",
        "--host",
        "127.0.0.1",
        "--port",
        "8000",
    ]
    helper_code = "\n".join([
        "import os, subprocess, sys, time",
        "target_pid = int(sys.argv[1])",
        "delay = float(sys.argv[2])",
        "cwd = sys.argv[3]",
        "cmd = sys.argv[4:]",
        "deadline = time.time() + 20",
        "while time.time() < deadline:",
        "    try:",
        "        os.kill(target_pid, 0)",
        "        time.sleep(0.2)",
        "    except OSError:",
        "        break",
        "time.sleep(delay)",
        "flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0) | getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)",
        "subprocess.Popen(cmd, cwd=cwd, creationflags=flags)",
    ])
    flags = 0
    if os.name == "nt":
        flags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
    subprocess.Popen(
        [sys.executable, "-c", helper_code, str(current_pid), str(delay_seconds), str(ROOT_DIR), *cmd],
        cwd=str(ROOT_DIR),
        creationflags=flags,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )


def _redact_settings(settings: dict) -> dict:
    out = dict(settings)
    for key in _SECRET_KEYS:
        val = out.get(key, "")
        if isinstance(val, str) and len(val) > 4:
            out[key] = "***" + val[-4:]
        elif val:
            out[key] = "***"
    return out


@router.get("/api/settings")
def api_get_settings():
    settings = dict(_load_settings())
    if "policy_budget_mode" not in settings:
        settings["policy_budget_mode"] = "budget"
    return _redact_settings(settings)


@router.post("/api/settings")
async def api_save_settings(request: Request):
    body = await request.json()
    allowed = set(DEFAULT_SETTINGS.keys()) | {
        "install_dir",
        "whatsapp_phone_number_id",
        "whatsapp_access_token",
        "whatsapp_verify_token",
        "policy_budget_mode",
    }
    body = {k: v for k, v in body.items() if k in allowed}
    if "policy_budget_mode" in body:
        mode = str(body.get("policy_budget_mode") or "budget").strip().lower()
        body["policy_budget_mode"] = "dynamic" if mode == "dynamic" else "budget"
    current = _load_settings()
    for key in _SECRET_KEYS:
        if key in body and isinstance(body[key], str) and body[key].startswith("***"):
            body.pop(key)
    current.update(body)
    _save_settings(current)
    _model_cache.clear()
    threading.Thread(target=_restart_slack_bot, daemon=True).start()
    threading.Thread(target=_restart_telegram_bot, daemon=True).start()
    threading.Thread(target=_mcp_discover_all, daemon=True).start()
    return {"ok": True, "settings": _redact_settings(current)}


@router.post("/api/settings/test-model-api-key")
async def api_test_model_api_key(request: Request):
    body = await request.json()
    provider = str(body.get("provider") or "").strip().lower()
    if provider not in _MODEL_KEY_FIELD_BY_PROVIDER:
        raise HTTPException(status_code=400, detail="Unsupported provider")

    settings = _load_settings()
    api_key = _resolve_model_api_test_key(provider, body.get("api_key"), settings)
    if not api_key:
        label = {
            "ollama_cloud": "Ollama Cloud",
            "openai": "OpenAI",
            "gemini": "Gemini",
            "nvidia": "NVIDIA",
        }.get(provider, provider)
        raise HTTPException(status_code=400, detail=f"No {label} API key configured to test.")

    probe_settings = dict(settings)
    for field in ("main_model", "fast_model", "fallback_model"):
        candidate = str(body.get(field) or "").strip()
        if candidate:
            probe_settings[field] = candidate
    selected_model = str(body.get("selected_model") or "").strip()

    result = _run_model_api_key_test(provider, api_key, probe_settings, selected_model=selected_model)
    status_code = 200 if result.get("ok") else 502
    payload = {
        "provider": provider,
        "ok": bool(result.get("ok")),
        "verified": bool(result.get("verified")),
        "detail": str(result.get("detail") or "").strip(),
    }
    return JSONResponse(payload, status_code=status_code)


@router.post("/api/reset")
async def api_reset():
    await _reset_runtime_state(close_ws=True)
    _schedule_self_restart()

    def _exit_soon():
        time.sleep(0.6)
        os._exit(0)

    threading.Thread(target=_exit_soon, daemon=True).start()
    return {
        "ok": True,
        "restarting": True,
        "message": "Server reset accepted. Process restart initiated.",
    }


@router.post("/api/shutdown")
async def api_shutdown():
    def _stop():
        time.sleep(0.3)
        os._exit(0)

    threading.Thread(target=_stop, daemon=True).start()
    return {"ok": True}


@router.post("/api/spawn-launcher")
async def api_spawn_launcher(request: Request):
    """Spawn launcher_server.py as a background process if not already running."""
    client = request.client.host if request.client else ""
    if client not in ("127.0.0.1", "::1", "localhost"):
        raise HTTPException(status_code=403, detail="Forbidden")

    def _launcher_port_open():
        try:
            with socket.create_connection(("127.0.0.1", 8001), timeout=1):
                return True
        except OSError:
            return False

    if _launcher_port_open():
        return {"ok": True, "message": "already running"}

    launcher_script = ROOT_DIR / "launcher_server.py"
    if not launcher_script.is_file():
        return {"ok": False, "message": "launcher_server.py not found"}

    lock_file = ROOT_DIR / ".launcher.lock"
    if lock_file.exists():
        try:
            lock_file.unlink()
        except OSError:
            pass

    pythonw = Path(sys.executable).with_name("pythonw.exe")
    popen_kwargs: dict = {
        "cwd": str(ROOT_DIR),
        "stdin": subprocess.DEVNULL,
        "stdout": subprocess.DEVNULL,
        "stderr": subprocess.DEVNULL,
    }
    if sys.platform == "win32":
        popen_kwargs["creationflags"] = (
            subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
        )

    try:
        exe = pythonw if pythonw.is_file() else Path(sys.executable)
        subprocess.Popen([str(exe), str(launcher_script)], **popen_kwargs)
    except Exception as exc:
        return {"ok": False, "message": f"Failed to spawn launcher: {exc}"}

    deadline = time.time() + 10
    while time.time() < deadline:
        if _launcher_port_open():
            return {"ok": True, "message": "started"}
        time.sleep(0.5)

    return {"ok": False, "message": "Launcher spawned but did not respond in time"}


@router.get("/api/health")
def api_health():
    """Run all integration checks and return a unified health status."""
    settings = _load_settings()
    checks: dict[str, dict] = {}

    try:
        response = requests.get(f"{OLLAMA_URL}/tags", timeout=3)
        models = [m["name"] for m in response.json().get("models", [])] if response.status_code == 200 else []
        checks["ollama"] = {"ok": response.status_code == 200, "models": models}
    except Exception:
        checks["ollama"] = {"ok": False, "error": "Not reachable"}

    outlook_enabled = bool(settings.get("outlook_enabled"))
    if outlook_enabled:
        try:
            import win32com.client  # noqa: F401
            checks["outlook"] = {"ok": True, "detail": "pywin32 available"}
        except ImportError:
            checks["outlook"] = {"ok": False, "error": "pywin32 not installed"}
    else:
        checks["outlook"] = {"ok": True, "detail": "Disabled", "skipped": True}

    google_enabled = bool(settings.get("google_enabled"))
    has_google_creds = bool((settings.get("google_client_id") or "").strip())
    google_token_path = Path("google_token.json")
    if google_enabled and has_google_creds:
        checks["google"] = {
            "ok": google_token_path.exists(),
            "detail": "Token present" if google_token_path.exists() else "Not yet authorized",
        }
    else:
        checks["google"] = {"ok": True, "detail": "Disabled", "skipped": True}

    has_brave = bool((settings.get("brave_api_key") or "").strip())
    checks["brave_search"] = {
        "ok": has_brave,
        "detail": "API key configured" if has_brave else "No API key",
    }

    for key_name, label in [
        ("openai_api_key", "OpenAI"),
        ("gemini_api_key", "Gemini"),
        ("nvidia_api_key", "NVIDIA"),
    ]:
        has_key = bool((settings.get(key_name) or "").strip())
        checks[label.lower()] = {
            "ok": True,
            "detail": "Configured" if has_key else "Not configured",
            "skipped": not has_key,
        }

    slack_enabled = bool((settings.get("slack_bot_token") or "").strip())
    checks["slack"] = {
        "ok": True,
        "detail": "Configured" if slack_enabled else "Not configured",
        "skipped": not slack_enabled,
    }

    wa_enabled = bool(settings.get("whatsapp_enabled"))
    checks["whatsapp"] = {
        "ok": True,
        "detail": "Enabled" if wa_enabled else "Not configured",
        "skipped": not wa_enabled,
    }

    try:
        from playwright.sync_api import sync_playwright  # noqa: F401
        checks["browser"] = {"ok": True, "detail": "Playwright available"}
    except ImportError:
        checks["browser"] = {"ok": False, "error": "Playwright not installed"}

    all_ok = all(check.get("ok", False) for check in checks.values())
    return {"ok": all_ok, "checks": checks}


@router.get("/api/setup/status")
def api_setup_status():
    settings = _load_settings()
    ollama_ok = False
    ollama_models = []
    try:
        response = requests.get(f"{OLLAMA_URL}/tags", timeout=3)
        if response.status_code == 200:
            ollama_ok = True
            ollama_models = [model["name"] for model in response.json().get("models", [])]
    except Exception:
        pass
    llamacpp_ok = False
    llamacpp_models: list[str] = []
    try:
        lc_url = (settings.get("llamacpp_url") or LLAMACPP_URL).rstrip("/")
        lc_resp = requests.get(f"{lc_url}/v1/models", timeout=2)
        if lc_resp.status_code == 200:
            llamacpp_ok = True
            llamacpp_models = [model["id"] for model in lc_resp.json().get("data", [])]
    except Exception:
        pass
    return {
        "setup_complete": bool(settings.get("setup_complete", False)),
        "cwd": str(Path.cwd().resolve()),
        "install_dir": settings.get("install_dir", ""),
        "ollama_running": ollama_ok,
        "ollama_models": ollama_models,
        "llamacpp_running": llamacpp_ok,
        "llamacpp_models": llamacpp_models,
        "has_ollama_cloud": bool((settings.get("ollama_cloud_api_key") or "").strip()),
        "has_openai": bool((settings.get("openai_api_key") or "").strip()),
        "has_gemini": bool((settings.get("gemini_api_key") or "").strip()),
        "has_nvidia": bool((settings.get("nvidia_api_key") or "").strip()),
        "has_brave": bool((settings.get("brave_api_key") or "").strip()),
        "has_google": bool((settings.get("google_client_id") or "").strip()),
        "has_slack": bool((settings.get("slack_bot_token") or "").strip()),
    }


@router.post("/api/setup/complete")
async def api_setup_complete(request: Request):
    body = await request.json()
    current = _load_settings()
    install_dir = body.get("install_dir", "").strip()
    if install_dir:
        target = Path(install_dir)
        try:
            target.mkdir(parents=True, exist_ok=True)
            for subdir in ["memory", "output", "tools", "attachments", "downloads"]:
                (target / subdir).mkdir(exist_ok=True)
            body["install_dir"] = str(target.resolve())
        except Exception as exc:
            return JSONResponse({"ok": False, "error": f"Cannot create folder: {exc}"}, status_code=400)
    for key in _SECRET_KEYS:
        if key in body and isinstance(body[key], str) and body[key].startswith("***"):
            body.pop(key)
    current.update(body)
    current["setup_complete"] = True
    _save_settings(current)
    _model_cache.clear()
    threading.Thread(target=_restart_slack_bot, daemon=True).start()
    threading.Thread(target=_restart_telegram_bot, daemon=True).start()
    return {"ok": True}


@router.post("/api/setup/validate-folder")
async def api_setup_validate_folder(request: Request):
    body = await request.json()
    folder = (body.get("path") or "").strip()
    if not folder:
        return JSONResponse({"ok": False, "error": "No path provided"}, status_code=400)
    path = Path(folder)
    if not path.is_absolute():
        return {"ok": False, "error": "Please enter a full path (e.g. D:\\Projects\\Agent)"}
    if path.exists():
        if not path.is_dir():
            return {"ok": False, "error": "Path exists but is not a folder"}
        try:
            test_file = path / ".agent_write_test"
            test_file.write_text("ok")
            test_file.unlink()
        except Exception:
            return {"ok": False, "error": "Folder exists but is not writable"}
        return {"ok": True, "exists": True, "writable": True}
    parent = path.parent
    if not parent.exists() or not parent.is_dir():
        return {"ok": False, "error": "Parent folder does not exist"}
    return {"ok": True, "exists": False, "writable": True}


@router.get("/api/version")
def api_version():
    return {
        "version": _get_local_version(),
        "update_base_url": UPDATE_BASE_URL,
        "manifest_url": _default_manifest_url(),
        "manifest_candidates": _candidate_manifest_urls(None),
    }


@router.post("/api/copilot/feedback")
async def api_copilot_feedback(request: Request):
    """Record user feedback on a copilot briefing."""
    body = await request.json()
    try:
        from backend.copilot_briefing import record_briefing_feedback
    except ImportError:
        return {"ok": False, "error": "Copilot briefing feedback not available"}
    entry = record_briefing_feedback(
        briefing_type=str(body.get("type") or "morning"),
        rating=str(body.get("rating") or "thumbs_up"),
        helpful_items=body.get("helpful_items"),
        unhelpful_items=body.get("unhelpful_items"),
        comment=str(body.get("comment") or ""),
    )
    return {"ok": True, "entry": entry}


@router.get("/api/update/check")
def api_update_check(manifest_url: str | None = None):
    local_version = _get_local_version()
    try:
        manifest = _fetch_update_manifest(manifest_url)
        latest = manifest["latest_version"]
        return {
            "ok": True,
            "current_version": local_version,
            "latest_version": latest,
            "update_available": _is_remote_newer(local_version, latest),
            "download_url": manifest["download_url"],
            "manifest_url": manifest["manifest_url"],
            "attempted_urls": manifest.get("attempted_urls", []),
            "notes": manifest["notes"],
            "checked_at": utc_now_iso_z(),
        }
    except Exception as exc:
        error_code = "UPDATE_CHECK_FAILED"
        user_message = "Unable to check for updates right now. Please try again later."
        if _is_dns_resolution_error(exc):
            error_code = "DNS_RESOLUTION_FAILED"
            user_message = (
                "Could not resolve the update server address. "
                "Check internet/DNS or verify the update host configuration."
            )
        elif isinstance(exc, requests.exceptions.Timeout):
            error_code = "UPDATE_SERVER_TIMEOUT"
            user_message = "The update server did not respond in time."
        elif isinstance(exc, requests.exceptions.ConnectionError):
            error_code = "UPDATE_SERVER_UNREACHABLE"
            user_message = "The update server is unreachable right now."
        return JSONResponse(
            {
                "ok": False,
                "current_version": local_version,
                "update_available": False,
                "error_code": error_code,
                "user_message": user_message,
                "error": str(exc),
                "attempted_urls": list(getattr(exc, "attempted_urls", []) or []),
                "checked_at": utc_now_iso_z(),
            },
            status_code=502,
        )


@router.post("/api/update/install")
async def api_update_install(request: Request):
    body = await request.json()
    manifest_url = (body.get("manifest_url") or "").strip() or None
    force_install = bool(body.get("force", False))
    local_version = _get_local_version()
    manifest = _fetch_update_manifest(manifest_url)
    latest = manifest["latest_version"]
    if not force_install and not _is_remote_newer(local_version, latest):
        return {
            "ok": True,
            "installed": False,
            "message": "No newer version available.",
            "current_version": local_version,
            "latest_version": latest,
        }
    await _reset_runtime_state(close_ws=True)
    downloads_dir = Path("downloads")
    downloads_dir.mkdir(exist_ok=True)
    zip_name = f"AgentOrchestrator-{latest}.zip"
    zip_path = downloads_dir / zip_name
    tmp_dir = Path(tempfile.mkdtemp(prefix="update_extract_", dir=str(downloads_dir)))
    sha = hashlib.sha256()
    try:
        with requests.get(manifest["download_url"], stream=True, timeout=180) as response:
            response.raise_for_status()
            with zip_path.open("wb") as handle:
                for chunk in response.iter_content(chunk_size=1024 * 256):
                    if not chunk:
                        continue
                    handle.write(chunk)
                    sha.update(chunk)
        expected_sha = manifest.get("sha256", "")
        actual_sha = sha.hexdigest().lower()
        if expected_sha and actual_sha != expected_sha:
            return JSONResponse(
                {
                    "ok": False,
                    "error": "Downloaded package checksum mismatch.",
                    "expected_sha256": expected_sha,
                    "actual_sha256": actual_sha,
                },
                status_code=400,
            )
        with zipfile.ZipFile(zip_path, "r") as archive:
            archive.extractall(tmp_dir)
        extracted_entries = [path_entry for path_entry in tmp_dir.iterdir() if path_entry.name != "__MACOSX"]
        source_root = tmp_dir
        if len(extracted_entries) == 1 and extracted_entries[0].is_dir():
            source_root = extracted_entries[0]
        report = _apply_update_from_extracted_dir(source_root, Path.cwd())
        VERSION_FILE.write_text(latest + "\n", encoding="utf-8")
        return {
            "ok": True,
            "installed": True,
            "current_version": local_version,
            "installed_version": latest,
            "download_url": manifest["download_url"],
            "downloaded_file": str(zip_path),
            "copied_files": report["copied_files"],
            "skipped_files": report["skipped_files"],
            "skipped_sample": report["skipped_sample"],
            "notes": manifest["notes"],
            "message": "Update installed. User settings/data were preserved. Reset completed.",
        }
    except Exception as exc:
        return JSONResponse({"ok": False, "error": str(exc)}, status_code=500)
    finally:
        try:
            shutil.rmtree(tmp_dir, ignore_errors=True)
        except Exception:
            pass