"""Single-instance file lock for the engine (``docs/02-architecture.md``).

Acquires an exclusive ``fcntl.flock`` on a sentinel file at boot. A
second container/process trying to start while another holds the lock
gets :class:`LockError` immediately and exits before doing any I/O.

The lock is released either on context exit or when the process dies
(the OS releases ``flock`` automatically), so a hard crash cannot
permanently wedge the system.
"""

from __future__ import annotations

import fcntl
import logging
import os
from pathlib import Path
from types import TracebackType

__all__ = ["EngineLock", "LockError"]


_log = logging.getLogger("cerbero_bite.runtime.lockfile")


class LockError(RuntimeError):
    """Raised when another instance already holds the engine lock."""


class EngineLock:
    """Context manager around a per-instance file lock.

    Usage::

        with EngineLock(Path("data/.lockfile")) as lock:
            ...

    On enter, writes the current PID to the lock file so an operator
    can identify the running instance. On exit, releases the lock.
    """

    def __init__(self, path: Path | str) -> None:
        self._path = Path(path)
        self._fh: object | None = None  # actual type: io.TextIOWrapper

    @property
    def path(self) -> Path:
        return self._path

    def acquire(self) -> None:
        self._path.parent.mkdir(parents=True, exist_ok=True)
        # ``r+`` would fail if the file does not exist yet; use ``a+``
        # then seek to overwrite the contents on each acquire.
        fh = self._path.open("a+", encoding="utf-8")
        try:
            fcntl.flock(fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError as exc:
            fh.close()
            raise LockError(
                f"another Cerbero Bite instance holds {self._path}; "
                f"check the running container or remove the file if stale"
            ) from exc

        fh.seek(0)
        fh.truncate()
        fh.write(f"{os.getpid()}\n")
        fh.flush()
        self._fh = fh
        _log.info("engine lock acquired: pid=%d path=%s", os.getpid(), self._path)

    def release(self) -> None:
        if self._fh is None:
            return
        try:
            fcntl.flock(self._fh.fileno(), fcntl.LOCK_UN)  # type: ignore[attr-defined]
        finally:
            self._fh.close()  # type: ignore[attr-defined]
            self._fh = None
        _log.info("engine lock released: %s", self._path)

    def __enter__(self) -> EngineLock:
        self.acquire()
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        self.release()
