"""Install and remove per-user worker autostart entries."""

from __future__ import annotations

from collections.abc import Callable
from collections.abc import Mapping
from dataclasses import dataclass
import os
from pathlib import Path
import platform
import plistlib
import shutil
import subprocess
import sys
import tempfile
import time


MACOS_LABEL = "me.judvika.a2a-worker"
WINDOWS_STARTUP_FILE_NAME = "A2A-Worker.cmd"
LINUX_UNIT_NAME = "a2a-worker.service"

_EXECUTOR_PROGRAMS = {
    "cursor": "cursor-agent",
    "grok": "grok",
    "opencode": "opencode",
}
_WINDOWS_FILE_RETRY_DELAYS = (0.1, 0.2, 0.4, 0.8, 1.6)

WhichCommand = Callable[[str], str | None]
RunCommand = Callable[..., subprocess.CompletedProcess[str]]


class AutostartError(RuntimeError):
    """Raised when an autostart entry cannot be installed or removed."""


@dataclass(frozen=True)
class AutostartResult:
    """A platform-neutral summary for the CLI."""

    action: str
    platform: str
    path: Path
    enabled: bool
    started: bool = False
    backup_path: Path | None = None
    manual_command: str | None = None


def install_autostart(
    executor: str,
    *,
    system_name: str | None = None,
    home: str | Path | None = None,
    argv0: str | None = None,
    executable: str | Path | None = None,
    environ: Mapping[str, str] | None = None,
    which: WhichCommand | None = None,
    run_command: RunCommand | None = None,
) -> AutostartResult:
    """Install the current worker for the next user login without starting it."""

    normalized_system = _normalized_system(system_name)
    current_environment = os.environ if environ is None else environ
    command_finder = shutil.which if which is None else which
    command_runner = subprocess.run if run_command is None else run_command
    home_path = _home_path(normalized_system, home, current_environment)
    agent_executable = resolve_agent_executable(
        argv0=argv0,
        executable=executable,
        which=command_finder,
    )
    executor_name = executor.strip().lower() or "echo"
    path_value = _path_with_executor(
        executor_name,
        current_environment.get("PATH", os.defpath),
        normalized_system,
        command_finder,
    )

    if normalized_system == "macos":
        return _install_macos(
            executor_name,
            home_path,
            agent_executable,
            path_value,
            command_finder,
            command_runner,
        )
    if normalized_system == "windows":
        return _install_windows(
            executor_name,
            home_path,
            agent_executable,
            path_value,
            current_environment,
        )
    if normalized_system == "linux":
        return _install_linux(
            executor_name,
            home_path,
            agent_executable,
            path_value,
            command_finder,
            command_runner,
        )
    raise AutostartError(f"unsupported operating system: {system_name or platform.system()}")


def uninstall_autostart(
    *,
    system_name: str | None = None,
    home: str | Path | None = None,
    environ: Mapping[str, str] | None = None,
    which: WhichCommand | None = None,
    run_command: RunCommand | None = None,
) -> AutostartResult:
    """Remove a per-user autostart entry without stopping a running worker."""

    normalized_system = _normalized_system(system_name)
    current_environment = os.environ if environ is None else environ
    command_finder = shutil.which if which is None else which
    command_runner = subprocess.run if run_command is None else run_command
    home_path = _home_path(normalized_system, home, current_environment)

    if normalized_system == "macos":
        path = home_path / "Library" / "LaunchAgents" / f"{MACOS_LABEL}.plist"
        path.unlink(missing_ok=True)
        return AutostartResult(
            action="uninstalled",
            platform="macos",
            path=path,
            enabled=False,
        )
    if normalized_system == "windows":
        return _uninstall_windows(home_path, current_environment)
    if normalized_system == "linux":
        return _uninstall_linux(home_path, command_finder, command_runner)
    raise AutostartError(f"unsupported operating system: {system_name or platform.system()}")


def resolve_agent_executable(
    *,
    argv0: str | None = None,
    executable: str | Path | None = None,
    which: WhichCommand | None = None,
) -> Path:
    """Resolve the installed ``a2a-agent`` console command to an absolute path."""

    if executable is not None:
        return Path(executable).expanduser().resolve()

    command_finder = shutil.which if which is None else which
    invoked_as = sys.argv[0] if argv0 is None else argv0
    invoked_path = Path(invoked_as).expanduser()
    if (
        invoked_path.is_file()
        and invoked_path.suffix.lower() != ".py"
        and os.access(invoked_path, os.X_OK)
    ):
        return invoked_path.resolve()

    resolved = command_finder(invoked_as)
    if resolved is None and invoked_as != "a2a-agent":
        resolved = command_finder("a2a-agent")
    if resolved is None:
        raise AutostartError(
            "could not resolve the absolute a2a-agent executable path"
        )
    return Path(resolved).expanduser().resolve()


def build_macos_plist(
    executor: str,
    agent_executable: Path,
    path_value: str,
    state_dir: Path,
) -> bytes:
    """Build a launchd property list without loading it."""

    payload = {
        "Label": MACOS_LABEL,
        "ProgramArguments": [str(agent_executable), "run"],
        "RunAtLoad": True,
        "KeepAlive": True,
        "EnvironmentVariables": {
            "A2A_EXECUTOR": executor,
            "PATH": path_value,
        },
        "StandardOutPath": str(state_dir / "worker.out.log"),
        "StandardErrorPath": str(state_dir / "worker.err.log"),
    }
    return plistlib.dumps(payload, fmt=plistlib.FMT_XML, sort_keys=False)


def build_windows_cmd(
    executor: str,
    agent_executable: Path,
    path_value: str,
) -> str:
    """Build the Windows worker wrapper using cmd-safe assignment syntax."""

    executor_value = _cmd_escape(executor)
    executable_value = _cmd_escape(str(agent_executable))
    path_environment = _cmd_escape(path_value)
    return (
        "@echo off\r\n"
        f'set "A2A_EXECUTOR={executor_value}"\r\n'
        f'set "PATH={path_environment}"\r\n'
        f'"{executable_value}" run\r\n'
    )


def build_windows_startup_cmd(start_worker_path: Path) -> str:
    """Build the per-user Startup entry that launches the worker wrapper."""

    worker_path = _cmd_escape(str(start_worker_path))
    return "@echo off\r\n" f'start "" /min "{worker_path}"\r\n'


def build_linux_service(
    executor: str,
    agent_executable: Path,
    path_value: str,
) -> str:
    """Build a systemd user service that starts at the user default target."""

    executor_environment = _systemd_quote(f"A2A_EXECUTOR={executor}")
    path_environment = _systemd_quote(f"PATH={path_value}")
    executable_value = _systemd_quote(str(agent_executable))
    return (
        "[Unit]\n"
        "Description=A2A worker\n"
        "\n"
        "[Service]\n"
        "Type=simple\n"
        f"Environment={executor_environment}\n"
        f"Environment={path_environment}\n"
        f"ExecStart={executable_value} run\n"
        "Restart=always\n"
        "RestartSec=5\n"
        "\n"
        "[Install]\n"
        "WantedBy=default.target\n"
    )


def _install_macos(
    executor: str,
    home: Path,
    agent_executable: Path,
    path_value: str,
    which: WhichCommand,
    run_command: RunCommand,
) -> AutostartResult:
    state_dir = home / ".a2a-agent"
    state_dir.mkdir(parents=True, exist_ok=True)
    path = home / "Library" / "LaunchAgents" / f"{MACOS_LABEL}.plist"
    content = build_macos_plist(
        executor,
        agent_executable,
        path_value,
        state_dir,
    )
    plutil = which("plutil")
    if plutil is None:
        raise AutostartError("plutil was not found; the launchd plist was not installed")

    def validate(candidate: Path) -> None:
        try:
            run_command(
                [plutil, "-lint", str(candidate)],
                check=True,
                capture_output=True,
                text=True,
            )
        except subprocess.CalledProcessError as exc:
            raise AutostartError(
                _called_process_error_message("plutil validation failed", exc)
            ) from exc
        except (OSError, subprocess.SubprocessError) as exc:
            raise AutostartError(f"plutil validation failed: {exc}") from exc

    backup_path = _atomic_write_with_backup(path, content, validate=validate)
    return AutostartResult(
        action="installed",
        platform="macos",
        path=path,
        enabled=True,
        backup_path=backup_path,
    )


def _install_windows(
    executor: str,
    home: Path,
    agent_executable: Path,
    path_value: str,
    environ: Mapping[str, str],
) -> AutostartResult:
    start_worker_path = home / ".a2a-agent" / "start-worker.cmd"
    start_worker_content = build_windows_cmd(
        executor,
        agent_executable,
        path_value,
    ).encode("utf-8")
    _atomic_write_with_backup(start_worker_path, start_worker_content)

    path = _windows_startup_path(home, environ)
    startup_content = build_windows_startup_cmd(start_worker_path).encode("utf-8")
    backup_path = _atomic_write_with_backup(path, startup_content)
    return AutostartResult(
        action="installed",
        platform="windows",
        path=path,
        enabled=True,
        backup_path=backup_path,
    )


def _install_linux(
    executor: str,
    home: Path,
    agent_executable: Path,
    path_value: str,
    which: WhichCommand,
    run_command: RunCommand,
) -> AutostartResult:
    path = home / ".config" / "systemd" / "user" / LINUX_UNIT_NAME
    content = build_linux_service(executor, agent_executable, path_value).encode(
        "utf-8"
    )
    backup_path = _atomic_write_with_backup(path, content)
    manual_command = f"systemctl --user enable {LINUX_UNIT_NAME}"
    systemctl = which("systemctl")
    if systemctl is None:
        return AutostartResult(
            action="installed",
            platform="linux",
            path=path,
            enabled=False,
            backup_path=backup_path,
            manual_command=manual_command,
        )

    try:
        completed = run_command(
            [systemctl, "--user", "enable", LINUX_UNIT_NAME],
            check=False,
            capture_output=True,
            text=True,
        )
    except subprocess.CalledProcessError as exc:
        raise AutostartError(
            _called_process_error_message("systemctl enable failed", exc)
        ) from exc
    except (OSError, subprocess.SubprocessError):
        completed = None
    enabled = completed is not None and completed.returncode == 0
    return AutostartResult(
        action="installed",
        platform="linux",
        path=path,
        enabled=enabled,
        backup_path=backup_path,
        manual_command=None if enabled else manual_command,
    )


def _uninstall_windows(
    home: Path,
    environ: Mapping[str, str],
) -> AutostartResult:
    path = _windows_startup_path(home, environ)
    path.unlink(missing_ok=True)
    (home / ".a2a-agent" / "start-worker.cmd").unlink(missing_ok=True)
    return AutostartResult(
        action="uninstalled",
        platform="windows",
        path=path,
        enabled=False,
    )


def _uninstall_linux(
    home: Path,
    which: WhichCommand,
    run_command: RunCommand,
) -> AutostartResult:
    path = home / ".config" / "systemd" / "user" / LINUX_UNIT_NAME
    manual_command = f"systemctl --user disable {LINUX_UNIT_NAME}"
    systemctl = which("systemctl")
    completed: subprocess.CompletedProcess[str] | None = None
    if systemctl is not None:
        try:
            completed = run_command(
                [systemctl, "--user", "disable", LINUX_UNIT_NAME],
                check=False,
                capture_output=True,
                text=True,
            )
        except subprocess.CalledProcessError as exc:
            raise AutostartError(
                _called_process_error_message("systemctl disable failed", exc)
            ) from exc
        except (OSError, subprocess.SubprocessError):
            pass
    disabled = completed is not None and completed.returncode == 0
    if disabled:
        path.unlink(missing_ok=True)
    return AutostartResult(
        action="uninstalled",
        platform="linux",
        path=path,
        enabled=not disabled,
        manual_command=None if disabled else manual_command,
    )


def _normalized_system(system_name: str | None) -> str:
    current = (platform.system() if system_name is None else system_name).lower()
    if current in {"darwin", "mac", "macos"}:
        return "macos"
    if current in {"windows", "win32", "nt"}:
        return "windows"
    if current == "linux":
        return "linux"
    return current


def _home_path(
    system_name: str,
    home: str | Path | None,
    environ: Mapping[str, str],
) -> Path:
    if home is not None:
        return Path(home).expanduser().resolve()
    if system_name == "windows" and environ.get("USERPROFILE"):
        return Path(environ["USERPROFILE"]).expanduser().resolve()
    return Path.home().resolve()


def _windows_startup_path(home: Path, environ: Mapping[str, str]) -> Path:
    appdata = environ.get("APPDATA")
    base = (
        Path(appdata).expanduser().resolve()
        if appdata
        else home / "AppData" / "Roaming"
    )
    return (
        base
        / "Microsoft"
        / "Windows"
        / "Start Menu"
        / "Programs"
        / "Startup"
        / WINDOWS_STARTUP_FILE_NAME
    )


def _path_with_executor(
    executor: str,
    current_path: str,
    system_name: str,
    which: WhichCommand,
) -> str:
    program = _EXECUTOR_PROGRAMS.get(executor, executor)
    if executor == "echo":
        return current_path
    executable = which(program)
    if executable is None:
        raise AutostartError(
            f"could not find the {program} executable required by executor {executor}"
        )

    separator = ";" if system_name == "windows" else ":"
    executable_dir = str(Path(executable).expanduser().resolve().parent)
    entries = [entry for entry in current_path.split(separator) if entry]
    compare = os.path.normcase if system_name == "windows" else lambda value: value
    if all(compare(entry) != compare(executable_dir) for entry in entries):
        entries.insert(0, executable_dir)
    return separator.join(entries) if entries else executable_dir


def _atomic_write_with_backup(
    path: Path,
    content: bytes,
    *,
    validate: Callable[[Path], None] | None = None,
) -> Path | None:
    path.parent.mkdir(parents=True, exist_ok=True)
    descriptor, temporary_name = tempfile.mkstemp(
        prefix=f".{path.name}.",
        suffix=".tmp",
        dir=path.parent,
    )
    temporary_path = Path(temporary_name)
    try:
        with os.fdopen(descriptor, "wb") as file:
            file.write(content)
            file.flush()
            os.fsync(file.fileno())
        if validate is not None:
            validate(temporary_path)

        backup_path = None
        if path.exists():
            backup_path = path.with_name(f"{path.name}.bak")
            _copy_with_retry(path, backup_path)
        _replace_with_retry(temporary_path, path)
        return backup_path
    finally:
        try:
            temporary_path.unlink(missing_ok=True)
        except OSError:
            if sys.exc_info()[0] is None:
                raise


def _replace_with_retry(
    source: Path,
    destination: Path,
    *,
    replace: Callable[[Path, Path], object] = os.replace,
    sleep: Callable[[float], object] = time.sleep,
    is_windows: bool | None = None,
) -> None:
    """Replace a file, tolerating transient Windows file locks."""

    _run_with_windows_file_retry(
        lambda: replace(source, destination),
        operation_name="replace",
        sleep=sleep,
        is_windows=is_windows,
    )


def _copy_with_retry(
    source: Path,
    destination: Path,
    *,
    copy: Callable[[Path, Path], object] = shutil.copy2,
    sleep: Callable[[float], object] = time.sleep,
    is_windows: bool | None = None,
) -> None:
    """Copy a file, tolerating transient Windows file locks."""

    _run_with_windows_file_retry(
        lambda: copy(source, destination),
        operation_name="backup copy",
        sleep=sleep,
        is_windows=is_windows,
    )


def _run_with_windows_file_retry(
    operation: Callable[[], object],
    *,
    operation_name: str,
    sleep: Callable[[float], object],
    is_windows: bool | None,
) -> None:
    """Retry a file operation only for transient Windows access failures."""

    should_retry = os.name == "nt" if is_windows is None else is_windows
    if not should_retry:
        operation()
        return

    for delay in _WINDOWS_FILE_RETRY_DELAYS:
        try:
            operation()
        except OSError:
            sleep(delay)
        else:
            return

    try:
        operation()
    except OSError:
        print(
            "Windows "
            f"{operation_name} failed after retries; antivirus real-time scanning "
            "may have locked the file.",
            file=sys.stderr,
        )
        raise


def _called_process_error_message(
    action: str,
    exc: subprocess.CalledProcessError,
) -> str:
    details = [
        f"{name}: {value}"
        for name, value in (("stderr", exc.stderr), ("stdout", exc.stdout))
        if value
    ]
    if details:
        return f"{action}: {exc} ({'; '.join(details)})"
    return f"{action}: {exc}"


def _cmd_escape(value: str) -> str:
    if "\x00" in value or "\r" in value or "\n" in value:
        raise AutostartError("Windows autostart values must be single-line strings")
    return value.replace("%", "%%")


def _systemd_quote(value: str) -> str:
    if "\x00" in value or "\r" in value or "\n" in value:
        raise AutostartError("systemd autostart values must be single-line strings")
    escaped = (
        value.replace("\\", "\\\\")
        .replace('"', '\\"')
        .replace("%", "%%")
    )
    return f'"{escaped}"'
