"""Generate and spawn update scripts for the self-update system.

The daemon cannot replace itself while running. Instead, it generates a
shell script that installs the new wheel, runs project upgrade, and restarts
the daemon. The script is spawned as a detached subprocess so it survives
the daemon's exit.
"""

from __future__ import annotations

import logging
import shlex
import subprocess
import tempfile
from pathlib import Path

from open_agent_kit.utils.global_config import (
    LOCK_FILE,
    STAGED_UPDATE_FILE,
    STAGING_DIR,
    UPDATE_ERROR_FILE,
    get_global_oak_dir,
    read_staged_update,
)
from open_agent_kit.utils.install_method import (
    InstallMethod,
    detect_install_method,
    get_install_command,
)
from open_agent_kit.utils.platform import (
    POSIX_SHELL,
    acquire_file_lock,
    get_process_detach_kwargs,
    release_file_lock,
)

logger = logging.getLogger(__name__)


def generate_update_script(
    project_root: Path,
    wheel_path: str,
    install_method: InstallMethod,
    daemon_type: str,
) -> str:
    """Generate a shell script that installs the wheel and restarts the daemon.

    Args:
        project_root: Absolute path to the project directory.
        wheel_path: Absolute path to the staged .whl file.
        install_method: How OAK was installed (determines install command).
        daemon_type: "team" or "swarm".

    Returns:
        Shell script content as a string.

    Raises:
        ValueError: If install_method is EDITABLE.
    """
    if install_method == InstallMethod.EDITABLE:
        msg = "Cannot generate update script for editable installs"
        raise ValueError(msg)

    # Build the install command — quote every part for shell safety
    install_cmd_parts = get_install_command(install_method, wheel_path)
    install_cmd = " ".join(shlex.quote(p) for p in install_cmd_parts)

    restart_cmd = f"oak {shlex.quote(daemon_type)} start"
    global_dir = get_global_oak_dir()
    q_project_root = shlex.quote(str(project_root))
    q_error_file = shlex.quote(str(global_dir / UPDATE_ERROR_FILE))
    q_staged_file = shlex.quote(str(global_dir / STAGED_UPDATE_FILE))
    q_staging_dir = shlex.quote(str(global_dir / STAGING_DIR))

    return f"""#!/bin/sh
# OAK self-update script — generated by daemon
# Installs staged wheel, upgrades project, restarts daemon.
set -e

SELF="$0"

# Wait for daemon to exit cleanly
sleep 2

cd {q_project_root}

# Install the new package
if ! {install_cmd}; then
    echo '{{"error": "Package install failed"}}' > {q_error_file}
    # Fallback: try to restart old version
    {restart_cmd} --quiet || true
    rm -f "$SELF"
    exit 1
fi

# Run project-level upgrade
if ! oak upgrade --force; then
    echo '{{"error": "Project upgrade failed (package was installed successfully)"}}' > {q_error_file}
    # Package installed but project upgrade failed — still restart with new version
    {restart_cmd} --quiet || true
    rm -f "$SELF"
    exit 1
fi

# Restart daemon with new version
{restart_cmd} --quiet || {{
    echo '{{"error": "Daemon restart failed after successful update"}}' > {q_error_file}
    rm -f "$SELF"
    exit 1
}}

# Cleanup on success
rm -f {q_staged_file}
rm -rf {q_staging_dir}/*
rm -f {q_error_file}
rm -f "$SELF"
"""


def apply_staged_update(
    project_root: Path,
    daemon_type: str,
) -> bool:
    """Apply a staged update by generating and spawning the update script.

    Acquires the update lock so an in-flight download finishes before we
    read the staged metadata and verify the wheel exists on disk.

    Returns True if the script was spawned successfully, False otherwise.
    """
    lock_path = get_global_oak_dir() / LOCK_FILE
    lock_fd = None
    try:
        lock_fd = lock_path.open("w")
        acquire_file_lock(lock_fd, blocking=True)
    except OSError as exc:
        logger.error("Could not acquire update lock: %s", exc)
        if lock_fd:
            lock_fd.close()
        return False

    try:
        return _apply_staged_update_locked(project_root, daemon_type)
    finally:
        try:
            release_file_lock(lock_fd)
        except Exception as exc:
            logger.debug("Failed to release update lock: %s", exc)
        lock_fd.close()


def _apply_staged_update_locked(
    project_root: Path,
    daemon_type: str,
) -> bool:
    """Inner apply logic, called with the update lock held."""
    staged = read_staged_update()
    if not staged:
        logger.warning("No staged update found")
        return False

    wheel_path = staged.get("wheel_path")
    if not wheel_path or not Path(wheel_path).exists():
        logger.error("Staged wheel not found at %s", wheel_path)
        return False

    method = detect_install_method()

    script = generate_update_script(
        project_root=project_root,
        wheel_path=wheel_path,
        install_method=method,
        daemon_type=daemon_type,
    )

    # Write script to temp file
    with tempfile.NamedTemporaryFile(
        mode="w",
        suffix=".sh",
        prefix="oak-update-",
        delete=False,
    ) as f:
        f.write(script)
        script_path = f.name

    Path(script_path).chmod(0o755)

    # Spawn detached subprocess
    try:
        subprocess.Popen(
            [POSIX_SHELL, script_path],
            cwd=str(project_root),
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            stdin=subprocess.DEVNULL,
            **get_process_detach_kwargs(),
        )
        logger.info("Update script spawned: %s", script_path)
        return True
    except OSError as exc:
        logger.error("Failed to spawn update script: %s", exc)
        return False


__all__ = ["generate_update_script", "apply_staged_update"]
