from __future__ import annotations

import atexit
import errno
import os
import signal
import subprocess
import sys
import time
from typing import Callable

from autosana_cli.models import Session


def is_pid_running(pid: int | None) -> bool:
    if not pid or pid <= 0:
        return False
    if sys.platform == "win32":
        import ctypes

        kernel32 = ctypes.windll.kernel32
        PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
        STILL_ACTIVE = 259
        handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
        if not handle:
            # ERROR_ACCESS_DENIED (5) means process exists but we can't open it
            return ctypes.GetLastError() == 5
        try:
            exit_code = ctypes.c_ulong()
            if kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
                return exit_code.value == STILL_ACTIVE
            return False
        finally:
            kernel32.CloseHandle(handle)
    # POSIX
    try:
        os.kill(pid, 0)
    except OSError as exc:
        if exc.errno == errno.ESRCH:
            return False
        return True
    return True


def terminate_pid(pid: int | None, *, timeout: float = 5.0) -> None:
    if not is_pid_running(pid):
        return
    assert pid is not None
    try:
        os.kill(pid, signal.SIGTERM)  # On Windows this calls TerminateProcess
    except (ProcessLookupError, OSError):
        return

    if sys.platform != "win32":
        # POSIX: wait, then SIGKILL if still alive
        deadline = time.time() + timeout
        while time.time() < deadline:
            if not is_pid_running(pid):
                return
            time.sleep(0.1)
        try:
            os.kill(pid, signal.SIGKILL)
        except (ProcessLookupError, OSError):
            return


def detach_kwargs(detach: bool) -> dict:
    """Return platform-appropriate kwargs for subprocess detachment."""
    if not detach:
        return {}
    if sys.platform == "win32":
        return {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}
    return {"start_new_session": True}


def terminate_process(proc: subprocess.Popen[bytes] | subprocess.Popen[str] | None, *, timeout: float = 5.0) -> None:
    if proc is None or proc.poll() is not None:
        return
    proc.terminate()
    try:
        proc.wait(timeout=timeout)
    except subprocess.TimeoutExpired:
        proc.kill()
        proc.wait(timeout=1)


def stop_session(session: Session) -> None:
    terminate_pid(session.tunnel_pid)
    terminate_pid(session.appium_pid)


def register_cleanup(cleanup: Callable[[], None]) -> None:
    called = False

    def guarded_cleanup() -> None:
        nonlocal called
        if called:
            return
        called = True
        cleanup()

    atexit.register(guarded_cleanup)

    def handler(_signum: int, _frame: object) -> None:
        guarded_cleanup()
        raise SystemExit(0)

    signal.signal(signal.SIGINT, handler)
    if sys.platform == "win32":
        signal.signal(signal.SIGBREAK, handler)  # Ctrl+Break on Windows
    else:
        signal.signal(signal.SIGTERM, handler)
