"""
FastAPI application for The Iron Vault.

Provides REST API endpoints for secure file system operations.
All endpoints require bearer token authentication.

API ENDPOINTS:
- GET  /healthz         - Health check
- GET  /metrics         - Prometheus metrics (if enabled)
- GET  /auth/status     - Auth configuration status (public)
- POST /v1/fs/list      - List directory contents
- POST /v1/fs/read      - Read file content
- POST /v1/fs/write     - Write file content (base64)
- POST /v1/fs/move      - Move/rename file or directory
- POST /v1/fs/copy      - Copy file or directory
- POST /v1/fs/mkdir     - Create directory
- POST /v1/fs/delete    - Delete file or directory
- POST /v1/fs/hash      - Calculate file hash

ARCHITECTURE:
All file I/O endpoints are `async def` to prevent blocking the event loop.
- Reads use aiofiles for true async I/O
- Writes/deletes/moves use run_in_executor (fsync is inherently blocking)
This ensures the "Blocked Brain" issue is avoided.
"""

from __future__ import annotations

import base64
import os
from contextlib import asynccontextmanager
from typing import Any

import anyio
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address

from ..core import async_io
from ..core.sandbox import get_sandbox_root, resolve_safe_path, validate_sandbox_config
from .auth import get_auth_status, require_auth, require_delete, require_read, require_write
from .logging import configure_logging, get_logger
from .metrics import (
    MetricsMiddleware,
    metrics_endpoint,
    record_bytes_read,
    record_bytes_written,
    record_operation,
    record_sandbox_violation,
)
from .middleware import RequestIdMiddleware, SecurityHeadersMiddleware
from .models import (
    DeleteIn,
    Entry,
    ExistsIn,
    HashIn,
    ListIn,
    MkdirIn,
    MoveCopyIn,
    ReadIn,
    StatIn,
    StatResult,
    WriteIn,
)
from .tracing import setup_tracing, trace_operation

# Configure structured logging on module load
configure_logging()
log = get_logger(__name__)

# Request timeout in seconds (configurable via environment)
REQUEST_TIMEOUT = float(os.environ.get("FSAL_REQUEST_TIMEOUT", "30"))

# Rate limiting configuration
limiter = Limiter(key_func=get_remote_address)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application lifespan manager - setup tracing and cleanup on shutdown."""
    # Production config health check (TASK-024)
    aperion_env = os.environ.get("APERION_ENV", "development").lower()
    if aperion_env == "production":
        missing = []
        for var in ("FSAL_TOKEN", "APERION_HMAC_KEY"):
            if not os.environ.get(var):
                missing.append(var)
        if missing:
            log.error(
                "production_startup_failed",
                missing_vars=missing,
                message="Required environment variables not set for production",
            )
            raise RuntimeError(f"Production startup blocked: missing {', '.join(missing)}")
        log.info("production_config_validated", env=aperion_env)

    # Initialize OpenTelemetry tracing if enabled
    setup_tracing(service_name="aperion-fsal")
    log.info("fsal_startup", version="1.2.2", timeout=REQUEST_TIMEOUT)
    yield
    # Cleanup I/O thread pool on shutdown
    async_io.shutdown_executor()
    log.info("fsal_shutdown")


# Determine if we're in production mode (hide API docs)
_is_dev = os.environ.get("APERION_ENV", "").lower() in ("dev", "test")

# Create FastAPI application
app = FastAPI(
    title="The Iron Vault - Aperion FSAL",
    version="1.2.2",
    description=(
        "The absolute boundary between AI agents and the filesystem. "
        "Enforces Constitution B3 (Sandbox Boundaries) and guarantees "
        "file integrity via atomic operations."
    ),
    docs_url="/docs" if _is_dev else None,
    redoc_url="/redoc" if _is_dev else None,
    lifespan=lifespan,
)

# Register rate limiter
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# Add middleware (order matters - first added = outermost)
app.add_middleware(MetricsMiddleware)  # Collect request metrics
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(RequestIdMiddleware)

# Configure CORS for local development (localhost only)
app.add_middleware(
    CORSMiddleware,
    allow_origins=[],
    allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Auth dependency
Auth = require_auth
AuthRead = require_read
AuthWrite = require_write
AuthDelete = require_delete


def _root() -> Any:
    """Get sandbox root via safeio to keep app + tests aligned."""
    return get_sandbox_root()


async def _check_sandbox_writable() -> tuple[bool, str | None]:
    """
    Check if the sandbox is actually writable.

    Returns (is_writable, error_message).
    """
    try:
        root = get_sandbox_root()
        if root is None:
            return False, "Sandbox not configured"

        test_file = root / ".healthcheck"
        # Write test
        test_file.write_bytes(b"ok")
        # Read test
        content = test_file.read_bytes()
        # Cleanup
        test_file.unlink()

        if content != b"ok":
            return False, "Read/write mismatch"

        return True, None
    except Exception as e:
        return False, str(e)


# ----------------------------- Health & Metrics ----------------------------


@app.get("/healthz")
async def healthz() -> dict[str, Any]:
    """
    Health check endpoint.

    Verifies:
    - Sandbox is configured
    - Sandbox is writable (actual write test)

    Returns service status and sandbox configuration.
    """
    sandbox_config = validate_sandbox_config()
    is_writable, write_error = await _check_sandbox_writable()

    ok = sandbox_config.get("configured", False) and is_writable

    result: dict[str, Any] = {
        "ok": ok,
        "service": "aperion-fsal",
        "title": "The Iron Vault",
        "version": "1.2.2",
        "sandbox": sandbox_config,
        "writable": is_writable,
    }

    if write_error:
        result["write_error"] = write_error

    return result


@app.get("/metrics")
async def get_metrics(request: Request):
    """
    Prometheus metrics endpoint.

    Returns metrics in Prometheus text format for scraping.
    """
    return await metrics_endpoint(request)


@app.get("/auth/status")
def auth_status() -> dict[str, Any]:
    """
    Get authentication configuration status.

    This endpoint is PUBLIC to allow clients to detect auth requirements.
    """
    status = get_auth_status()
    return {
        "service": "aperion-fsal",
        "auth_config": status,
        "note": (
            "In 'none' mode, all requests are allowed (dev only). "
            "In 'strict' mode, Bearer token required."
        ),
    }


@app.get("/")
def root() -> dict[str, Any]:
    """Root endpoint with service information."""
    auth_config = get_auth_status()

    return {
        "service": "The Iron Vault - Aperion FSAL",
        "version": "1.2.2",
        "auth_mode": auth_config["mode"],
        "endpoints": [
            "GET /healthz",
            "GET /auth/status",
            "POST /v1/fs/list",
            "POST /v1/fs/read",
            "POST /v1/fs/write",
            "POST /v1/fs/move",
            "POST /v1/fs/copy",
            "POST /v1/fs/mkdir",
            "POST /v1/fs/delete",
            "POST /v1/fs/hash",
        ],
    }


# ----------------------------- API: List --------------------------------


@app.post("/v1/fs/list", response_model=list[Entry])
@limiter.limit("200/minute")
async def fs_list(request: Request, body: ListIn, _: Any = Depends(AuthRead)) -> list[Entry]:
    """
    List directory contents with optional filtering.

    Returns list of entries with metadata (path, is_dir, size, mtime).
    Supports recursive listing with optional depth limit.
    """
    try:
        with anyio.fail_after(REQUEST_TIMEOUT):
            if body.recursive:
                entries = await async_io.listdir_recursive_async(
                    body.path,
                    body.glob,
                    bool(body.files_only),
                    bool(body.dirs_only),
                    body.max_entries,
                    body.max_depth,
                    root=_root(),
                )
            else:
                entries = await async_io.listdir_async(
                    body.path,
                    body.glob,
                    bool(body.files_only),
                    bool(body.dirs_only),
                    body.max_entries,
                    root=_root(),
                )
            record_operation("list")
        return entries  # type: ignore[return-value]
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        raise HTTPException(status_code=403, detail=str(e)) from e
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail="Directory not found") from e
    except Exception as e:
        log.exception("list_failed", path=body.path)
        raise HTTPException(status_code=500, detail=f"List failed: {e}") from e


# ----------------------------- API: Read --------------------------------


@app.post("/v1/fs/read")
@limiter.limit("200/minute")
async def fs_read(request: Request, body: ReadIn, _: Any = Depends(AuthRead)) -> dict[str, Any]:
    """
    Read file content, optionally as text or with byte range.

    Returns either base64-encoded content or decoded text.
    """
    try:
        with trace_operation("fs_read", {"path": body.path}):
            with anyio.fail_after(REQUEST_TIMEOUT):
                raw = await async_io.read_async(
                    body.path, offset=body.offset, length=body.length, root=_root()
                )

                # Record metrics
                record_operation("read")
                record_bytes_read(len(raw))

                if body.as_text:
                    try:
                        return {"text": raw.decode(body.encoding or "utf-8")}
                    except UnicodeDecodeError as e:
                        raise HTTPException(status_code=400, detail="File is not valid text") from e
                return {"content_b64": base64.b64encode(raw).decode("ascii")}
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        record_sandbox_violation("traversal")
        raise HTTPException(status_code=403, detail=str(e)) from e
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail="File not found") from e
    except HTTPException:
        raise
    except Exception as e:
        log.exception("read_failed", path=body.path)
        raise HTTPException(status_code=500, detail=f"Read failed: {e}") from e


# ----------------------------- API: Write -------------------------------


@app.post("/v1/fs/write")
@limiter.limit("100/minute")
async def fs_write(request: Request, body: WriteIn, _: Any = Depends(AuthWrite)) -> dict[str, Any]:
    """
    Write base64-encoded content to file atomically.

    Creates parent directories if make_dirs is True.
    """
    try:
        with trace_operation("fs_write", {"path": body.path}):
            with anyio.fail_after(REQUEST_TIMEOUT):
                await async_io.write_async(
                    body.path,
                    body.content_b64,
                    mode=body.mode,
                    make_dirs=bool(body.make_dirs),
                    root=_root(),
                )

                # Record metrics (base64 is ~4/3 of raw size)
                raw_size = len(body.content_b64) * 3 // 4
                record_operation("write")
                record_bytes_written(raw_size)

        return {"ok": True, "message": f"Written to {body.path}"}
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        record_sandbox_violation("traversal")
        raise HTTPException(status_code=403, detail=str(e)) from e
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e)) from e
    except Exception as e:
        log.exception("write_failed", path=body.path)
        raise HTTPException(status_code=500, detail=f"Write failed: {e}") from e


# ----------------------------- API: Move/Copy ---------------------------


@app.post("/v1/fs/move")
@limiter.limit("100/minute")
async def fs_move(
    request: Request, body: MoveCopyIn, _: Any = Depends(AuthWrite)
) -> dict[str, Any]:
    """
    Move file or directory atomically.

    Fails if destination exists unless overwrite=True.
    """
    try:
        with anyio.fail_after(REQUEST_TIMEOUT):
            await async_io.move_async(
                body.src, body.dst, overwrite=bool(body.overwrite), root=_root()
            )
        return {"ok": True, "message": f"Moved {body.src} to {body.dst}"}
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        raise HTTPException(status_code=403, detail=str(e)) from e
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail="Source not found") from e
    except FileExistsError as e:
        raise HTTPException(status_code=409, detail="Destination exists") from e
    except Exception as e:
        log.exception("move_failed", src=body.src, dst=body.dst)
        raise HTTPException(status_code=500, detail=f"Move failed: {e}") from e


@app.post("/v1/fs/copy")
@limiter.limit("50/minute")
async def fs_copy(
    request: Request, body: MoveCopyIn, _: Any = Depends(AuthWrite)
) -> dict[str, Any]:
    """
    Copy file or directory recursively.

    Fails if destination exists unless overwrite=True.
    """
    try:
        with anyio.fail_after(REQUEST_TIMEOUT):
            await async_io.copy_async(
                body.src, body.dst, overwrite=bool(body.overwrite), root=_root()
            )
        return {"ok": True, "message": f"Copied {body.src} to {body.dst}"}
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        raise HTTPException(status_code=403, detail=str(e)) from e
    except FileExistsError as e:
        raise HTTPException(status_code=409, detail="Destination exists") from e
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail="Source not found") from e
    except Exception as e:
        log.exception("copy_failed", src=body.src, dst=body.dst)
        raise HTTPException(status_code=500, detail=f"Copy failed: {e}") from e


# ----------------------------- API: Mkdir/Delete ------------------------


@app.post("/v1/fs/mkdir")
@limiter.limit("100/minute")
async def fs_mkdir(request: Request, body: MkdirIn, _: Any = Depends(AuthWrite)) -> dict[str, Any]:
    """
    Create directory with optional permissions.

    Creates parent directories as needed.
    """
    try:
        with anyio.fail_after(REQUEST_TIMEOUT):
            await async_io.mkdir_async(
                body.path,
                mode=body.mode,
                exist_ok=bool(body.exist_ok),
                root=_root(),
                parents=True,
            )
            # Best-effort chmod if mode was provided
            if body.mode is not None:
                try:
                    os.chmod(resolve_safe_path(body.path, root=_root()), int(body.mode))
                except Exception as exc:
                    log.debug("mkdir_chmod_failed", path=body.path, error=str(exc))
        return {"ok": True, "message": f"Created directory {body.path}"}
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        raise HTTPException(status_code=403, detail=str(e)) from e
    except FileExistsError as e:
        raise HTTPException(status_code=409, detail="Directory exists") from e
    except Exception as e:
        log.exception("mkdir_failed", path=body.path)
        raise HTTPException(status_code=500, detail=f"Mkdir failed: {e}") from e


@app.post("/v1/fs/delete")
@limiter.limit("50/minute")
async def fs_delete(
    request: Request, body: DeleteIn, _: Any = Depends(AuthDelete)
) -> dict[str, Any]:
    """
    Delete file or directory.

    Requires recursive=True for non-empty directories.
    """
    try:
        with anyio.fail_after(REQUEST_TIMEOUT):
            await async_io.delete_async(
                body.path,
                recursive=bool(body.recursive),
                root=_root(),
            )
        return {"ok": True, "message": f"Deleted {body.path}"}
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        raise HTTPException(status_code=403, detail=str(e)) from e
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail="Path not found") from e
    except OSError as e:
        if "Directory not empty" in str(e):
            raise HTTPException(
                status_code=400, detail="Directory not empty, use recursive=true"
            ) from e
        raise HTTPException(status_code=500, detail=str(e)) from e
    except Exception as e:
        log.exception("delete_failed", path=body.path)
        raise HTTPException(status_code=500, detail=f"Delete failed: {e}") from e


# ----------------------------- API: Hash --------------------------------


@app.post("/v1/fs/hash")
@limiter.limit("200/minute")
async def fs_hash(request: Request, body: HashIn, _: Any = Depends(AuthRead)) -> dict[str, Any]:
    """
    Calculate file hash digest using specified algorithm.

    Supports any algorithm available in Python's hashlib.
    """
    try:
        with anyio.fail_after(REQUEST_TIMEOUT):
            hex_digest = await async_io.digest_async(body.path, algo=body.algo, root=_root())
        return {
            "ok": True,
            "path": body.path,
            "algo": body.algo,
            "hex": hex_digest,
        }
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        raise HTTPException(status_code=403, detail=str(e)) from e
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail="File not found") from e
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e)) from e
    except Exception as e:
        log.exception("hash_failed", path=body.path)
        raise HTTPException(status_code=500, detail=f"Hash failed: {e}") from e


# ----------------------------- API: Exists & Stat ---------------------------


@app.post("/v1/fs/exists")
@limiter.limit("200/minute")
async def fs_exists(request: Request, body: ExistsIn, _: Any = Depends(AuthRead)) -> dict[str, Any]:
    """
    Check if a file or directory exists.

    Returns existence status without raising errors for missing paths.
    """
    try:
        with anyio.fail_after(REQUEST_TIMEOUT):
            exists_result = await async_io.exists_async(body.path, root=_root())
            record_operation("exists")
        return {"path": body.path, "exists": exists_result}
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        record_sandbox_violation("traversal")
        raise HTTPException(status_code=403, detail=str(e)) from e
    except Exception as e:
        log.exception("exists_failed", path=body.path)
        raise HTTPException(status_code=500, detail=f"Exists check failed: {e}") from e


@app.post("/v1/fs/stat", response_model=StatResult)
@limiter.limit("200/minute")
async def fs_stat(request: Request, body: StatIn, _: Any = Depends(AuthRead)) -> StatResult:
    """
    Get detailed file or directory statistics.

    Returns metadata including size, timestamps, and permissions.
    Does not raise 404 for missing paths - check the 'exists' field.
    """
    try:
        with anyio.fail_after(REQUEST_TIMEOUT):
            stat_result = await async_io.stat_async(body.path, root=_root())
            record_operation("stat")
        return StatResult(**stat_result)
    except TimeoutError:
        raise HTTPException(status_code=504, detail="Request timed out") from None
    except PermissionError as e:
        record_sandbox_violation("traversal")
        raise HTTPException(status_code=403, detail=str(e)) from e
    except Exception as e:
        log.exception("stat_failed", path=body.path)
        raise HTTPException(status_code=500, detail=f"Stat failed: {e}") from e
