#!/usr/bin/env python3
"""Private monotonic runtime-generation state shared by runtime hook processes."""

from __future__ import annotations

import fcntl
import os
import stat
from collections.abc import Mapping
from pathlib import Path
from typing import Final

MAX_GENERATION: Final = (1 << 63) - 1
MAX_GENERATION_BYTES: Final = 32


def parse_generation(value: object) -> int:
    if not isinstance(value, str) or not value.isascii() or not value.isdigit():
        raise ValueError("invalid runtime generation")
    generation = int(value)
    if generation < 0 or generation > MAX_GENERATION:
        raise ValueError("invalid runtime generation")
    return generation


def _validate_descriptor(descriptor: int) -> None:
    metadata = os.fstat(descriptor)
    if not stat.S_ISREG(metadata.st_mode):
        raise ValueError("runtime generation state is not a regular file")
    if metadata.st_uid != os.geteuid():
        raise ValueError("runtime generation state has the wrong owner")
    if stat.S_IMODE(metadata.st_mode) & 0o077:
        raise ValueError("runtime generation state permissions are not private")
    if metadata.st_size > MAX_GENERATION_BYTES:
        raise ValueError("runtime generation state is oversized")


def _read_descriptor(descriptor: int) -> int:
    os.lseek(descriptor, 0, os.SEEK_SET)
    raw = os.read(descriptor, MAX_GENERATION_BYTES + 1)
    if len(raw) > MAX_GENERATION_BYTES:
        raise ValueError("runtime generation state is oversized")
    try:
        return parse_generation(raw.decode("ascii").strip())
    except UnicodeDecodeError as exc:
        raise ValueError("invalid runtime generation state") from exc


def _write_descriptor(descriptor: int, generation: int) -> None:
    payload = f"{generation}\n".encode("ascii")
    os.lseek(descriptor, 0, os.SEEK_SET)
    os.ftruncate(descriptor, 0)
    remaining = memoryview(payload)
    while remaining:
        written = os.write(descriptor, remaining)
        if written <= 0:
            raise OSError("runtime generation write made no progress")
        remaining = remaining[written:]
    os.fsync(descriptor)


def initialize_runtime_generation(path: Path, generation: int) -> None:
    if generation < 0 or generation > MAX_GENERATION:
        raise ValueError("invalid runtime generation")
    descriptor = os.open(
        path,
        os.O_RDWR | os.O_CREAT | os.O_TRUNC | os.O_CLOEXEC | os.O_NOFOLLOW,
        0o600,
    )
    try:
        os.fchmod(descriptor, 0o600)
        _validate_descriptor(descriptor)
        _write_descriptor(descriptor, generation)
    finally:
        os.close(descriptor)


def read_runtime_generation(environ: Mapping[str, str]) -> int:
    state_path = environ.get("MOSAIC_LEASE_GENERATION_FILE")
    if not state_path:
        return parse_generation(environ["MOSAIC_RUNTIME_GENERATION"])
    descriptor = os.open(state_path, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW)
    try:
        _validate_descriptor(descriptor)
        return _read_descriptor(descriptor)
    finally:
        os.close(descriptor)


def bump_runtime_generation(environ: Mapping[str, str]) -> int:
    state_path = environ.get("MOSAIC_LEASE_GENERATION_FILE")
    if not state_path:
        raise ValueError("runtime generation file is required for same-PID rollover")
    descriptor = os.open(state_path, os.O_RDWR | os.O_CLOEXEC | os.O_NOFOLLOW)
    try:
        fcntl.flock(descriptor, fcntl.LOCK_EX)
        _validate_descriptor(descriptor)
        current = _read_descriptor(descriptor)
        if current >= MAX_GENERATION:
            raise ValueError("runtime generation exhausted")
        generation = current + 1
        _write_descriptor(descriptor, generation)
        return generation
    finally:
        os.close(descriptor)
