import ctypes
import logging
import threading
import time
from typing import Callable

from soniclue_basekit.terminable_thread import TerminableThread

logger = logging.getLogger(__name__)

_PR_SET_NAME = 15


def _try_set_thread_name(name: str | None) -> None:
    """Sets the OS-visible name of the current thread via prctl.

    References:
        https://stackoverflow.com/a/51668422
    """
    if not name:
        return
    lib = "libcap.so.2"
    try:
        libcap = ctypes.CDLL(lib)
    except OSError:
        logger.warning("Library %s not found, unable to set thread name to %s", lib, name)
        return
    # Thread name has a 16-character limit on Linux (including terminating `\0`);
    # the kernel silently truncates any excess.
    # https://man7.org/linux/man-pages/man2/prctl.2.html
    libcap.prctl(_PR_SET_NAME, name.encode())


class DaemonThread(TerminableThread):
    """Daemon thread skeleton with pause/resume support and optional error notification.

    Subclasses override `_process` to implement per-iteration logic.
    """

    def __init__(
        self,
        name: str | None = None,
        interval_sec: float = 0,
        error_notifier: Callable[[str, Exception], None] | None = None,
    ) -> None:
        """Constructor.

        Args:
            name: Customized thread name.
            interval_sec: Seconds to pause between iterations; 0 means busy-loop.
            error_notifier: Called with (message, exception) whenever `_process` raises.
        """
        assert interval_sec >= 0
        assert error_notifier is None or callable(error_notifier)
        super().__init__(name=name)
        self._processed_count = 0
        self._interval_sec = interval_sec
        self._error_notifier = error_notifier
        self._paused = False
        self._pause_cond = threading.Condition()
        logger.info("Created daemon thread: %s", name)

    def _process(self) -> None:
        """Processes one iteration; override in subclasses."""

    @property
    def processed_count(self) -> int:
        """Number of completed (non-raising) `_process` calls."""
        return self._processed_count

    def pause(self) -> None:
        """Suspends iteration until `resume` is called."""
        self._paused = True
        logger.info(self._format_log("Paused"))

    def resume(self) -> None:
        """Resumes iteration after a pause."""
        self._paused = False
        logger.info(self._format_log("Resumed"))

    def wake_up(self) -> None:
        """Interrupts the inter-iteration wait immediately."""
        with self._pause_cond:
            self._pause_cond.notify()

    def run(self) -> None:
        """Thread entry point; calls `_process` in a loop until terminated."""
        logger.info(self._format_log("Started"))
        _try_set_thread_name(self._log_name)
        while not self.is_terminating():
            try:
                if not self._paused:
                    self._process()
                    self._processed_count += 1
            except Exception as ex:
                msg = self._format_log(f"unhandled exception: {type(ex).__name__}: {ex}")
                logger.exception(msg)
                if self._error_notifier is not None:
                    self._error_notifier(msg, ex)
            if not self._interval_sec:
                # Yield to other threads and release the GIL in busy-loop mode.
                time.sleep(1e-6)
            with self._pause_cond:
                self._pause_cond.wait(self._interval_sec)
        logger.info(self._format_log("Terminated"))

    def set_terminating(self) -> None:
        """Requests termination and wakes the thread from any inter-iteration wait."""
        super().set_terminating()
        self.wake_up()
