"""File-level locking for parallel swarm workers.

Prevents parallel DaveLoop agents from corrupting shared files by providing
file-level locks via atomic file creation. Works on both Windows and Unix.
"""
import json
import os
import time
from contextlib import contextmanager
from pathlib import Path


class FileLockManager:
    """Manages file-level locks for parallel swarm workers.

    Lock state is stored in .boris/locks/ in the project directory.
    Each lock is an atomic file recording: owner (milestone ID), timestamp, file path.
    """

    def __init__(self, project_dir: str):
        self.lock_dir = Path(project_dir) / ".boris" / "locks"
        self.lock_dir.mkdir(parents=True, exist_ok=True)

    def _lock_path(self, filepath: str) -> Path:
        """Get the lock file path for a given source file."""
        normalized = os.path.normpath(filepath)
        # Replace path separators with underscores for flat lock directory
        safe_name = normalized.replace(os.sep, "_").replace("/", "_").replace("\\", "_")
        return self.lock_dir / f"{safe_name}.lock"

    @contextmanager
    def lock_file(self, filepath: str, owner: str, timeout: int = 30):
        """Acquire a lock on a file. Blocks until available or timeout.

        Args:
            filepath: The file to lock (relative or absolute path).
            owner: Identifier for the lock owner (e.g. milestone ID).
            timeout: Max seconds to wait for the lock.

        Raises:
            TimeoutError: If the lock cannot be acquired within timeout.
        """
        lock_path = self._lock_path(filepath)
        start = time.time()

        while True:
            try:
                # Atomic create-or-fail: 'x' mode fails if file exists
                fd = open(lock_path, "x", encoding="utf-8")
                fd.write(json.dumps({
                    "owner": owner,
                    "file": filepath,
                    "time": time.time(),
                }))
                fd.close()
                break
            except FileExistsError:
                if time.time() - start > timeout:
                    # Read who holds the lock for better error messages
                    try:
                        holder = json.loads(lock_path.read_text(encoding="utf-8"))
                        holder_info = f" (held by {holder.get('owner', 'unknown')})"
                    except Exception:
                        holder_info = ""
                    raise TimeoutError(
                        f"Could not acquire lock on {filepath}{holder_info} "
                        f"after {timeout}s"
                    )
                time.sleep(0.5)

        try:
            yield
        finally:
            try:
                lock_path.unlink()
            except FileNotFoundError:
                pass

    def get_locked_files(self) -> dict:
        """Return dict of currently locked files and their owners."""
        locks = {}
        for lock_file in self.lock_dir.glob("*.lock"):
            try:
                data = json.loads(lock_file.read_text(encoding="utf-8"))
                original_file = data.get("file", lock_file.stem.replace("_", os.sep))
                locks[original_file] = data.get("owner", "unknown")
            except (json.JSONDecodeError, KeyError, OSError):
                pass
        return locks

    def is_locked(self, filepath: str) -> bool:
        """Check if a file is currently locked."""
        lock_path = self._lock_path(filepath)
        return lock_path.exists()

    def lock_owner(self, filepath: str) -> str:
        """Return the owner of the lock on a file, or None if unlocked."""
        lock_path = self._lock_path(filepath)
        if not lock_path.exists():
            return None
        try:
            data = json.loads(lock_path.read_text(encoding="utf-8"))
            return data.get("owner")
        except (json.JSONDecodeError, KeyError, OSError):
            return None

    def release_all(self, owner: str):
        """Release all locks held by a specific owner (milestone cleanup)."""
        for lock_file in self.lock_dir.glob("*.lock"):
            try:
                data = json.loads(lock_file.read_text(encoding="utf-8"))
                if data.get("owner") == owner:
                    lock_file.unlink()
            except (json.JSONDecodeError, KeyError, FileNotFoundError, OSError):
                pass

    def cleanup(self):
        """Remove all lock files (use after all workers complete)."""
        for lock_file in self.lock_dir.glob("*.lock"):
            try:
                lock_file.unlink()
            except (FileNotFoundError, OSError):
                pass
