"""Cross-process advisory lock built on O_CREAT|O_EXCL.

Why not "check then create": `os.path.exists()` followed by a write is a TOCTOU
race — two processes can both see "no lock" and both proceed. Creating the file
exclusively *is* the claim, so exactly one process wins.

Stale locks are reaped by **age**, never by PID liveness alone: PIDs are reused,
and a reaper that trusts a recycled PID will delete a live lock.
"""

import json
import os
import time
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path

DEFAULT_STALE_AFTER_SECONDS = 120.0
LOCK_FILE_MODE = 0o600


def acquire(path: Path, *, stale_after: float = DEFAULT_STALE_AFTER_SECONDS) -> bool:
    """Try once to claim `path`. Reaps a stale claim and retries once.

    Returns True when this process holds the lock, False when someone else does.
    On an unexpected OS error the lock is treated as acquired (fail open): a
    duplicate login is a far cheaper failure than a permanent deadlock.
    """
    path.parent.mkdir(parents=True, exist_ok=True)
    for is_retry in (False, True):
        try:
            fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, LOCK_FILE_MODE)
        except FileExistsError:
            if is_retry or not _reap_if_stale(path, stale_after):
                return False
            continue
        except OSError:
            return True  # fail open
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump({"pid": os.getpid(), "acquired_at": time.time()}, handle)
        return True
    return False


def release(path: Path) -> None:
    try:
        os.unlink(path)
    except OSError:
        pass


@contextmanager
def hold(path: Path, *, stale_after: float = DEFAULT_STALE_AFTER_SECONDS) -> Iterator[bool]:
    """Context manager yielding whether the lock was acquired.

    The body always runs — the caller decides what losing the race means. Only
    the winner releases, so a loser can never delete the winner's claim.
    """
    acquired = acquire(path, stale_after=stale_after)
    try:
        yield acquired
    finally:
        if acquired:
            release(path)


def _reap_if_stale(path: Path, stale_after: float) -> bool:
    """Delete `path` when it is older than `stale_after`. Returns True if reaped."""
    try:
        age = time.time() - path.stat().st_mtime
    except OSError:
        return False
    if age < stale_after:
        return False
    try:
        os.unlink(path)
    except OSError:
        return False
    return True
