"""
Scoped filesystem broker for plugin runtime requests.
"""
from __future__ import annotations

import os
import secrets
import threading
import time
from dataclasses import dataclass

from utils.path_safety import safe_resolve_path


@dataclass
class FsHandle:
    handle: str
    plugin_slug: str
    resolved_path: str
    allow_write: bool
    expires_at: float


class PluginFsBroker:
    def __init__(self) -> None:
        self._lock = threading.Lock()
        self._handles: dict[str, FsHandle] = {}

    def issue_handle(
        self,
        *,
        plugin_slug: str,
        workspace_path: str,
        requested_path: str,
        allow_write: bool,
        ttl_s: float = 120.0,
    ) -> str:
        resolved, err = safe_resolve_path(workspace_path, requested_path, allow_write=allow_write)
        if not resolved or err:
            raise ValueError(err or "path not allowed")
        token = secrets.token_urlsafe(24)
        with self._lock:
            self._handles[token] = FsHandle(
                handle=token,
                plugin_slug=str(plugin_slug),
                resolved_path=os.path.abspath(resolved),
                allow_write=bool(allow_write),
                expires_at=time.time() + max(1.0, float(ttl_s)),
            )
        return token

    def resolve_handle(self, *, plugin_slug: str, handle: str, want_write: bool = False) -> str:
        # Look up before gc(): gc() drops expired entries and would turn "expired" into "unknown".
        with self._lock:
            fh = self._handles.get(handle)
        if fh is None:
            raise ValueError("unknown filesystem handle")
        if fh.plugin_slug != str(plugin_slug):
            raise ValueError("filesystem handle not owned by plugin")
        now = time.time()
        if now > fh.expires_at:
            with self._lock:
                self._handles.pop(handle, None)
            raise ValueError("filesystem handle expired")
        if want_write and not fh.allow_write:
            raise ValueError("filesystem handle is read-only")
        self.gc()
        return fh.resolved_path

    def gc(self) -> None:
        now = time.time()
        with self._lock:
            stale = [k for k, v in self._handles.items() if v.expires_at < now]
            for k in stale:
                self._handles.pop(k, None)

