#!/usr/bin/env python3
"""
Novyx Core - FastAPI Application (Phase 6 + 7: REST + GraphQL APIs)
====================================================================
Hybrid API for Novyx Core: RESTful endpoints + GraphQL interface with authentication,
rate limiting, error handling, and logging.

Features:
- 8 RESTful endpoints (create, ingest, query, validate, repair)
- GraphQL endpoint at /graphql (Phase 7)
- API key authentication (both REST and GraphQL)
- Rate limiting (100 req/min)
- Comprehensive error handling
- Atomic operations (reuses factory.py, graph.py, sentinel.py)
- Request logging to memory/logs/ (Phase 6, Days 6-7)

Usage:
    # Development
    uvicorn tools.api:app --reload --host 0.0.0.0 --port 8000
    
    # Production
    uvicorn tools.api:app --host 0.0.0.0 --port 8000 --workers 4
    
    # GraphQL Playground
    Open: http://localhost:8000/graphql

Environment Variables:
    NOVYX_API_KEY: API key for authentication (default: "novyx-dev-key-change-in-production")
    NOVYX_JWT_SECRET: JWT signing secret, min 32 chars (default: development placeholder)
    NOVYX_RATE_LIMIT: Requests per minute (default: "100/minute")
    NOVYX_ENABLE_LOGGING: Enable request logging (default: "1")
    NOVYX_ENVIRONMENT: Set to "development" to suppress security warnings
    NOVYX_STRICT_SECURITY: Set to "1" to exit on insecure config (non-development only)
"""

import os
import re
import sys
import time
from pathlib import Path
from typing import Dict, List, Optional, Any
from datetime import datetime

# FastAPI and dependencies
try:
    from fastapi import FastAPI, HTTPException, Security, Body, Query, Path as PathParam, status, Request, Depends, UploadFile, Form
    from fastapi.security import APIKeyHeader
    from fastapi.responses import JSONResponse
    from fastapi.middleware.cors import CORSMiddleware
    from pydantic import BaseModel, Field
    from slowapi import Limiter, _rate_limit_exceeded_handler
    from slowapi.util import get_remote_address
    from slowapi.errors import RateLimitExceeded
except ImportError:
    print("❌ Missing dependencies. Install with:")
    print("   pip install 'fastapi[standard]' uvicorn slowapi python-multipart")
    sys.exit(1)

# Strawberry GraphQL (Phase 7)
try:
    from strawberry.fastapi import GraphQLRouter
    from tools.schema import schema as graphql_schema
    GRAPHQL_ENABLED = True
except ImportError:
    print("⚠️  GraphQL not available. Install with: pip install 'strawberry-graphql[fastapi]'")
    GRAPHQL_ENABLED = False

# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))

# Import Novyx tools
from tools.factory import create_artifact, persist_artifact
from tools.sentinel import NovyxSentinel
from tools.graph import scan_artifacts, build_index, find_orphaned_links
from tools.graph_cache import get_index, invalidate as invalidate_graph_cache
from tools.artifacts import get_artifact_config, list_artifact_types
from tools.security import check_security_config

# Phase 9: Multi-Tenant Authentication
try:
    from tools.auth import (
        create_access_token, verify_token, authenticate_user,
        has_permission, load_tenant, AUTH_AVAILABLE
    )
    JWT_ENABLED = AUTH_AVAILABLE
except ImportError:
    print("⚠️  JWT auth not available. Install with: pip install pyjwt 'passlib[bcrypt]'")
    JWT_ENABLED = False

# Import logging spoke (conditional)
ENABLE_LOGGING = os.getenv("NOVYX_ENABLE_LOGGING", "1") == "1"
if ENABLE_LOGGING:
    try:
        from spokes.request_logger import log_request
    except ImportError:
        print("⚠️  Logging spoke not available (structlog missing)")
        ENABLE_LOGGING = False

# Configuration
API_KEY = os.getenv("NOVYX_API_KEY", "novyx-dev-key-change-in-production")
RATE_LIMIT = os.getenv("NOVYX_RATE_LIMIT", "100/minute")
DEFAULT_MEMORY_DIR = Path("memory")

# CORS Configuration (Phase 12: Security Hardening)
# Set NOVYX_CORS_ORIGINS to comma-separated list of allowed origins
# Example: NOVYX_CORS_ORIGINS=https://app.example.com,https://admin.example.com
CORS_ORIGINS = os.getenv("NOVYX_CORS_ORIGINS", "http://localhost:3000,http://localhost:8080").split(",")
CORS_ORIGINS = [origin.strip() for origin in CORS_ORIGINS if origin.strip()]

# Input Validation Patterns (Phase 12: Security Hardening)
# UUID pattern: urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx or just the UUID part
UUID_PATTERN = re.compile(r'^(urn:uuid:)?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$', re.IGNORECASE)
# Tenant ID pattern: lowercase alphanumeric with hyphens/underscores, 3-64 chars
TENANT_PATTERN = re.compile(r'^[a-z0-9][a-z0-9_-]{2,63}$')


def validate_artifact_id(artifact_id: str) -> str:
    """
    Validate artifact ID format to prevent path traversal attacks.

    Args:
        artifact_id: UUID string with optional urn:uuid: prefix

    Returns:
        Validated artifact_id string

    Raises:
        HTTPException(400) if format is invalid

    Security:
        - Prevents path traversal (../, etc.)
        - Ensures only valid UUID characters
    """
    if not artifact_id:
        raise HTTPException(status_code=400, detail="artifact_id is required")

    # Check for path traversal attempts
    if '..' in artifact_id or '/' in artifact_id or '\\' in artifact_id:
        raise HTTPException(status_code=400, detail="Invalid artifact_id: path traversal not allowed")

    # Validate UUID format
    if not UUID_PATTERN.match(artifact_id):
        raise HTTPException(
            status_code=400,
            detail="Invalid artifact_id format. Expected UUID (e.g., urn:uuid:12345678-1234-1234-1234-123456789abc)"
        )

    return artifact_id


def validate_tenant_id(tenant_id: Optional[str]) -> Optional[str]:
    """
    Validate tenant ID format to prevent path traversal attacks.

    Args:
        tenant_id: Optional tenant identifier string

    Returns:
        Validated tenant_id string or None

    Raises:
        HTTPException(400) if format is invalid

    Security:
        - Prevents path traversal (../, etc.)
        - Ensures only safe directory name characters
    """
    if tenant_id is None:
        return None

    # Check for path traversal attempts
    if '..' in tenant_id or '/' in tenant_id or '\\' in tenant_id:
        raise HTTPException(status_code=400, detail="Invalid tenant_id: path traversal not allowed")

    # Validate tenant ID format
    if not TENANT_PATTERN.match(tenant_id):
        raise HTTPException(
            status_code=400,
            detail="Invalid tenant_id format. Expected 3-64 lowercase alphanumeric chars with hyphens/underscores"
        )

    return tenant_id

# Initialize FastAPI app
app = FastAPI(
    title="Novyx Core API",
    description="Persistent intelligence system with REST + GraphQL APIs, semantic search, and cryptographic integrity",
    version="1.3.0",
    docs_url="/api/docs",
    redoc_url="/api/redoc",
)

# CORS middleware (Phase 12: Restricted to NOVYX_CORS_ORIGINS)
app.add_middleware(
    CORSMiddleware,
    allow_origins=CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization", "X-API-Key"],
)

# Security check at startup
@app.on_event("startup")
async def startup_security_check():
    """Validate security configuration on API startup."""
    check_security_config()

# Logging middleware (Phase 6, Days 6-7)
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
    """Log all API requests with timing and status."""
    start_time = time.time()
    
    # Get API key from header for logging
    api_key = request.headers.get("X-API-Key", "")
    
    # Process request
    response = await call_next(request)
    
    # Calculate duration
    duration_ms = (time.time() - start_time) * 1000
    
    # Log request (background task, non-blocking)
    if ENABLE_LOGGING and request.url.path != "/health":  # Skip health checks
        try:
            # Extract error from response if status >= 400
            error = None
            if response.status_code >= 400:
                error = f"HTTP {response.status_code}"
            
            # Log asynchronously (in background)
            # Note: For production, use proper background tasks or async queue
            log_request(
                endpoint=request.url.path,
                method=request.method,
                status_code=response.status_code,
                user_key=api_key if api_key else None,
                duration_ms=round(duration_ms, 2),
                error=error
            )
        except Exception as e:
            # Don't fail request if logging fails
            print(f"⚠️  Logging failed: {e}")
    
    return response

# Rate limiting
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# API Key authentication
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)

# GraphQL Router (Phase 7) with authentication
if GRAPHQL_ENABLED:
    async def get_context(
        request: Request,
        api_key: Optional[str] = Security(api_key_header)
    ):
        """
        GraphQL context provider with authentication.
        Validates API key for GraphQL operations.
        """
        if api_key != API_KEY:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid or missing API key"
            )
        return {"request": request, "authenticated": True}
    
    graphql_app = GraphQLRouter(
        schema=graphql_schema,
        context_getter=get_context,
        path="/graphql"
    )
    
    # Mount GraphQL endpoint
    app.include_router(graphql_app, prefix="", tags=["GraphQL"])


async def verify_api_key(api_key: str = Security(api_key_header)):
    """Verify API key from header."""
    if api_key != API_KEY:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or missing API key"
        )
    return api_key


# Phase 9: JWT Authentication Functions
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

jwt_bearer = HTTPBearer(auto_error=False)


async def verify_jwt_token(credentials: Optional[HTTPAuthorizationCredentials] = Security(jwt_bearer)) -> Optional[Dict[str, Any]]:
    """
    Verify JWT token from Authorization header.
    
    Returns:
        Token payload dict or None if invalid/missing
    """
    if not JWT_ENABLED:
        return None
    
    if not credentials:
        return None
    
    token = credentials.credentials
    payload = verify_token(token)
    return payload


async def get_current_user(
    api_key: Optional[str] = Security(api_key_header),
    jwt_payload: Optional[Dict[str, Any]] = Depends(verify_jwt_token)
) -> Dict[str, Any]:
    """
    Get current user from either API key or JWT token.
    
    Returns:
        User context dict with tenant_id, email, role
    
    Raises:
        HTTPException if neither auth method succeeds
    """
    # Try JWT first (Phase 9: Multi-Tenant)
    if jwt_payload:
        return {
            "authenticated": True,
            "auth_method": "jwt",
            "tenant_id": jwt_payload.get("tenant_id"),
            "email": jwt_payload.get("sub"),
            "role": jwt_payload.get("role", "viewer")
        }
    
    # Fall back to API key (legacy, Phase 6)
    if api_key == API_KEY:
        return {
            "authenticated": True,
            "auth_method": "api_key",
            "tenant_id": None,  # API key = global access
            "email": None,
            "role": "admin"  # API key = full admin access
        }
    
    # No valid authentication
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid or missing authentication (use X-API-Key or Authorization Bearer token)"
    )


async def require_role(required_role: str, user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
    """
    Dependency to require a minimum role level.
    
    Args:
        required_role: Minimum required role (viewer, editor, admin)
        user: Current user context from get_current_user
    
    Returns:
        User context if authorized
    
    Raises:
        HTTPException if insufficient permissions
    """
    user_role = user.get("role", "viewer")
    
    if not has_permission(user_role, required_role):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=f"Insufficient permissions. Required: {required_role}, have: {user_role}"
        )
    
    return user


# Pydantic models for request/response

class CreateArtifactRequest(BaseModel):
    """Request model for creating artifacts."""
    payload: Dict[str, Any] = Field(..., description="Artifact-specific fields")
    external_links: Optional[List[str]] = Field(None, description="External authority URLs")


class IngestTextRequest(BaseModel):
    """Request model for text ingestion."""
    text: str = Field(..., description="Raw text content")
    category: str = Field(..., description="Artifact category")
    title: Optional[str] = Field(None, description="Optional title")
    external_links: Optional[List[str]] = Field(None, description="External URLs")


class IngestDecisionRequest(BaseModel):
    """Request model for Decision ingestion."""
    title: str
    context: str
    options_considered: List[str]
    chosen_option: str
    reasoning: str
    constraints: List[str]
    assumptions: List[str]
    confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence between 0 and 1")
    expected_outcome: str
    category: str


class ValidateRequest(BaseModel):
    """Request model for validation."""
    directory: Optional[str] = Field("memory", description="Directory to validate")
    enforce_graph: bool = Field(False, description="Enable graph enforcement")
    verbose: bool = Field(False, description="Include detailed errors")


class RepairRequest(BaseModel):
    """Request model for graph repair."""
    directory: Optional[str] = Field("memory", description="Directory to repair")
    dry_run: bool = Field(True, description="Preview changes without modifying")
    create_stubs: bool = Field(False, description="Create stub artifacts")


class StandardResponse(BaseModel):
    """Standard API response."""
    status: str
    message: Optional[str] = None
    data: Optional[Dict[str, Any]] = None


# Phase 9: Multi-Tenant Authentication Models

class LoginRequest(BaseModel):
    """Request model for JWT login."""
    tenant_id: str = Field(..., description="Tenant identifier")
    email: str = Field(..., description="User email")
    password: str = Field(..., description="User password")


class LoginResponse(BaseModel):
    """Response model for JWT login."""
    access_token: str = Field(..., description="JWT access token")
    token_type: str = Field(default="bearer", description="Token type")
    tenant_id: str = Field(..., description="Tenant identifier")
    email: str = Field(..., description="User email")
    role: str = Field(..., description="User role")
    expires_in: int = Field(..., description="Token expiration in seconds")


# Authentication endpoints (Phase 9)

@app.post("/api/v1/auth/login", response_model=LoginResponse)
@limiter.limit(RATE_LIMIT)
async def login(request: Request, login_data: LoginRequest):
    """
    Authenticate user and return JWT token for tenant access.
    
    Phase 9: Multi-Tenant Authentication
    
    Args:
        login_data: Tenant ID, email, and password
    
    Returns:
        JWT token with tenant context and role
    
    Example:
        ```bash
        curl -X POST http://localhost:8000/api/v1/auth/login \\
          -H "Content-Type: application/json" \\
          -d '{
            "tenant_id": "acme-corp",
            "email": "admin@acme.com",
            "password": "secret123"
          }'
        ```
    
    Response:
        ```json
        {
          "access_token": "eyJ...",
          "token_type": "bearer",
          "tenant_id": "acme-corp",
          "email": "admin@acme.com",
          "role": "admin",
          "expires_in": 3600
        }
        ```
    """
    if not JWT_ENABLED:
        raise HTTPException(
            status_code=status.HTTP_501_NOT_IMPLEMENTED,
            detail="JWT authentication not available (install pyjwt and passlib)"
        )
    
    # Authenticate user against tenant
    user = authenticate_user(login_data.tenant_id, login_data.email, login_data.password)
    
    if not user:
        # Log request for failed auth attempts
        if ENABLE_LOGGING:
            await log_request(
                request=request,
                endpoint="POST /api/v1/auth/login",
                status_code=401,
                duration_ms=0,
                user_key=f"{login_data.tenant_id}:{login_data.email}",
                error="Authentication failed"
            )
        
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid tenant, email, or password"
        )
    
    # Create JWT token
    from datetime import timedelta
    from tools.auth import ACCESS_TOKEN_EXPIRE_MINUTES
    
    access_token = create_access_token(
        tenant_id=user["tenant_id"],
        user_email=user["email"],
        role=user["role"]
    )
    
    # Log successful authentication
    if ENABLE_LOGGING:
        await log_request(
            request=request,
            endpoint="POST /api/v1/auth/login",
            status_code=200,
            duration_ms=0,
            user_key=f"{user['tenant_id']}:{user['email']}"
        )
    
    return LoginResponse(
        access_token=access_token,
        token_type="bearer",
        tenant_id=user["tenant_id"],
        email=user["email"],
        role=user["role"],
        expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60
    )


@app.get("/api/v1/auth/me")
@limiter.limit(RATE_LIMIT)
async def get_current_user_info(
    request: Request,
    user: Dict[str, Any] = Depends(get_current_user)
):
    """
    Get information about the currently authenticated user.
    
    Phase 9: Multi-Tenant Authentication
    
    Returns:
        User context including tenant, email, role, auth method
    
    Example:
        ```bash
        # Using JWT
        curl http://localhost:8000/api/v1/auth/me \\
          -H "Authorization: Bearer eyJ..."
        
        # Using API Key
        curl http://localhost:8000/api/v1/auth/me \\
          -H "X-API-Key: novyx-dev-key-change-in-production"
        ```
    """
    return {
        "status": "success",
        "data": user
    }


# Health check endpoint

@app.get("/health", response_model=StandardResponse)
async def health_check():
    """Health check endpoint."""
    return {
        "status": "healthy",
        "message": "Novyx Core API is running",
        "data": {
            "version": "1.2.0",
            "timestamp": datetime.utcnow().isoformat(),
            "memory_dir": str(DEFAULT_MEMORY_DIR)
        }
    }


# Group 1: POST endpoints for artifact creation

@app.post("/api/v1/create/{artifact_type}", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def create_artifact_endpoint(
    request: Request,
    artifact_type: str = PathParam(..., description="Type of artifact to create"),
    body: CreateArtifactRequest = Body(...),
    api_key: str = Security(verify_api_key)
):
    """
    Create a new artifact of the specified type.
    
    Uses factory.py for schema-driven creation with automatic:
    - UUID generation (urn:uuid:...)
    - Timestamps (ISO 8601)
    - Integrity hashes (SHA-256)
    - Schema validation
    """
    try:
        # Verify artifact type is registered
        config = get_artifact_config(artifact_type)
        if not config:
            raise HTTPException(
                status_code=400,
                detail=f"Unknown artifact type: {artifact_type}. Available: {list_artifact_types()}"
            )
        
        # Create artifact using factory
        try:
            artifact = create_artifact(artifact_type, body.payload, body.external_links)
        except (KeyError, ValueError) as e:
            raise HTTPException(status_code=400, detail=str(e))
        
        # Persist to appropriate directory
        output_dir = Path(config["output_dir"])
        file_path = persist_artifact(artifact, output_dir)
        
        return {
            "status": "success",
            "message": f"Created {artifact_type} artifact",
            "data": {
                "artifact_id": artifact["uuid"],
                "file_path": str(file_path),
                "integrity_hash": artifact["novyx:integrityHash"]
            }
        }
    
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Internal error: {e}")


@app.post("/api/v1/ingest/text", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def ingest_text_endpoint(
    request: Request,
    body: IngestTextRequest = Body(...),
    api_key: str = Security(verify_api_key)
):
    """
    Ingest raw text and generate semantic embeddings.
    
    Note: This endpoint requires sentence-transformers to be installed.
    For now, creates a basic DigitalDocument artifact without embeddings.
    Full embedding support requires running ingestor.py separately.
    """
    try:
        # Create a basic document artifact
        # Full semantic embedding would call ingestor.py, which requires ML deps
        payload = {
            "title": body.title or f"Ingested at {datetime.utcnow().isoformat()}",
            "articleBody": body.text,
            "category": body.category,
            "description": f"Ingested text in category: {body.category}"
        }
        
        # Use factory to create (currently limited to registered types)
        # For generic text, we'll use a workaround
        artifact = create_artifact("decision", {
            "title": payload["title"],
            "context": body.text[:500] + ("..." if len(body.text) > 500 else ""),
            "options_considered": ["Ingest as document"],
            "chosen_option": "Ingest as document",
            "reasoning": f"Text ingestion via API for category: {body.category}",
            "constraints": [],
            "assumptions": ["Text is valid content"],
            "confidence": 0.8,
            "expected_outcome": "Artifact created and searchable",
            "category": body.category
        }, body.external_links)
        
        output_dir = DEFAULT_MEMORY_DIR / body.category
        file_path = persist_artifact(artifact, output_dir)
        
        return {
            "status": "success",
            "message": "Text ingested (basic mode, no embeddings)",
            "data": {
                "artifact_id": artifact["uuid"],
                "file_path": str(file_path),
                "note": "Full semantic embedding requires sentence-transformers"
            }
        }
    
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Ingestion error: {e}")


@app.post("/api/v1/ingest/decision", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def ingest_decision_endpoint(
    request: Request,
    body: IngestDecisionRequest = Body(...),
    api_key: str = Security(verify_api_key)
):
    """
    Create a Decision artifact with causal reasoning fields.
    
    Validates:
    - Confidence range (0.0 to 1.0)
    - All required Decision fields from schema
    - Automatic integrity hashing
    """
    try:
        payload = body.model_dump()
        
        # Create Decision artifact
        artifact = create_artifact("decision", payload)
        
        # Persist to decisions directory
        output_dir = DEFAULT_MEMORY_DIR / "decisions"
        file_path = persist_artifact(artifact, output_dir)
        
        return {
            "status": "success",
            "message": "Decision artifact created",
            "data": {
                "artifact_id": artifact["uuid"],
                "file_path": str(file_path),
                "confidence": body.confidence,
                "integrity_hash": artifact["novyx:integrityHash"]
            }
        }
    
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Decision creation error: {e}")


# Group 2: GET endpoints for querying

@app.get("/api/v1/query/search", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def search_query_endpoint(
    request: Request,
    search_term: str = Query(..., description="Search query"),
    limit: int = Query(10, ge=1, le=100, description="Max results"),
    threshold: float = Query(0.2, ge=0.0, le=1.0, description="Min similarity score"),
    api_key: str = Security(verify_api_key)
):
    """
    Perform semantic search across artifacts.
    
    Note: Requires embeddings to be present in artifacts.
    Returns basic file matching for now.
    """
    try:
        # Scan artifacts
        artifacts, errors = scan_artifacts(DEFAULT_MEMORY_DIR)
        
        # Basic text matching (full semantic search requires embeddings)
        results = []
        for artifact_entry in artifacts:
            data = artifact_entry['data']
            title = data.get('title') or data.get('name', '')
            context = data.get('context', '') or data.get('articleBody', '') or data.get('description', '')
            
            # Simple case-insensitive substring match
            if search_term.lower() in title.lower() or search_term.lower() in context.lower():
                primary_id = data.get('uuid') or data.get('Persistent_ID') or data.get('@id', '')
                results.append({
                    "artifact_id": primary_id,
                    "title": title,
                    "file_path": str(artifact_entry['path']),
                    "similarity": 0.5  # Placeholder
                })
                
                if len(results) >= limit:
                    break
        
        return {
            "status": "success",
            "message": f"Found {len(results)} matches (basic text matching)",
            "data": {
                "results": results,
                "total": len(results),
                "note": "Full semantic search requires embeddings"
            }
        }
    
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Search error: {e}")


@app.get("/api/v1/query/stats", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def stats_query_endpoint(
    request: Request,
    directory: str = Query("memory", description="Directory to analyze"),
    api_key: str = Security(verify_api_key)
):
    """
    Return statistics about the knowledge graph.
    
    Includes:
    - Total artifacts
    - Artifacts by type
    - Latest timestamps
    - Total links
    """
    try:
        dir_path = Path(directory)
        if not dir_path.exists():
            raise HTTPException(status_code=404, detail=f"Directory not found: {directory}")
        
        artifacts, errors = scan_artifacts(dir_path)
        
        # Count by type
        by_type = {}
        latest_timestamp = None
        
        for artifact_entry in artifacts:
            data = artifact_entry['data']
            artifact_type = data.get('@type', ['Unknown'])
            if isinstance(artifact_type, list):
                artifact_type = artifact_type[0]
            
            by_type[artifact_type] = by_type.get(artifact_type, 0) + 1
            
            # Track latest timestamp
            timestamp = data.get('createdAt') or data.get('Temporal_Marker')
            if timestamp:
                if not latest_timestamp or timestamp > latest_timestamp:
                    latest_timestamp = timestamp
        
        # Build graph index for link counts (cached)
        index = get_index(dir_path)
        total_links = sum(len(links) for links in index['file_to_links'].values())
        
        return {
            "status": "success",
            "message": "Statistics retrieved",
            "data": {
                "total_artifacts": len(artifacts),
                "by_type": by_type,
                "latest_timestamp": latest_timestamp,
                "total_links": total_links,
                "parse_errors": len(errors)
            }
        }
    
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Stats error: {e}")


@app.get("/api/v1/graph", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def graph_analysis_endpoint(
    request: Request,
    directory: str = Query("memory", description="Directory to analyze"),
    include_orphans: bool = Query(True, description="Include orphan details"),
    api_key: str = Security(verify_api_key)
):
    """
    Return comprehensive graph index with IDs, links, and orphans.
    """
    try:
        dir_path = Path(directory)
        if not dir_path.exists():
            raise HTTPException(status_code=404, detail=f"Directory not found: {directory}")
        
        artifacts, errors = scan_artifacts(dir_path)
        index = get_index(dir_path)
        orphaned = find_orphaned_links(index)

        # Top linked artifacts
        top_linked = []
        for target_id, sources in sorted(
            index['inbound_links'].items(),
            key=lambda x: len(x[1]),
            reverse=True
        )[:5]:
            top_linked.append({
                "id": target_id[:80],
                "inbound_count": len(sources)
            })
        
        response_data = {
            "total_artifacts": len(artifacts),
            "total_ids": len(index['id_to_file']),
            "total_links": sum(len(links) for links in index['file_to_links'].values()),
            "orphaned_links": sum(len(orphans) for orphans in orphaned.values()),
            "top_linked": top_linked
        }
        
        if include_orphans and orphaned:
            orphan_details = []
            for file_path, orphan_ids in list(orphaned.items())[:10]:
                orphan_details.append({
                    "file": str(file_path),
                    "orphan_count": len(orphan_ids),
                    "orphan_ids": list(orphan_ids)[:5]
                })
            response_data["orphan_details"] = orphan_details
        
        return {
            "status": "success",
            "message": "Graph analysis complete",
            "data": response_data
        }
    
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Graph analysis error: {e}")


# Group 3: Validation and Repair endpoints

@app.post("/api/v1/validate", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def validate_endpoint(
    request: Request,
    body: ValidateRequest = Body(...),
    api_key: str = Security(verify_api_key)
):
    """
    Run Sentinel integrity checks on artifacts.
    
    Validates:
    - Cryptographic hashes
    - Schema compliance
    - Optional: graph integrity
    """
    try:
        dir_path = Path(body.directory)
        if not dir_path.exists():
            raise HTTPException(status_code=404, detail=f"Directory not found: {body.directory}")
        
        sentinel = NovyxSentinel(
            root_dir=dir_path,
            verbose=body.verbose,
            enforce_graph=body.enforce_graph
        )
        sentinel.scan_directory(dir_path)
        
        passed_count = sum(1 for r in sentinel.results if r.passed)
        failed_count = len(sentinel.results) - passed_count
        
        errors_list = []
        if body.verbose:
            for result in sentinel.results:
                if not result.passed:
                    errors_list.append({
                        "file": str(result.file_path),
                        "errors": result.errors
                    })
        
        return {
            "status": "success" if failed_count == 0 and not sentinel.graph_errors else "validation_failed",
            "message": f"Validation complete: {passed_count} passed, {failed_count} failed",
            "data": {
                "passed": passed_count,
                "failed": failed_count,
                "total": len(sentinel.results),
                "errors": errors_list,
                "graph_errors": sentinel.graph_errors if body.enforce_graph else []
            }
        }
    
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Validation error: {e}")


@app.post("/api/v1/repair", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def repair_graph_endpoint(
    request: Request,
    body: RepairRequest = Body(...),
    api_key: str = Security(verify_api_key)
):
    """
    Repair orphaned links in the graph.
    
    Modes:
    - dry_run=True: Preview changes only
    - dry_run=False: Execute repairs
    - create_stubs: Create placeholder artifacts for missing IDs
    """
    try:
        dir_path = Path(body.directory)
        if not dir_path.exists():
            raise HTTPException(status_code=404, detail=f"Directory not found: {body.directory}")
        
        # Scan and find orphans
        artifacts, errors = scan_artifacts(dir_path)
        index = get_index(dir_path)
        orphaned = find_orphaned_links(index)

        if not orphaned:
            return {
                "status": "success",
                "message": "No orphaned links found",
                "data": {
                    "dry_run": body.dry_run,
                    "orphans_found": 0,
                    "files_modified": 0,
                    "stubs_created": 0,
                    "changes": []
                }
            }
        
        changes = []
        files_modified = 0
        stubs_created = 0
        
        # For dry-run or actual repair, we'd implement the repair logic here
        # For now, just report what would be done
        for file_path, orphan_ids in orphaned.items():
            for orphan_id in orphan_ids:
                changes.append({
                    "file": str(file_path),
                    "action": "create_stub" if body.create_stubs else "remove_link",
                    "link_id": orphan_id
                })
        
        files_modified = len(orphaned)
        if body.create_stubs:
            stubs_created = sum(len(ids) for ids in orphaned.values())

        # Invalidate graph cache after actual repair (not dry-run)
        if not body.dry_run:
            invalidate_graph_cache(dir_path)

        return {
            "status": "success",
            "message": f"Repair {'preview' if body.dry_run else 'executed'}",
            "data": {
                "dry_run": body.dry_run,
                "orphans_found": sum(len(ids) for ids in orphaned.values()),
                "files_modified": files_modified if not body.dry_run else 0,
                "stubs_created": stubs_created if not body.dry_run else 0,
                "changes": changes[:20]  # Limit to first 20
            }
        }
    
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Repair error: {e}")


# Phase 10: Version Management Endpoints

@app.get("/api/v1/version/{artifact_id}/list", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def list_versions_endpoint(
    request: Request,
    artifact_id: str,
    tenant_id: Optional[str] = None,
    api_key: str = Security(verify_api_key)
):
    """
    List all versions for an artifact.

    Args:
        artifact_id: UUID of the artifact (with or without urn:uuid: prefix)
        tenant_id: Optional tenant ID for multi-tenant filtering

    Returns:
        List of version records with version numbers and timestamps
    """
    try:
        from tools.versioning import list_versions

        # Phase 12: Validate inputs to prevent path traversal
        artifact_id = validate_artifact_id(artifact_id)
        tenant_id = validate_tenant_id(tenant_id)

        # Normalize artifact_id (add urn:uuid: if not present)
        if not artifact_id.startswith("urn:uuid:"):
            artifact_id = f"urn:uuid:{artifact_id}"

        # Determine versions directory (tenant-aware)
        versions_dir = Path("memory/versions")
        if tenant_id:
            versions_dir = versions_dir / tenant_id
        
        # List versions
        versions = list_versions(artifact_id, versions_dir=versions_dir)
        
        return {
            "status": "success",
            "message": f"Found {len(versions)} version(s) for artifact {artifact_id}",
            "data": {
                "artifact_id": artifact_id,
                "tenant_id": tenant_id,
                "version_count": len(versions),
                "versions": [
                    {
                        "version_number": v.get("version_number"),
                        "uuid": v.get("uuid"),
                        "timestamp": v.get("createdAt"),
                        "change_summary": v.get("change_summary"),
                        "changed_by": v.get("changed_by")
                    }
                    for v in sorted(versions, key=lambda x: x.get("version_number", 0))
                ]
            }
        }
    
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail=f"Artifact not found: {e}")
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Version listing error: {e}")


@app.post("/api/v1/version/{artifact_id}/rollback", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def rollback_version_endpoint(
    request: Request,
    artifact_id: str,
    version_number: int = Body(..., embed=True),
    tenant_id: Optional[str] = None,
    api_key: str = Security(verify_api_key)
):
    """
    Rollback an artifact to a specific version.

    Args:
        artifact_id: UUID of the artifact (with or without urn:uuid: prefix)
        version_number: Target version number to rollback to
        tenant_id: Optional tenant ID for multi-tenant support

    Returns:
        Success message with updated artifact path
    """
    try:
        from tools.versioning import rollback_to_version
        from tools import factory
        from tools.artifacts import get_artifact_config

        # Phase 12: Validate inputs to prevent path traversal
        artifact_id = validate_artifact_id(artifact_id)
        tenant_id = validate_tenant_id(tenant_id)

        # Normalize artifact_id
        if not artifact_id.startswith("urn:uuid:"):
            artifact_id = f"urn:uuid:{artifact_id}"
        
        # Rollback to version
        restored_artifact = rollback_to_version(artifact_id, version_number)
        
        if not restored_artifact:
            raise HTTPException(
                status_code=404,
                detail=f"Version {version_number} not found for artifact {artifact_id}"
            )
        
        # Determine artifact type and output directory
        artifact_type_raw = restored_artifact.get("@type", "")
        if isinstance(artifact_type_raw, list):
            artifact_type_raw = artifact_type_raw[0] if artifact_type_raw else ""
        artifact_type = artifact_type_raw.split(":")[-1].lower() if artifact_type_raw else ""
        
        config = get_artifact_config(artifact_type)
        if not config:
            raise HTTPException(status_code=400, detail=f"Unknown artifact type: {artifact_type}")
        
        output_dir = Path(config["output_dir"])
        if tenant_id:
            output_dir = output_dir / tenant_id
        
        # Update timestamps
        from datetime import datetime, timezone
        restored_artifact["updatedAt"] = datetime.now(timezone.utc).isoformat()
        restored_artifact["novyx:updatedAt"] = restored_artifact["updatedAt"]
        
        # Persist the restored artifact (skip versioning to avoid circular versioning)
        updated_path = factory.persist_artifact(restored_artifact, output_dir, skip_versioning=True)
        
        return {
            "status": "success",
            "message": f"Artifact rolled back to version {version_number}",
            "data": {
                "artifact_id": artifact_id,
                "version_number": version_number,
                "tenant_id": tenant_id,
                "updated_path": str(updated_path),
                "updated_at": restored_artifact["updatedAt"]
            }
        }
    
    except HTTPException:
        raise
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail=f"Artifact or version not found: {e}")
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Rollback error: {e}")


# Phase 13: Batch Operations Endpoints

class BatchCreateRequest(BaseModel):
    """Request body for batch artifact creation."""
    artifacts: List[Dict[str, Any]] = Field(..., description="List of artifact payloads (max 100)")
    tenant_id: Optional[str] = Field(None, description="Tenant ID for multi-tenant storage")
    skip_versioning: bool = Field(False, description="Skip version tracking for bulk imports")


class BatchCountRequest(BaseModel):
    """Request body for batch count."""
    directory: str = Field("memory", description="Directory to count")
    tenant_id: Optional[str] = Field(None, description="Tenant ID filter")


@app.post("/api/v1/batch/create", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def batch_create_endpoint(
    request: Request,
    body: BatchCreateRequest,
    api_key: str = Security(verify_api_key)
):
    """
    Create multiple artifacts in a single batch operation (Phase 13).

    Validates and persists up to 100 artifacts atomically.
    Each artifact is created with proper hashing and indexing.

    Args:
        artifacts: List of artifact payloads (max 100)
        tenant_id: Optional tenant ID for multi-tenant storage
        skip_versioning: Skip version tracking for bulk imports

    Returns:
        Batch operation results with created/failed counts
    """
    try:
        from tools.batch import batch_create, MAX_BATCH_SIZE

        # Validate batch size
        if len(body.artifacts) > MAX_BATCH_SIZE:
            raise HTTPException(
                status_code=400,
                detail=f"Batch size {len(body.artifacts)} exceeds maximum {MAX_BATCH_SIZE}"
            )

        if not body.artifacts:
            return {
                "status": "success",
                "message": "No artifacts to create",
                "data": {"created": 0, "failed": 0, "results": [], "errors": []}
            }

        # Validate tenant_id if provided
        tenant_id = validate_tenant_id(body.tenant_id)

        # Execute batch create
        result = batch_create(
            body.artifacts,
            tenant_id=tenant_id,
            directory=DEFAULT_MEMORY_DIR,
            skip_versioning=body.skip_versioning
        )

        # Invalidate graph cache after batch create
        invalidate_graph_cache(DEFAULT_MEMORY_DIR)

        status = "success" if result["failed"] == 0 else "partial_success"
        message = f"Created {result['created']} artifact(s)"
        if result["failed"] > 0:
            message += f", {result['failed']} failed"

        return {
            "status": status,
            "message": message,
            "data": result
        }

    except HTTPException:
        raise
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Batch create error: {e}")


@app.get("/api/v1/batch/count", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def batch_count_endpoint(
    request: Request,
    directory: str = Query("memory", description="Directory to count"),
    tenant_id: Optional[str] = Query(None, description="Tenant ID filter"),
    api_key: str = Security(verify_api_key)
):
    """
    Count artifacts without loading content (fast).

    Uses file counting only - no parsing required.
    Useful for progress tracking and capacity planning.
    """
    try:
        from tools.batch import count_artifacts

        # Validate tenant_id if provided
        tenant_id = validate_tenant_id(tenant_id)

        count = count_artifacts(Path(directory), tenant_id=tenant_id)

        return {
            "status": "success",
            "message": f"Found {count} artifact(s)",
            "data": {
                "count": count,
                "directory": directory,
                "tenant_id": tenant_id
            }
        }

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Count error: {e}")


@app.post("/api/v1/batch/validate", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def batch_validate_endpoint(
    request: Request,
    directory: str = Body("memory", embed=True),
    tenant_id: Optional[str] = Body(None, embed=True),
    batch_size: int = Body(50, embed=True),
    api_key: str = Security(verify_api_key)
):
    """
    Validate artifacts in streaming batches (Phase 13).

    Processes artifacts in batches to limit memory usage.
    Returns aggregate validation results.
    """
    try:
        from tools.batch import batch_validate

        # Validate tenant_id if provided
        tenant_id = validate_tenant_id(tenant_id)

        # Aggregate results from streaming validation
        total_passed = 0
        total_failed = 0
        all_errors = []
        batches_processed = 0

        for result in batch_validate(Path(directory), tenant_id=tenant_id, batch_size=batch_size):
            batches_processed += 1
            total_passed += result["passed"]
            total_failed += result["failed"]
            all_errors.extend(result["errors"][:5])  # Limit errors per batch

        status = "success" if total_failed == 0 else "validation_failed"

        return {
            "status": status,
            "message": f"Validated {total_passed + total_failed} artifact(s): {total_passed} passed, {total_failed} failed",
            "data": {
                "total": total_passed + total_failed,
                "passed": total_passed,
                "failed": total_failed,
                "batches_processed": batches_processed,
                "errors": all_errors[:20]  # Limit total errors returned
            }
        }

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Batch validate error: {e}")


# Export/Import Endpoints (Phase 11)

class ExportRequest(BaseModel):
    """Request model for artifact export."""
    directory: str = Field("memory", description="Source directory")
    format: str = Field("jsonld", description="Export format: rdf, neo4j, cypher, jsonld")
    tenant_id: Optional[str] = Field(None, description="Filter by tenant ID")
    include_embeddings: bool = Field(False, description="Include vector embeddings")
    rdf_format: str = Field("turtle", description="RDF serialization format")


class ImportRequest(BaseModel):
    """Request model for artifact import."""
    format: str = Field("jsonld", description="Import format: rdf, jsonld")
    output_dir: str = Field("memory", description="Output directory")
    tenant_id: Optional[str] = Field(None, description="Tenant ID for imported artifacts")
    regenerate_hashes: bool = Field(True, description="Recalculate integrity hashes")
    rdf_format: str = Field("turtle", description="RDF serialization format")


@app.post("/api/v1/export", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def export_endpoint(
    request: Request,
    export_request: ExportRequest,
    api_key: str = Security(verify_api_key)
):
    """
    Export artifacts to external formats (Phase 11).

    Supports: RDF/Turtle, Neo4j Cypher, JSON-LD.
    Returns export statistics and base64-encoded file content.
    """
    try:
        from tools.export import export_artifacts, ExportFormat, check_dependencies
        import tempfile
        import base64

        # Validate tenant_id if provided
        tenant_id = validate_tenant_id(export_request.tenant_id)

        # Map format string to enum
        format_map = {
            "rdf": ExportFormat.RDF,
            "neo4j": ExportFormat.NEO4J,
            "cypher": ExportFormat.CYPHER,
            "jsonld": ExportFormat.JSONLD,
        }

        if export_request.format.lower() not in format_map:
            raise HTTPException(
                status_code=400,
                detail=f"Invalid format: {export_request.format}. Supported: rdf, neo4j, cypher, jsonld"
            )

        export_format = format_map[export_request.format.lower()]

        # Check dependencies
        available, msg = check_dependencies(export_format)
        if not available:
            raise HTTPException(status_code=400, detail=msg)

        # Export to temp file
        with tempfile.NamedTemporaryFile(mode='w', suffix='.export', delete=False) as tmp:
            tmp_path = Path(tmp.name)

        try:
            stats = export_artifacts(
                Path(export_request.directory),
                export_format,
                tmp_path,
                tenant_id=tenant_id,
                include_embeddings=export_request.include_embeddings,
                rdf_format=export_request.rdf_format
            )

            # Read exported content
            with open(tmp_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # Base64 encode for safe transport
            content_b64 = base64.b64encode(content.encode('utf-8')).decode('ascii')

            return {
                "status": "success",
                "message": f"Exported {stats['exported']} artifact(s) to {export_request.format.upper()}",
                "data": {
                    "total_artifacts": stats["total_artifacts"],
                    "exported": stats["exported"],
                    "failed": stats["failed"],
                    "format": export_request.format,
                    "content_base64": content_b64,
                    "triples": stats.get("triples"),
                    "nodes": stats.get("nodes")
                }
            }

        finally:
            # Clean up temp file
            tmp_path.unlink(missing_ok=True)

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Export error: {e}")


@app.post("/api/v1/import", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def import_endpoint(
    request: Request,
    file: UploadFile,
    format: str = Form("jsonld"),
    output_dir: str = Form("memory"),
    tenant_id: Optional[str] = Form(None),
    regenerate_hashes: bool = Form(True),
    rdf_format: str = Form("turtle"),
    api_key: str = Security(verify_api_key)
):
    """
    Import artifacts from external formats (Phase 11).

    Upload a file (RDF/Turtle or JSON-LD) to import artifacts.
    Supports: RDF/Turtle, JSON-LD.
    """
    try:
        from tools.import_data import import_artifacts, ImportFormat, check_dependencies
        import tempfile

        # Validate tenant_id if provided
        tenant_id = validate_tenant_id(tenant_id)

        # Map format string to enum
        format_map = {
            "rdf": ImportFormat.RDF,
            "jsonld": ImportFormat.JSONLD,
        }

        if format.lower() not in format_map:
            raise HTTPException(
                status_code=400,
                detail=f"Invalid format: {format}. Supported: rdf, jsonld"
            )

        import_format = format_map[format.lower()]

        # Check dependencies
        available, msg = check_dependencies(import_format)
        if not available:
            raise HTTPException(status_code=400, detail=msg)

        # Save uploaded file to temp location
        with tempfile.NamedTemporaryFile(mode='wb', suffix='.import', delete=False) as tmp:
            content = await file.read()
            tmp.write(content)
            tmp_path = Path(tmp.name)

        try:
            stats = import_artifacts(
                tmp_path,
                import_format,
                Path(output_dir),
                tenant_id=tenant_id,
                regenerate_hashes=regenerate_hashes,
                rdf_format=rdf_format
            )

            return {
                "status": "success",
                "message": f"Imported {stats['imported']} artifact(s) from {format.upper()}",
                "data": {
                    "total_items": stats.get("total_artifacts", stats.get("total_subjects", 0)),
                    "imported": stats["imported"],
                    "failed": stats["failed"],
                    "skipped": stats["skipped"],
                    "output_dir": stats["output_dir"],
                    "format": format
                }
            }

        finally:
            # Clean up temp file
            tmp_path.unlink(missing_ok=True)

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Import error: {e}")


# ============================================================================
# Federation Endpoints (Phase 14)
# ============================================================================

@app.get("/api/v1/federation/status", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def federation_status_endpoint(
    request: Request,
    api_key: str = Security(verify_api_key)
):
    """
    Get federation status (Phase 14).

    Returns overview of RemoteRefs and connected instances.
    """
    try:
        from tools.federation import federation_status

        status = federation_status()

        return {
            "status": "success",
            "message": f"Federation has {status['total']} RemoteRef(s)",
            "data": {
                "total_refs": status["total"],
                "by_status": status["by_status"],
                "by_direction": status["by_direction"],
                "remote_instances": status["remote_instances"],
                "requests_available": status["requests_available"]
            }
        }

    except ImportError:
        raise HTTPException(
            status_code=500,
            detail="Federation module not available"
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Federation status error: {e}")


@app.get("/api/v1/federation/refs", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def federation_list_refs(
    request: Request,
    status: Optional[str] = Query(None, description="Filter by sync status"),
    direction: Optional[str] = Query(None, description="Filter by direction"),
    tenant_id: Optional[str] = Query(None, description="Filter by tenant"),
    api_key: str = Security(verify_api_key)
):
    """
    List RemoteRef artifacts (Phase 14).

    Returns list of remote references with optional filtering.
    """
    try:
        from tools.federation import get_remote_refs

        # Validate tenant_id
        tenant_id = validate_tenant_id(tenant_id)

        refs = get_remote_refs(
            status=status,
            direction=direction,
            tenant_id=tenant_id
        )

        return {
            "status": "success",
            "message": f"Found {len(refs)} RemoteRef(s)",
            "data": {
                "count": len(refs),
                "refs": [
                    {
                        "uuid": ref.get("uuid"),
                        "remote_artifact_id": ref.get("remote_artifact_id"),
                        "remote_instance": ref.get("remote_instance"),
                        "sync_status": ref.get("sync_status"),
                        "direction": ref.get("direction"),
                        "local_artifact_id": ref.get("local_artifact_id"),
                        "last_synced_at": ref.get("last_synced_at")
                    }
                    for ref in refs
                ]
            }
        }

    except ImportError:
        raise HTTPException(
            status_code=500,
            detail="Federation module not available"
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"List refs error: {e}")


@app.post("/api/v1/federation/pull", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def federation_pull(
    request: Request,
    remote_instance: str = Query(..., description="Remote instance URL"),
    artifact_id: str = Query(..., description="Remote artifact ID"),
    tenant_id: Optional[str] = Query(None, description="Tenant for local storage"),
    api_key: str = Security(verify_api_key)
):
    """
    Pull an artifact from a remote instance (Phase 14).

    Fetches artifact from remote, creates local copy, and tracks with RemoteRef.
    """
    try:
        from tools.federation import pull_artifact, REQUESTS_AVAILABLE

        if not REQUESTS_AVAILABLE:
            raise HTTPException(
                status_code=503,
                detail="requests library not available. Install with: pip install requests"
            )

        # Validate tenant_id
        tenant_id = validate_tenant_id(tenant_id)

        artifact, ref = pull_artifact(
            remote_instance,
            artifact_id,
            tenant_id=tenant_id
        )

        return {
            "status": "success",
            "message": f"Pulled artifact from {remote_instance}",
            "data": {
                "local_artifact_id": artifact.get("uuid"),
                "remote_artifact_id": artifact_id,
                "remote_instance": remote_instance,
                "sync_status": ref.get("sync_status"),
                "ref_uuid": ref.get("uuid")
            }
        }

    except ImportError:
        raise HTTPException(
            status_code=500,
            detail="Federation module not available"
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Pull error: {e}")


@app.post("/api/v1/federation/sync", response_model=StandardResponse)
@limiter.limit(RATE_LIMIT)
async def federation_sync(
    request: Request,
    status: Optional[str] = Query(None, description="Filter refs by status"),
    direction: Optional[str] = Query(None, description="Filter refs by direction"),
    tenant_id: Optional[str] = Query(None, description="Filter refs by tenant"),
    conflict_resolution: Optional[str] = Query(None, description="Override conflict resolution"),
    api_key: str = Security(verify_api_key)
):
    """
    Sync pending RemoteRefs (Phase 14).

    Synchronizes all matching refs with their remote instances.
    """
    try:
        from tools.federation import sync_refs

        # Validate tenant_id
        tenant_id = validate_tenant_id(tenant_id)

        results = sync_refs(
            status=status,
            direction=direction,
            tenant_id=tenant_id,
            conflict_resolution=conflict_resolution
        )

        return {
            "status": "success" if results["failed"] == 0 else "partial",
            "message": f"Synced {results['synced']} ref(s), {results['failed']} failed",
            "data": {
                "synced": results["synced"],
                "failed": results["failed"],
                "conflicts": results["conflicts"],
                "errors": results["errors"]
            }
        }

    except ImportError:
        raise HTTPException(
            status_code=500,
            detail="Federation module not available"
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Sync error: {e}")


# Error handlers

@app.exception_handler(404)
async def not_found_handler(request, exc):
    """Handle 404 errors."""
    return JSONResponse(
        status_code=404,
        content={"status": "error", "message": "Endpoint not found"}
    )


@app.exception_handler(500)
async def internal_error_handler(request, exc):
    """Handle 500 errors."""
    return JSONResponse(
        status_code=500,
        content={"status": "error", "message": "Internal server error"}
    )


# Main entry point
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
