"""Focused Linux inotify watcher for an atomic cron/jobs.json replacement."""

from __future__ import annotations

import ctypes
import os
import select
from dataclasses import dataclass
from pathlib import Path
from types import TracebackType
from typing import Final, Literal, Protocol, Self, final, runtime_checkable

_IN_CLOSE_WRITE: Final = 0x00000008
_IN_MOVED_TO: Final = 0x00000080
_IN_NONBLOCK: Final = 0x00000800
_IN_MASK: Final = _IN_CLOSE_WRITE | _IN_MOVED_TO
_EVENT_HEADER_SIZE: Final = 16


@runtime_checkable
class _InotifyLibrary(Protocol):
    """The tiny typed libc surface used by this watcher."""

    def inotify_init1(self, flags: int) -> int: ...

    def inotify_add_watch(self, descriptor: int, path: bytes, mask: int) -> int: ...


class CronWatchError(RuntimeError):
    """The exact cron state-change event could not be safely observed."""


@dataclass(frozen=True, slots=True)
class CronJobsEvent:
    """The only accepted cron event: an atomic jobs.json replacement/write."""

    name: str
    replacement: bool


@final
class CronJobsEventWatcher:
    """One-shot inotify subscription; no polling or wall-clock sleeps."""

    def __init__(self, directory: Path) -> None:
        self._directory = directory
        self._descriptor: int | None = None
        self._watch: int | None = None

    def __enter__(self) -> Self:
        return self

    def __exit__(
        self,
        error_type: type[BaseException] | None,
        error: BaseException | None,
        traceback: TracebackType | None,
    ) -> Literal[False]:
        del error_type, error, traceback
        self.close()
        return False

    def arm(self) -> None:
        """Subscribe before service start to cron directory replacement events."""
        if self._descriptor is not None:
            raise CronWatchError("already_armed")
        descriptor = _inotify_init()
        watch = _inotify_add_watch(descriptor, self._directory, _IN_MASK)
        self._descriptor = descriptor
        self._watch = watch

    def wait(self, *, timeout_seconds: int) -> CronJobsEvent:
        """Await one matching event within the sealed <=120 second bound."""
        descriptor = self._descriptor
        if descriptor is None:
            raise CronWatchError("not_armed")
        if not 1 <= timeout_seconds <= 120:
            raise CronWatchError("timeout_bound")
        readable, _, _ = select.select((descriptor,), (), (), timeout_seconds)
        if not readable:
            raise CronWatchError("timeout")
        payload = os.read(descriptor, 65_536)
        return _parse_event(payload, self._watch)

    def close(self) -> None:
        """Release the one-shot descriptor once confirmation ends."""
        descriptor, self._descriptor = self._descriptor, None
        self._watch = None
        if descriptor is not None:
            os.close(descriptor)


def _inotify_init() -> int:
    descriptor = _libc().inotify_init1(os.O_CLOEXEC | _IN_NONBLOCK)
    if descriptor < 0:
        raise CronWatchError("inotify_init")
    return descriptor


def _inotify_add_watch(descriptor: int, directory: Path, mask: int) -> int:
    watch = _libc().inotify_add_watch(descriptor, os.fsencode(directory), mask)
    if watch < 0:
        os.close(descriptor)
        raise CronWatchError("inotify_watch")
    return watch


def _libc() -> _InotifyLibrary:
    library = ctypes.CDLL("libc.so.6", use_errno=True)
    init: object = library.inotify_init1
    add_watch: object = library.inotify_add_watch
    setattr(library, "inotify_init1", init)
    setattr(library, "inotify_add_watch", add_watch)
    candidate: object = library
    if not isinstance(candidate, _InotifyLibrary):
        raise CronWatchError("inotify_library")
    return candidate


def _parse_event(payload: bytes, watch: int | None) -> CronJobsEvent:
    if watch is None or len(payload) < _EVENT_HEADER_SIZE:
        raise CronWatchError("event")
    offset = 0
    while offset + _EVENT_HEADER_SIZE <= len(payload):
        watched = int.from_bytes(payload[offset:offset + 4], "little", signed=True)
        mask = int.from_bytes(payload[offset + 4:offset + 8], "little")
        name_size = int.from_bytes(payload[offset + 12:offset + 16], "little")
        end = offset + _EVENT_HEADER_SIZE + name_size
        if end > len(payload):
            raise CronWatchError("event")
        name = payload[offset + _EVENT_HEADER_SIZE:end].rstrip(b"\0")
        if watched == watch and name == b"jobs.json" and mask & _IN_MASK:
            return CronJobsEvent("jobs.json", bool(mask & _IN_MOVED_TO))
        offset = end
    raise CronWatchError("wrong_event")
