from __future__ import annotations

import os
import signal
import time
from types import FrameType
from typing import Callable

from cybervisor.logging import CybervisorLogger


class SignalHandler:
    def __init__(self, logger: CybervisorLogger, log_file: str = ".cybervisor/logs/cybervisor.log.jsonl") -> None:
        self.logger = logger
        self.log_file = log_file
        self.pid: int | None = None
        self.process_group_id: int | None = None
        self.interrupted = False
        self.signal_number: int | None = None
        self._cleanup_callback: Callable[[], str] | None = None

    def _process_exists(self) -> bool:
        if self.process_group_id is not None and hasattr(os, "killpg"):
            try:
                os.killpg(self.process_group_id, 0)
            except ProcessLookupError:
                return False
            except PermissionError:
                return True
            return True

        if self.pid is None:
            return False
        try:
            os.kill(self.pid, 0)
        except ProcessLookupError:
            return False
        except PermissionError:
            return True
        return True

    def _terminate_active_process(self, signum: int) -> None:
        if self.pid is None:
            return

        target = self.process_group_id if self.process_group_id is not None and hasattr(os, "killpg") else self.pid
        send_signal = os.killpg if self.process_group_id is not None and hasattr(os, "killpg") else os.kill
        target_kind = "subprocess group" if self.process_group_id is not None and hasattr(os, "killpg") else "subprocess"
        target_value = self.process_group_id if self.process_group_id is not None and hasattr(os, "killpg") else self.pid

        try:
            send_signal(target, signum)
            self.logger.log_to_stderr(
                f"Sent {signal.Signals(signum).name} to active {target_kind} "
                f"{'pgid' if target_kind == 'subprocess group' else 'pid'}={target_value}",
                level="warning",
            )
        except ProcessLookupError:
            self.logger.log_to_stderr(f"Subprocess pid={self.pid} already exited", level="warning")
            return

        if signum != signal.SIGKILL:
            deadline = time.monotonic() + 2.0
            while time.monotonic() < deadline:
                if not self._process_exists():
                    return
                time.sleep(0.05)
            if self._process_exists():
                self._terminate_active_process(signal.SIGTERM if signum == signal.SIGINT else signal.SIGKILL)

    def register(self) -> None:
        signal.signal(signal.SIGINT, self._handle_signal)
        signal.signal(signal.SIGTERM, self._handle_signal)

    def set_pid(self, pid: int | None, process_group_id: int | None = None) -> None:
        self.pid = pid
        self.process_group_id = process_group_id

    def set_cleanup_callback(self, callback: Callable[[], str] | None) -> None:
        self._cleanup_callback = callback

    def _handle_signal(self, signum: int, _frame: FrameType | None) -> None:
        self.interrupted = True
        self.signal_number = signum
        cleanup_result = "skipped"
        if self.pid is not None:
            self._terminate_active_process(signum)
        if self._cleanup_callback is not None:
            try:
                cleanup_result = self._cleanup_callback()
            except Exception as exc:
                cleanup_result = f"failed:{exc}"
                self.logger.log_to_stderr(f"Cleanup during interrupt failed: {exc}", level="error")
        self.logger.log_to_stderr(f"Received signal {signum}; shutting down", level="error")
        self.logger.log_to_json(
            self.logger.make_entry(
                "system",
                "Interrupted",
                f"Received signal {signum}; shutting down",
                cleanup_result=cleanup_result,
            ),
            self.log_file,
        )
        raise SystemExit(130)
