"""
Async I/O Operations for The Iron Vault.

This module provides non-blocking file operations that integrate with
FastAPI's async event loop, preventing the "Blocked Brain" issue where
a single file operation blocks all other requests.

ARCHITECTURE:
- Simple reads use aiofiles for true async I/O
- Atomic writes use run_in_executor (os.fsync/replace are inherently blocking)
- All functions have sync wrappers for backward compatibility

USAGE:
    # In async context (FastAPI handlers)
    content = await read_async("file.txt", root=sandbox)

    # In sync context (scripts, tests)
    content = read_sync("file.txt", root=sandbox)
"""

from __future__ import annotations

import asyncio
import base64
import functools
import logging
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any

import aiofiles
import aiofiles.os

from . import safeio  # Sync versions for atomic operations
from .sandbox import (
    _coerce_helper_path,
    resolve_safe_path,
)

log = logging.getLogger(__name__)

# Shared executor for blocking operations
# Using a dedicated pool prevents exhausting the default executor
_IO_EXECUTOR: ThreadPoolExecutor | None = None


def _get_executor() -> ThreadPoolExecutor:
    """Get or create the I/O executor."""
    global _IO_EXECUTOR
    if _IO_EXECUTOR is None or _IO_EXECUTOR._shutdown:
        _IO_EXECUTOR = ThreadPoolExecutor(max_workers=20, thread_name_prefix="fsal-io-")
    return _IO_EXECUTOR


async def _run_sync(func, *args, **kwargs):
    """Run a synchronous function in the thread pool executor."""
    loop = asyncio.get_running_loop()
    executor = _get_executor()
    return await loop.run_in_executor(
        executor,
        functools.partial(func, *args, **kwargs),
    )


# ----------------------------- Async Reading --------------------------------


async def read_async(
    path: str | Path,
    offset: int | None = None,
    length: int | None = None,
    *,
    root: Path | None = None,
) -> bytes:
    """
    Async file read with optional offset/length.

    Uses aiofiles for non-blocking I/O.
    """
    p = resolve_safe_path(path, root=root)

    # Check existence first (avoid holding file handle during error)
    if not await aiofiles.os.path.exists(p):
        raise FileNotFoundError(str(p))

    if await aiofiles.os.path.isdir(p):
        raise IsADirectoryError(str(p))

    async with aiofiles.open(p, "rb") as f:
        if offset is not None:
            await f.seek(max(0, offset))
        if length is not None:
            return await f.read(length)
        return await f.read()


async def read_bytes_async(path: str | Path, *, root: Path | None = None) -> bytes:
    """Async read entire file as bytes."""
    p = _coerce_helper_path(path, root=root)
    async with aiofiles.open(p, "rb") as f:
        return await f.read()


# ----------------------------- Async Writing --------------------------------


async def atomic_write_async(
    path: str | Path,
    content: bytes,
    *,
    perms: int | None = None,
    make_dirs: bool = False,
    root: Path | None = None,
) -> None:
    """
    Async atomic write.

    Note: Uses run_in_executor because os.fsync() and os.replace()
    are inherently blocking kernel calls. There's no async alternative
    in the Python stdlib (would need io_uring).
    """
    await _run_sync(
        safeio.atomic_write,
        path,
        content,
        perms=perms,
        make_dirs=make_dirs,
        root=root,
    )


async def write_async(
    path: str | Path,
    content_b64: str,
    mode: int | None = None,
    make_dirs: bool = False,
    *,
    root: Path | None = None,
) -> None:
    """Async write with base64 decoding."""
    try:
        data = base64.b64decode(content_b64, validate=True)
    except Exception as exc:
        raise ValueError("invalid base64") from exc
    await atomic_write_async(path, data, perms=mode, make_dirs=make_dirs, root=root)


async def write_bytes_async(
    path: str | Path,
    data: bytes,
    *,
    make_dirs: bool = True,
    root: Path | None = None,
) -> None:
    """Async write raw bytes atomically."""
    await atomic_write_async(path, data, perms=None, make_dirs=make_dirs, root=root)


# ----------------------------- Async Listing --------------------------------


async def listdir_async(
    path: str | Path = ".",
    glob: str | None = None,
    files_only: bool = False,
    dirs_only: bool = False,
    max_entries: int | None = None,
    *,
    root: Path | None = None,
) -> list[dict[str, Any]]:
    """
    Async directory listing.

    Uses run_in_executor because iterdir() with stat() calls
    performs many small blocking operations.
    """
    return await _run_sync(
        safeio.listdir,
        path,
        glob,
        files_only,
        dirs_only,
        max_entries,
        root=root,
    )


async def listdir_recursive_async(
    path: str | Path = ".",
    glob: str | None = None,
    files_only: bool = False,
    dirs_only: bool = False,
    max_entries: int | None = None,
    max_depth: int | None = None,
    *,
    root: Path | None = None,
) -> list[dict[str, Any]]:
    """
    Async recursive directory listing.

    Lists all entries in directory and subdirectories.
    """
    return await _run_sync(
        safeio.listdir_recursive,
        path,
        glob,
        files_only,
        dirs_only,
        max_entries,
        max_depth,
        root=root,
    )


async def stat_async(
    path: str | Path,
    *,
    root: Path | None = None,
) -> dict[str, Any]:
    """Async file/directory stat operation."""
    return await _run_sync(safeio.stat, path, root=root)


async def exists_async(
    path: str | Path,
    *,
    root: Path | None = None,
) -> bool:
    """Async existence check."""
    return await _run_sync(safeio.exists, path, root=root)


# ----------------------------- Async Directory Ops --------------------------


async def mkdir_async(
    path: str | Path,
    mode: int | None = None,
    exist_ok: bool = False,
    *,
    root: Path | None = None,
    **kwargs: Any,
) -> None:
    """Async directory creation."""
    await _run_sync(
        safeio.mkdir,
        path,
        mode,
        exist_ok,
        root=root,
        **kwargs,
    )


async def delete_async(
    path: str | Path,
    recursive: bool = False,
    *,
    root: Path | None = None,
    missing_ok: bool = False,
) -> None:
    """Async file/directory deletion."""
    await _run_sync(
        safeio.delete,
        path,
        recursive,
        root=root,
        missing_ok=missing_ok,
    )


# ----------------------------- Async Move/Copy ------------------------------


async def move_async(
    src: str | Path,
    dst: str | Path,
    *,
    overwrite: bool = False,
    root: Path | None = None,
) -> None:
    """Async file/directory move."""
    await _run_sync(
        safeio.move,
        src,
        dst,
        overwrite=overwrite,
        root=root,
    )


async def copy_async(
    src: str | Path,
    dst: str | Path,
    *,
    overwrite: bool = False,
    root: Path | None = None,
) -> None:
    """Async file/directory copy."""
    await _run_sync(
        safeio.copy,
        src,
        dst,
        overwrite=overwrite,
        root=root,
    )


# ----------------------------- Async Hashing --------------------------------


async def digest_async(
    path: str | Path,
    algo: str = "sha256",
    *,
    root: Path | None = None,
) -> str:
    """Async file hash calculation."""
    return await _run_sync(
        safeio.digest,
        path,
        algo,
        root=root,
    )


# ----------------------------- Sync Wrappers --------------------------------


def _run_async_in_sync(coro):
    """
    Run an async coroutine from synchronous code.

    Handles both cases:
    1. Called from within an existing event loop (use threadsafe scheduling)
    2. Called from script without loop (create new loop)
    """
    try:
        loop = asyncio.get_running_loop()
        # We're inside an async context - schedule threadsafe
        future = asyncio.run_coroutine_threadsafe(coro, loop)
        return future.result(timeout=60)
    except RuntimeError:
        # No running loop - safe to create one
        return asyncio.run(coro)


def read_sync(
    path: str | Path,
    offset: int | None = None,
    length: int | None = None,
    *,
    root: Path | None = None,
) -> bytes:
    """Sync wrapper for read_async."""
    return _run_async_in_sync(read_async(path, offset=offset, length=length, root=root))


def write_sync(
    path: str | Path,
    content_b64: str,
    mode: int | None = None,
    make_dirs: bool = False,
    *,
    root: Path | None = None,
) -> None:
    """Sync wrapper for write_async."""
    _run_async_in_sync(write_async(path, content_b64, mode, make_dirs, root=root))


def listdir_sync(
    path: str | Path = ".",
    glob: str | None = None,
    files_only: bool = False,
    dirs_only: bool = False,
    max_entries: int | None = None,
    *,
    root: Path | None = None,
) -> list[dict[str, Any]]:
    """Sync wrapper for listdir_async."""
    return _run_async_in_sync(
        listdir_async(path, glob, files_only, dirs_only, max_entries, root=root)
    )


# ----------------------------- Cleanup --------------------------------------


def shutdown_executor():
    """Shutdown the I/O executor. Call during application shutdown."""
    global _IO_EXECUTOR
    if _IO_EXECUTOR is not None:
        _IO_EXECUTOR.shutdown(wait=True)
        _IO_EXECUTOR = None
