import os
import threading
import time
from dataclasses import dataclass, field, replace
from pathlib import Path

from engine.memo import Memo
from engine.stored import read_json
from providers import PROVIDERS
from providers.payload import Hook
from resources.fields import Loaded
from runner import chat_mirror
from runner.hooks import answer

REPLAYING = threading.Lock()
SLICE, SLICE_WITHIN, SLICE_PAUSE = 25, 0.05, 0.02
WAITING = Memo()


@dataclass(frozen=True)
class Spooled(Loaded):
    """A hook event the server did not answer, kept by the hook with who sent it."""

    agent: str = ""
    env: str = ""
    pid: int = 0
    body: dict = field(default_factory=dict)


def kept_stamp(root: Path) -> int:
    try:
        return os.stat(chat_mirror.unsent(root)).st_mtime_ns
    except OSError:
        return 0


def waiting_events(root: Path) -> list[Path]:
    """The events kept while the server could not answer, oldest first, listed again only when the folder that holds them changed."""
    return WAITING.get(str(root), kept_stamp(root), lambda: sorted(chat_mirror.unsent(root).glob("*.json"), key=lambda f: (f.stat().st_mtime_ns, f.name)))


def replay(root: Path, most: int = 0, within: float = 0.0) -> int:
    """Takes every event kept while the server could not answer (the oldest `most` of them, when given, for as long as `within` seconds allow, the first always), oldest first: a shown message goes to the chat, any other event is handled as if it had just arrived, at the time the hook kept it."""
    if not REPLAYING.acquire(blocking=False):
        return 0
    handled = 0
    try:
        waiting = waiting_events(root)
        until = time.monotonic() + within
        for kept in waiting[:most] if most else waiting:
            handle(root, kept)
            handled += 1
            if within and time.monotonic() >= until:
                break
    finally:
        REPLAYING.release()
    return handled


def spooled(root: Path) -> bool:
    """Whether an event waits in the spool, from the same listing replay takes, so the server's loop looks at the folder's stamp and not at its files."""
    return bool(waiting_events(root))


def drain(root: Path) -> None:
    """Replays what is kept in short slices with a pause between them, so the server stays free for the hooks and commands that arrive meanwhile."""
    while replay(root, SLICE, SLICE_WITHIN):
        time.sleep(SLICE_PAUSE)


def handle(root: Path, kept: Path) -> None:
    spooled = Spooled.from_json(read_json(kept, dict, {}))
    provider = PROVIDERS.get(spooled.agent)
    if provider is None or not spooled.body:
        chat_mirror.replay_file(root, kept)
        return
    if provider.display_chunk(spooled.body) is not None:
        chat_mirror.replay_file(root, kept, spooled.body)
        return
    at = kept.stat().st_mtime
    kept.unlink(missing_ok=True)
    answer(provider(), root, replace(Hook.read(spooled.body, provider.tool_kinds), at=at), spooled.pid, spooled.env)
