from __future__ import annotations

import ctypes
import ctypes.util
import os
import select
import time
from pathlib import Path
from queue import Queue

from agent_guard.events import Event


IN_CREATE = 0x00000100
IN_MODIFY = 0x00000002
IN_CLOSE_WRITE = 0x00000008
IN_MOVED_TO = 0x00000080
MASK = IN_CREATE | IN_MODIFY | IN_CLOSE_WRITE | IN_MOVED_TO

# An atomic replace (`ln -sfn`, `install`, `os.replace`) creates a randomly named entry and
# renames it over the target, so the created name never exists once the operation settles.
# Reporting it named a path nobody can inspect and buried real changes; a path is reported
# only if it is still there after the write settles.
SETTLE_SECONDS = 1.0


def flush_settled(queue: Queue, pending: dict[str, float], now: float, settle: float) -> None:
    for changed in [p for p, seen_at in pending.items() if now - seen_at >= settle]:
        del pending[changed]
        if os.path.lexists(changed):
            queue.put(Event("notice", "fswatch", f"Sensitive path changed: {changed}", actions=["dismiss"]))


def follow(queue: Queue, paths: list[str], stop) -> None:
    libc_path = ctypes.util.find_library("c")
    if not libc_path:
        return
    libc = ctypes.CDLL(libc_path, use_errno=True)
    fd = libc.inotify_init1(os.O_NONBLOCK)
    if fd < 0:
        return
    watches: dict[int, Path] = {}
    try:
        for raw in paths:
            path = Path(os.path.expanduser(raw))
            if not path.exists():
                continue
            wd = libc.inotify_add_watch(fd, bytes(path), MASK)
            if wd >= 0:
                watches[wd] = path
        pending: dict[str, float] = {}
        while not stop.is_set():
            ready, _, _ = select.select([fd], [], [], SETTLE_SECONDS / 2)
            if ready:
                data = os.read(fd, 65536)
                offset = 0
                while offset + 16 <= len(data):
                    wd = int.from_bytes(data[offset:offset + 4], "little", signed=True)
                    length = int.from_bytes(data[offset + 12:offset + 16], "little")
                    name = data[offset + 16:offset + 16 + length].split(b"\0", 1)[0].decode(errors="replace")
                    base = watches.get(wd)
                    if base:
                        pending[str(base / name) if name else str(base)] = time.monotonic()
                    offset += 16 + length
            flush_settled(queue, pending, time.monotonic(), SETTLE_SECONDS)
    finally:
        os.close(fd)
