"""
Octopoda Cloud API Server
========================
FastAPI-based REST API for external developers to interact with Octopoda.
Runs on port 8741 (separate from the Flask dashboard on 7842).

Auto-generated docs at /docs (Swagger UI).
"""

import time
import os
import threading
from collections import OrderedDict

from fastapi import FastAPI, Depends, HTTPException, Header, Request, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from typing import Any, Optional, List


# ---------------------------------------------------------------------------
# Lightweight in-process rate limiter (no external dependency)
# ---------------------------------------------------------------------------

# Per-plan rate limits (requests per minute)
_PLAN_RATE_LIMITS = {
    "free": 10_000,       # ~166/sec — enough for 50+ concurrent agents
    "pro": 60_000,        # ~1000/sec — production workloads
    "enterprise": 300_000, # ~5000/sec — heavy enterprise usage
}
_DEFAULT_RPM = int(os.environ.get("SYNRIX_RATE_LIMIT_RPM", "10000"))


class _RateLimiter:
    """Token-bucket rate limiter keyed by tenant ID (not IP)."""

    def __init__(self):
        self._buckets: dict = {}  # tenant_id -> [tokens, last_refill, rpm]
        self._lock = threading.Lock()

    def _refill(self, bucket: list):
        now = time.monotonic()
        elapsed = now - bucket[1]
        rpm = bucket[2]
        bucket[0] = min(rpm, bucket[0] + elapsed * (rpm / 60.0))
        bucket[1] = now

    def allow(self, tenant_id: str, plan: str = "free", rpm_override: int = 0) -> bool:
        rpm = rpm_override if rpm_override > 0 else _PLAN_RATE_LIMITS.get(plan, _DEFAULT_RPM)
        with self._lock:
            if tenant_id not in self._buckets:
                self._buckets[tenant_id] = [rpm, time.monotonic(), rpm]
            bucket = self._buckets[tenant_id]
            bucket[2] = rpm  # update if plan changed
            self._refill(bucket)
            if bucket[0] >= 1.0:
                bucket[0] -= 1.0
                return True
            return False

    def get_remaining(self, tenant_id: str) -> int:
        with self._lock:
            if tenant_id not in self._buckets:
                return _DEFAULT_RPM
            bucket = self._buckets[tenant_id]
            self._refill(bucket)
            return int(bucket[0])


_rate_limiter = _RateLimiter()

# Separate stricter rate limiter for auth endpoints (prevent brute-force)
_AUTH_RPM = 20  # 20 attempts per minute per IP
_auth_rate_limiter = _RateLimiter()

from synrix_runtime.api.cloud_models import (
    RegisterAgentRequest, RememberRequest, BatchRememberRequest,
    SnapshotRequest, RestoreRequest,
    SharedWriteRequest, TaskCreateRequest, TaskActionRequest, DecisionLogRequest,
    RawWriteRequest, HealthResponse, MemoryResponse, RecallResponse, SearchResponse,
    SnapshotResponse, RestoreResponse, AgentResponse, BatchMemoryResponse, ErrorResponse,
)
from synrix_runtime.api.auth import APIKeyManager
from synrix_runtime.log import get_logger

logger = get_logger("api")

# ---------------------------------------------------------------------------
# App setup
# ---------------------------------------------------------------------------

app = FastAPI(
    title="Octopoda Agent Memory API",
    version="2.0.0",
    description="Persistent Memory Kernel for AI Agents. Sub-millisecond crash recovery, shared memory bus, full audit trail.",
    docs_url="/docs",
    redoc_url="/redoc",
)

# CORS: restrict to localhost by default; set SYNRIX_CORS_ORIGINS to override
_cors_origins = os.environ.get("SYNRIX_CORS_ORIGINS", "").strip()
if _cors_origins:
    _allowed_origins = [o.strip() for o in _cors_origins.split(",") if o.strip()]
    _origin_regex = None
else:
    _allowed_origins = [
        "http://localhost:7842", "http://127.0.0.1:7842",
        "http://localhost:8741", "http://127.0.0.1:8741",
        "http://localhost:3000", "http://localhost:5173",
    ]
    # Also allow Loveable preview domains
    _origin_regex = r"https://.*\.(lovable\.app|lovableproject\.com)"

app.add_middleware(
    CORSMiddleware,
    allow_origins=_allowed_origins,
    allow_origin_regex=_origin_regex,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
    allow_credentials=True,
)


@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    """Enforce per-tenant rate limiting on all endpoints except /health and auth."""
    path = request.url.path
    if path == "/health":
        return await call_next(request)

    # Rate limit auth endpoints by IP (prevent brute-force)
    if path in ("/v1/auth/login", "/v1/auth/signup"):
        client_ip = request.client.host if request.client else "unknown"
        if not _auth_rate_limiter.allow(f"auth:{client_ip}", rpm_override=_AUTH_RPM):
            from fastapi.responses import JSONResponse
            return JSONResponse(
                status_code=429,
                content={"detail": "Too many attempts. Try again later.", "retry_after_seconds": 60},
            )
        return await call_next(request)

    # Extract tenant from auth header for rate limiting
    tenant_id = "anonymous"
    plan = "free"
    auth_header = request.headers.get("authorization", "") or request.headers.get("x-api-key", "")
    if auth_header.startswith("Bearer "):
        api_key = auth_header[7:]
        try:
            from synrix_runtime.api.tenant import TenantManager
            tm = TenantManager.get_instance()
            tenant = tm.verify_api_key(api_key)
            if tenant:
                tenant_id = tenant.get("tenant_id", "anonymous")
                plan = tenant.get("plan", "free")
        except Exception:
            pass

    if not _rate_limiter.allow(tenant_id, plan):
        from fastapi.responses import JSONResponse
        remaining = _rate_limiter.get_remaining(tenant_id)
        rpm = _PLAN_RATE_LIMITS.get(plan, _DEFAULT_RPM)
        return JSONResponse(
            status_code=429,
            content={
                "detail": "Rate limit exceeded.",
                "limit": rpm,
                "plan": plan,
                "retry_after_seconds": 1,
            },
            headers={"Retry-After": "1", "X-RateLimit-Limit": str(rpm), "X-RateLimit-Remaining": str(remaining)},
        )
    return await call_next(request)


# Global state (initialized by start_cloud_server())
_boot_time = time.time()
_daemon = None
_auth_manager = None
_config = None


def init_cloud_server(daemon, config):
    """Initialize the cloud server with daemon and config references."""
    global _daemon, _auth_manager, _config, _boot_time
    _daemon = daemon
    _config = config
    _boot_time = time.time()
    _auth_manager = APIKeyManager(
        backend=daemon.backend if daemon else None,
        master_key=config.api_key if config else "",
    )


# ---------------------------------------------------------------------------
# Auth dependency
# ---------------------------------------------------------------------------

async def verify_auth(authorization: Optional[str] = Header(None)):
    """Verify API key. Returns tenant info dict or None."""
    auth_disabled = os.environ.get("SYNRIX_AUTH_DISABLED", "").strip() == "1"
    if auth_disabled:
        # Only allow in local development — refuse if running on a public port
        bind_host = os.environ.get("SYNRIX_API_HOST", "127.0.0.1")
        if bind_host not in ("127.0.0.1", "localhost", "::1"):
            logger.error("SYNRIX_AUTH_DISABLED=1 is NOT allowed when binding to %s — ignoring", bind_host)
        else:
            return {"tenant_id": "dev", "plan": "pro", "max_agents": 100, "max_memories_per_agent": 100000}

    # Try multi-tenant auth first
    try:
        from synrix_runtime.api.tenant import TenantManager
        tm = TenantManager.get_instance()
        if authorization:
            tenant_info = tm.verify_api_key(authorization)
            if tenant_info:
                return tenant_info
    except Exception:
        pass

    # Fallback to legacy auth
    if _auth_manager:
        if not authorization:
            raise HTTPException(
                status_code=401,
                detail="API key required. Pass Authorization: Bearer sk-octopoda-... "
                       "Sign up at POST /v1/auth/signup",
            )
        key_info = _auth_manager.verify_key(authorization)
        if not key_info:
            raise HTTPException(status_code=401, detail="Invalid API key")
        return {"tenant_id": key_info.tenant_id, "plan": "legacy", "max_agents": 100, "max_memories_per_agent": 100000}
    return None


# ---------------------------------------------------------------------------
# Helper: get or create AgentRuntime (tenant-isolated)
# ---------------------------------------------------------------------------

_agent_runtimes: OrderedDict = OrderedDict()
_MAX_CACHED_RUNTIMES = 1000


def _get_tenant_id(auth) -> str:
    """Extract tenant_id from auth info. Raises 401 if not authenticated."""
    if auth and isinstance(auth, dict):
        tid = auth.get("tenant_id")
        if tid:
            return tid
    raise HTTPException(status_code=401, detail="Authentication required")


def _get_runtime(agent_id: str, auth=None):
    """Get or create a tenant-isolated AgentRuntime."""
    tenant_id = _get_tenant_id(auth)
    cache_key = f"{tenant_id}:{agent_id}"

    if cache_key in _agent_runtimes:
        _agent_runtimes.move_to_end(cache_key)
        return _agent_runtimes[cache_key]

    # Use TenantManager for isolated runtime
    try:
        from synrix_runtime.api.tenant import TenantManager, TenantLimitError
        tm = TenantManager.get_instance()
        runtime = tm.get_runtime(tenant_id, agent_id)
    except TenantLimitError as e:
        raise HTTPException(status_code=403, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=503, detail=f"Failed to initialize tenant runtime: {e}")

    # Attach per-tenant LLM config so fact extractor uses the right provider
    tenant_settings = _get_tenant_settings(tenant_id)
    if tenant_settings:
        # Check platform free tier limit
        if tenant_settings.get("llm_provider") == "platform":
            used = tenant_settings.get("platform_extractions_used", 0)
            if used >= _PLATFORM_FREE_LIMIT:
                # Exceeded free tier — downgrade to embedding-only
                tenant_settings["llm_provider"] = "none"
                _save_tenant_settings(tenant_id, tenant_settings)
                logger.info("Tenant %s exceeded platform free tier (%d/%d), downgraded to embedding-only",
                           tenant_id, used, _PLATFORM_FREE_LIMIT)
        runtime._llm_config = tenant_settings

    # Evict oldest if at capacity
    while len(_agent_runtimes) >= _MAX_CACHED_RUNTIMES:
        oldest_key, oldest_rt = _agent_runtimes.popitem(last=False)
        logger.info("Evicted stale runtime: %s", oldest_key)

    _agent_runtimes[cache_key] = runtime
    return runtime


# ---------------------------------------------------------------------------
# Auth: Signup / Login
# ---------------------------------------------------------------------------

from pydantic import BaseModel as _PydanticBase

class SignupRequest(_PydanticBase):
    email: str
    password: str

class LoginRequest(_PydanticBase):
    email: str
    password: str


import re as _re

_EMAIL_RE = _re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
_AGENT_ID_RE = _re.compile(r"^[a-zA-Z0-9_\-\.]{1,128}$")
_KEY_RE = _re.compile(r"^[a-zA-Z0-9_\-\.:/]{1,512}$")


def _validate_email(email: str):
    if not email or len(email) > 254 or not _EMAIL_RE.match(email):
        raise HTTPException(status_code=422, detail="Invalid email address")

def _validate_password(password: str):
    if not password or len(password) < 8:
        raise HTTPException(status_code=422, detail="Password must be at least 8 characters")
    if len(password) > 128:
        raise HTTPException(status_code=422, detail="Password too long (max 128 characters)")

def _validate_agent_id(agent_id: str):
    if not _AGENT_ID_RE.match(agent_id):
        raise HTTPException(
            status_code=422,
            detail="Invalid agent_id. Use letters, numbers, hyphens, underscores, dots (max 128 chars)",
        )

def _validate_key(key: str):
    if not _KEY_RE.match(key):
        raise HTTPException(
            status_code=422,
            detail="Invalid key. Use letters, numbers, hyphens, underscores, dots, colons, slashes (max 512 chars)",
        )


@app.post("/v1/auth/signup")
async def signup(req: SignupRequest):
    """Create a new account. Returns tenant_id + API key."""
    _validate_email(req.email)
    _validate_password(req.password)
    from synrix_runtime.api.tenant import TenantManager
    tm = TenantManager.get_instance()
    result = tm.create_tenant(req.email, req.password)
    if not result.get("success"):
        raise HTTPException(status_code=409, detail=result.get("error", "Signup failed"))
    return result


@app.post("/v1/auth/login")
async def login(req: LoginRequest):
    """Login with email + password. Returns tenant info + API key."""
    _validate_email(req.email)
    from synrix_runtime.api.tenant import TenantManager
    tm = TenantManager.get_instance()
    tenant = tm.authenticate(req.email, req.password)
    if not tenant:
        raise HTTPException(status_code=401, detail="Invalid email or password")
    api_key = tm.create_session_key(tenant["tenant_id"])
    return {
        "tenant_id": tenant["tenant_id"],
        "email": tenant["email"],
        "plan": tenant["plan"],
        "api_key": api_key,
    }


@app.post("/v1/auth/api-key")
async def regenerate_key(auth=Depends(verify_auth)):
    """Generate a new API key (deactivates old ones)."""
    tenant_id = _get_tenant_id(auth)
    from synrix_runtime.api.tenant import TenantManager
    tm = TenantManager.get_instance()
    new_key = tm.regenerate_api_key(tenant_id)
    if not new_key:
        raise HTTPException(status_code=500, detail="Failed to regenerate key")
    return {"api_key": new_key, "warning": "Save this key — it will not be shown again."}


@app.get("/v1/auth/me")
async def get_me(auth=Depends(verify_auth)):
    """Get current account info."""
    tenant_id = _get_tenant_id(auth)
    from synrix_runtime.api.tenant import TenantManager
    tm = TenantManager.get_instance()
    tenant = tm.get_tenant(tenant_id)
    if not tenant:
        return {"tenant_id": tenant_id, "plan": "dev"}
    return {
        "tenant_id": tenant["tenant_id"],
        "email": tenant["email"],
        "plan": tenant["plan"],
        "max_agents": tenant["max_agents"],
        "max_memories_per_agent": tenant["max_memories_per_agent"],
    }


class ChangePasswordRequest(_PydanticBase):
    old_password: str
    new_password: str


@app.post("/v1/auth/change-password")
async def change_password(req: ChangePasswordRequest, auth=Depends(verify_auth)):
    """Change account password."""
    tenant_id = _get_tenant_id(auth)
    from synrix_runtime.api.tenant import TenantManager
    tm = TenantManager.get_instance()
    result = tm.change_password(tenant_id, req.old_password, req.new_password)
    if not result.get("success"):
        raise HTTPException(status_code=400, detail=result.get("error", "Password change failed"))
    return {"success": True}


# ---------------------------------------------------------------------------
# GDPR: Data export & account deletion
# ---------------------------------------------------------------------------

@app.get("/v1/auth/export")
async def export_data(auth=Depends(verify_auth)):
    """Download all your data as JSON (GDPR Article 20)."""
    tenant_id = _get_tenant_id(auth)
    from synrix_runtime.api.tenant import TenantManager
    tm = TenantManager.get_instance()
    return tm.export_tenant_data(tenant_id)


@app.delete("/v1/auth/account")
async def delete_account(auth=Depends(verify_auth)):
    """Permanently delete your account and all data (GDPR Article 17)."""
    tenant_id = _get_tenant_id(auth)
    from synrix_runtime.api.tenant import TenantManager
    tm = TenantManager.get_instance()
    result = tm.delete_tenant(tenant_id)
    if not result.get("deleted"):
        raise HTTPException(status_code=404, detail=result.get("error", "Deletion failed"))
    # Clear from runtime cache
    cache_keys = [k for k in _agent_runtimes if k.startswith(f"{tenant_id}:")]
    for k in cache_keys:
        _agent_runtimes.pop(k, None)
    return result


# ---------------------------------------------------------------------------
# Usage stats
# ---------------------------------------------------------------------------

@app.get("/v1/usage")
async def usage_stats(auth=Depends(verify_auth)):
    """Get usage statistics — agents, memories, plan limits."""
    tenant_id = _get_tenant_id(auth)
    from synrix_runtime.api.tenant import TenantManager
    tm = TenantManager.get_instance()
    return tm.get_tenant_usage(tenant_id)


# ---------------------------------------------------------------------------
# Health & System
# ---------------------------------------------------------------------------

@app.get("/health", response_model=HealthResponse)
async def health():
    backend_type = "unknown"
    if _daemon and hasattr(_daemon, 'backend'):
        backend_type = getattr(_daemon.backend, 'backend_type', 'unknown')
    return HealthResponse(
        status="ok",
        version="2.0.0",
        backend=backend_type,
        uptime_seconds=time.time() - _boot_time,
    )


@app.get("/v1/status")
async def system_status(auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    agents = _get_agents_from_backend(backend)
    active = [a for a in agents if a.get("state") != "deregistered"]
    return {
        "status": "running",
        "uptime_seconds": round(time.time() - _boot_time, 1),
        "version": "2.0.0",
        "total_agents": len(agents),
        "active_agents": len(active),
        "agents": active,
    }


# ---------------------------------------------------------------------------
# Agent Management (with pagination)
# ---------------------------------------------------------------------------

@app.post("/v1/agents", response_model=AgentResponse)
async def register_agent(req: RegisterAgentRequest, auth=Depends(verify_auth)):
    _validate_agent_id(req.agent_id)
    try:
        runtime = _get_runtime(req.agent_id, auth)
        return AgentResponse(
            agent_id=req.agent_id,
            agent_type=req.agent_type,
            status="running",
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/v1/agents")
async def list_agents(
    offset: int = Query(default=0, ge=0),
    limit: int = Query(default=50, ge=1, le=500),
    auth=Depends(verify_auth),
):
    tenant_id = _get_tenant_id(auth)
    try:
        from synrix_runtime.api.tenant import TenantManager
        tm = TenantManager.get_instance()
        all_agents = tm.get_tenant_agents(tenant_id)
    except Exception:
        all_agents = []
    total = len(all_agents)
    page = all_agents[offset:offset + limit]
    return {"agents": page, "count": len(page), "total": total, "offset": offset, "limit": limit}


@app.get("/v1/agents/{agent_id}")
async def get_agent(agent_id: str, auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    if not backend:
        raise HTTPException(status_code=503, detail="Backend not available")
    state_result = backend.read(f"runtime:agents:{agent_id}:state")
    state = None
    if state_result:
        data = state_result.get("data", {})
        val = data.get("value", data)
        state = val.get("value") if isinstance(val, dict) else val
    if state:
        try:
            from synrix_runtime.monitoring.metrics import MetricsCollector
            mc = MetricsCollector(backend)
            metrics = mc.get_agent_metrics(agent_id)
            return {
                "agent_id": agent_id,
                "state": state,
                "metrics": {
                    "total_operations": metrics.total_operations,
                    "total_writes": metrics.total_writes,
                    "total_reads": metrics.total_reads,
                    "avg_write_latency_us": metrics.avg_write_latency_us,
                    "avg_read_latency_us": metrics.avg_read_latency_us,
                    "crash_count": metrics.crash_count,
                    "performance_score": metrics.performance_score,
                    "uptime_seconds": metrics.uptime_seconds,
                },
            }
        except Exception:
            return {"agent_id": agent_id, "state": state}
    raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")


@app.delete("/v1/agents/{agent_id}")
async def deregister_agent(agent_id: str, auth=Depends(verify_auth)):
    tenant_id = _get_tenant_id(auth)
    cache_key = f"{tenant_id}:{agent_id}"
    if cache_key in _agent_runtimes:
        _agent_runtimes[cache_key].shutdown()
        del _agent_runtimes[cache_key]
    backend = _get_tenant_backend(auth)
    if backend:
        backend.write(f"runtime:agents:{agent_id}:state", {"value": "deregistered"})
        backend.write(f"runtime:agents:{agent_id}:last_active", {"value": time.time()})
    return {"agent_id": agent_id, "deregistered": True}


# ---------------------------------------------------------------------------
# Memory Operations
# ---------------------------------------------------------------------------

@app.post("/v1/agents/{agent_id}/remember", response_model=MemoryResponse)
async def remember(agent_id: str, req: RememberRequest, auth=Depends(verify_auth)):
    _validate_agent_id(agent_id)
    _validate_key(req.key)
    # License enforcement: check memory limit
    try:
        from synrix.licensing import check_memory_limit, record_memory_written, MemoryLimitError
        check_memory_limit(agent_id)
    except MemoryLimitError as e:
        raise HTTPException(status_code=403, detail=str(e))
    except Exception:
        pass

    runtime = _get_runtime(agent_id, auth)
    tenant_id = _get_tenant_id(auth)
    result = runtime.remember(req.key, req.value, tags=req.tags)

    # Track platform free tier usage
    _increment_platform_usage(tenant_id)

    # Track the write
    try:
        from synrix.licensing import record_memory_written
        record_memory_written(agent_id)
    except Exception:
        pass

    return MemoryResponse(
        node_id=result.node_id,
        key=req.key,
        latency_us=result.latency_us,
        timestamp=result.timestamp,
        success=result.success,
    )


@app.post("/v1/agents/{agent_id}/remember/batch", response_model=BatchMemoryResponse)
async def remember_batch(agent_id: str, req: BatchRememberRequest, auth=Depends(verify_auth)):
    runtime = _get_runtime(agent_id, auth)
    results = []
    for item in req.items:
        # License enforcement: check memory limit per item
        try:
            from synrix.licensing import check_memory_limit, record_memory_written, MemoryLimitError
            check_memory_limit(agent_id)
        except MemoryLimitError as e:
            raise HTTPException(status_code=403, detail=str(e))
        except Exception:
            pass

        result = runtime.remember(item.key, item.value, tags=item.tags)

        try:
            record_memory_written(agent_id)
        except Exception:
            pass

        results.append({
            "key": item.key,
            "node_id": result.node_id,
            "latency_us": result.latency_us,
            "success": result.success,
        })
    return BatchMemoryResponse(agent_id=agent_id, results=results, count=len(results))


@app.get("/v1/agents/{agent_id}/recall/{key:path}", response_model=RecallResponse)
async def recall(agent_id: str, key: str, auth=Depends(verify_auth)):
    runtime = _get_runtime(agent_id, auth)
    result = runtime.recall(key)
    return RecallResponse(
        value=result.value,
        key=key,
        latency_us=result.latency_us,
        found=result.found,
    )


@app.get("/v1/agents/{agent_id}/search", response_model=SearchResponse)
async def search(
    agent_id: str,
    prefix: str = "",
    limit: int = Query(default=50, ge=1, le=1000),
    auth=Depends(verify_auth),
):
    runtime = _get_runtime(agent_id, auth)
    result = runtime.search(prefix, limit=limit)
    return SearchResponse(
        items=result.items,
        count=result.count,
        latency_us=result.latency_us,
    )


@app.get("/v1/agents/{agent_id}/similar")
async def semantic_search(
    agent_id: str,
    q: str = Query(..., description="Natural language search query"),
    limit: int = Query(default=10, ge=1, le=100),
    auth=Depends(verify_auth),
):
    """Semantic search — find memories by meaning, not just exact keys."""
    runtime = _get_runtime(agent_id, auth)
    result = runtime.recall_similar(q, limit=limit)
    return {
        "agent_id": agent_id,
        "query": q,
        "items": result.items,
        "count": result.count,
        "latency_us": result.latency_us,
    }


@app.get("/v1/agents/{agent_id}/history/{key:path}")
async def memory_history(agent_id: str, key: str, auth=Depends(verify_auth)):
    """Get all versions of a memory over time."""
    runtime = _get_runtime(agent_id, auth)
    result = runtime.recall_history(key)
    return {
        "agent_id": agent_id,
        "key": result.key,
        "current_version": result.current_version,
        "versions": result.versions,
        "latency_us": result.latency_us,
    }


@app.get("/v1/agents/{agent_id}/related/{entity}")
async def related_entities(agent_id: str, entity: str, auth=Depends(verify_auth)):
    """Query the knowledge graph for entity relationships."""
    runtime = _get_runtime(agent_id, auth)
    result = runtime.related(entity)
    return {
        "agent_id": agent_id,
        "entity": result.entity,
        "entity_type": result.entity_type,
        "found": result.found,
        "relationships": result.relationships,
        "latency_us": result.latency_us,
    }


@app.get("/v1/agents/{agent_id}/memory")
async def list_memory(
    agent_id: str,
    offset: int = Query(default=0, ge=0),
    limit: int = Query(default=50, ge=1, le=500),
    auth=Depends(verify_auth),
):
    tenant_id = _get_tenant_id(auth)
    try:
        from synrix_runtime.api.tenant import TenantManager
        backend = TenantManager.get_instance().get_backend(tenant_id)
    except Exception:
        backend = None
    if backend:
        start = time.perf_counter_ns()
        results = backend.query_prefix(f"agents:{agent_id}:", limit=offset + limit)
        latency_us = (time.perf_counter_ns() - start) / 1000
        items = []
        for item in results:
            key = item.get("key", "")
            data = item.get("data", {})
            items.append({"key": key, "value": data.get("value", data)})
        page = items[offset:offset + limit]
        # Record read metric
        try:
            from synrix_runtime.monitoring.metrics import MetricsCollector
            mc = MetricsCollector(backend)
            mc.record_read(agent_id, f"memory:list", latency_us, len(page) > 0)
        except Exception:
            pass
        return {"agent_id": agent_id, "items": page, "count": len(page), "total": len(items), "offset": offset}
    return {"agent_id": agent_id, "items": [], "count": 0, "total": 0, "offset": offset}


# ---------------------------------------------------------------------------
# TTL / Auto-Expire
# ---------------------------------------------------------------------------

@app.post("/v1/agents/{agent_id}/remember/ttl")
async def remember_with_ttl(agent_id: str, req: dict, auth=Depends(verify_auth)):
    """Store a memory that auto-expires after ttl_seconds."""
    key = req.get("key")
    value = req.get("value")
    ttl_seconds = req.get("ttl_seconds", 3600)
    tags = req.get("tags")
    if not key or value is None:
        raise HTTPException(status_code=422, detail="key and value required")
    if ttl_seconds < 1 or ttl_seconds > 31536000:  # max 1 year
        raise HTTPException(status_code=422, detail="ttl_seconds must be 1-31536000")
    runtime = _get_runtime(agent_id, auth)
    result = runtime.remember_with_ttl(key, value, ttl_seconds, tags=tags)
    return {
        "node_id": result.node_id,
        "key": key,
        "ttl_seconds": ttl_seconds,
        "expires_at": time.time() + ttl_seconds,
        "latency_us": result.latency_us,
        "success": result.success,
    }

@app.post("/v1/agents/{agent_id}/cleanup")
async def cleanup_expired(agent_id: str, auth=Depends(verify_auth)):
    """Remove all expired TTL memories for this agent."""
    runtime = _get_runtime(agent_id, auth)
    result = runtime.cleanup_expired()
    return result


# ---------------------------------------------------------------------------
# Memory Importance Scoring
# ---------------------------------------------------------------------------

@app.post("/v1/agents/{agent_id}/remember/important")
async def remember_important(agent_id: str, req: dict, auth=Depends(verify_auth)):
    """Store a memory with importance level (critical/normal/low)."""
    key = req.get("key")
    value = req.get("value")
    importance = req.get("importance", "normal")
    tags = req.get("tags")
    if not key or value is None:
        raise HTTPException(status_code=422, detail="key and value required")
    if importance not in ("critical", "normal", "low"):
        raise HTTPException(status_code=422, detail="importance must be critical, normal, or low")
    runtime = _get_runtime(agent_id, auth)
    result = runtime.remember_important(key, value, importance=importance, tags=tags)
    return {
        "node_id": result.node_id,
        "key": key,
        "importance": importance,
        "latency_us": result.latency_us,
        "success": result.success,
    }


# ---------------------------------------------------------------------------
# Conflict Detection
# ---------------------------------------------------------------------------

@app.post("/v1/agents/{agent_id}/conflicts")
async def detect_conflicts(agent_id: str, req: dict, auth=Depends(verify_auth)):
    """Check if a new value conflicts with existing memories."""
    key = req.get("key", "")
    value = req.get("value")
    threshold = req.get("threshold", 0.7)
    if value is None:
        raise HTTPException(status_code=422, detail="value required")
    runtime = _get_runtime(agent_id, auth)
    result = runtime.detect_conflicts(key, value, threshold=threshold)
    return result

@app.post("/v1/agents/{agent_id}/remember/safe")
async def remember_safe(agent_id: str, req: dict, auth=Depends(verify_auth)):
    """Write a memory and return any detected conflicts."""
    key = req.get("key")
    value = req.get("value")
    tags = req.get("tags")
    threshold = req.get("conflict_threshold", 0.85)
    if not key or value is None:
        raise HTTPException(status_code=422, detail="key and value required")
    runtime = _get_runtime(agent_id, auth)
    result = runtime.remember_safe(key, value, tags=tags, conflict_threshold=threshold)
    return result


# ---------------------------------------------------------------------------
# Usage Analytics
# ---------------------------------------------------------------------------

@app.get("/v1/agents/{agent_id}/analytics")
async def agent_analytics(agent_id: str, auth=Depends(verify_auth)):
    """Get detailed usage analytics for an agent."""
    runtime = _get_runtime(agent_id, auth)
    return runtime.usage_analytics()


# ---------------------------------------------------------------------------
# Webhooks
# ---------------------------------------------------------------------------

# In-memory webhook registry (per-tenant)
_webhook_registry: dict = {}  # tenant_id -> list of webhook configs

@app.post("/v1/webhooks")
async def register_webhook(req: dict, auth=Depends(verify_auth)):
    """Register a webhook URL to receive event notifications.

    Events: agent.crash, agent.recovery, memory.limit, memory.conflict
    """
    url = req.get("url")
    events = req.get("events", ["agent.crash", "agent.recovery"])
    if not url:
        raise HTTPException(status_code=422, detail="url required")
    # SSRF protection: only allow HTTPS URLs to public hosts
    from urllib.parse import urlparse
    parsed = urlparse(url)
    if parsed.scheme not in ("https",):
        raise HTTPException(status_code=422, detail="Only HTTPS webhook URLs are allowed")
    hostname = parsed.hostname or ""
    if hostname in ("localhost", "127.0.0.1", "0.0.0.0", "::1") or hostname.startswith("10.") \
       or hostname.startswith("172.") or hostname.startswith("192.168.") or hostname.startswith("169.254."):
        raise HTTPException(status_code=422, detail="Webhook URLs must point to public hosts")
    tenant_id = _get_tenant_id(auth)
    if tenant_id not in _webhook_registry:
        _webhook_registry[tenant_id] = []
    webhook_id = f"wh_{int(time.time()*1000)}"
    _webhook_registry[tenant_id].append({
        "id": webhook_id,
        "url": url,
        "events": events,
        "created_at": time.time(),
        "active": True,
    })
    return {"id": webhook_id, "url": url, "events": events, "active": True}

@app.get("/v1/webhooks")
async def list_webhooks(auth=Depends(verify_auth)):
    """List all registered webhooks."""
    tenant_id = _get_tenant_id(auth)
    hooks = _webhook_registry.get(tenant_id, [])
    return {"webhooks": [h for h in hooks if h["active"]], "count": len(hooks)}

@app.delete("/v1/webhooks/{webhook_id}")
async def delete_webhook(webhook_id: str, auth=Depends(verify_auth)):
    """Delete a webhook."""
    tenant_id = _get_tenant_id(auth)
    hooks = _webhook_registry.get(tenant_id, [])
    for h in hooks:
        if h["id"] == webhook_id:
            h["active"] = False
            return {"deleted": True, "id": webhook_id}
    raise HTTPException(status_code=404, detail="Webhook not found")


def _fire_webhooks(tenant_id: str, event: str, payload: dict):
    """Fire webhooks for a given event (runs in background thread)."""
    hooks = _webhook_registry.get(tenant_id, [])
    for h in hooks:
        if h["active"] and event in h["events"]:
            def _send(url, data):
                try:
                    import urllib.request
                    req = urllib.request.Request(
                        url, data=json.dumps(data).encode(),
                        headers={"Content-Type": "application/json"},
                    )
                    urllib.request.urlopen(req, timeout=10)
                except Exception:
                    pass
            threading.Thread(
                target=_send, args=(h["url"], {"event": event, **payload}),
                daemon=True,
            ).start()


# ---------------------------------------------------------------------------
# Snapshots
# ---------------------------------------------------------------------------

@app.post("/v1/agents/{agent_id}/snapshot", response_model=SnapshotResponse)
async def snapshot(agent_id: str, req: SnapshotRequest, auth=Depends(verify_auth)):
    runtime = _get_runtime(agent_id, auth)
    result = runtime.snapshot(req.label)
    return SnapshotResponse(
        label=result.label,
        keys_captured=result.keys_captured,
        latency_us=result.latency_us,
    )


@app.post("/v1/agents/{agent_id}/restore", response_model=RestoreResponse)
async def restore(agent_id: str, req: RestoreRequest, auth=Depends(verify_auth)):
    runtime = _get_runtime(agent_id, auth)
    result = runtime.restore(req.label)
    return RestoreResponse(
        label=result.label,
        keys_restored=result.keys_restored,
        recovery_time_us=result.recovery_time_us,
    )


# ---------------------------------------------------------------------------
# Shared Memory (with pagination)
# ---------------------------------------------------------------------------

@app.post("/v1/shared/{space}")
async def shared_write(space: str, req: SharedWriteRequest, auth=Depends(verify_auth)):
    runtime = _get_runtime(req.author_agent_id, auth)
    result = runtime.share(req.key, req.value, space=space)
    return {
        "node_id": result.node_id,
        "key": req.key,
        "space": space,
        "latency_us": result.latency_us,
        "success": result.success,
    }


def _get_tenant_backend(auth):
    """Get the tenant-isolated backend for the current request."""
    tenant_id = _get_tenant_id(auth)
    try:
        from synrix_runtime.api.tenant import TenantManager
        return TenantManager.get_instance().get_backend(tenant_id)
    except Exception:
        return None


def _get_agents_from_backend(backend) -> list:
    """Query agents directly from a tenant backend (for SSE and listings)."""
    if not backend:
        return []
    try:
        results = backend.query_prefix("runtime:agents:", limit=500)
        agents = {}
        for r in results:
            key = r.get("key", "")
            parts = key.split(":")
            if len(parts) >= 3:
                aid = parts[2]
                if aid == "system":
                    continue
                if aid not in agents:
                    agents[aid] = {"agent_id": aid}
                if len(parts) > 3:
                    data = r.get("data", {})
                    value = data.get("value", data)
                    if isinstance(value, dict) and "value" in value:
                        value = value["value"]
                    agents[aid][parts[3]] = value
        return [a for a in agents.values() if a.get("state") != "deregistered"]
    except Exception:
        return []


@app.get("/v1/shared/{space}/detail")
async def shared_space_detail(space: str, auth=Depends(verify_auth)):
    """Get space items + changelog (used by Shared Memory dashboard tab)."""
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.api.shared_memory import SharedMemoryBus
        bus = SharedMemoryBus(backend)
        items = bus.get_all(space)
        changelog = bus.get_changelog(space, limit=20)
        return {"space": space, "items": items, "changelog": changelog}
    except Exception:
        return {"space": space, "items": [], "changelog": []}


@app.get("/v1/shared/{space}/{key:path}")
async def shared_read(space: str, key: str, auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    if backend:
        result = backend.read(f"shared:{space}:{key}")
        if result:
            data = result.get("data", {})
            return {"key": key, "space": space, "value": data.get("value", data), "found": True}
    return {"key": key, "space": space, "value": None, "found": False}


@app.get("/v1/shared/{space}")
async def shared_list(
    space: str,
    offset: int = Query(default=0, ge=0),
    limit: int = Query(default=50, ge=1, le=500),
    auth=Depends(verify_auth),
):
    backend = _get_tenant_backend(auth)
    if backend:
        results = backend.query_prefix(f"shared:{space}:", limit=offset + limit + 200)
        items = []
        for item in results:
            key = item.get("key", "").replace(f"shared:{space}:", "")
            if ":changelog:" not in key:
                data = item.get("data", {})
                items.append({"key": key, "value": data.get("value", data)})
        page = items[offset:offset + limit]
        return {"space": space, "items": page, "count": len(page), "total": len(items), "offset": offset}
    return {"space": space, "items": [], "count": 0, "total": 0, "offset": offset}


@app.get("/v1/shared")
async def shared_spaces(auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    if backend:
        try:
            from synrix_runtime.api.shared_memory import SharedMemoryBus
            bus = SharedMemoryBus(backend)
            return {"spaces": bus.list_spaces()}
        except Exception:
            pass
    return {"spaces": []}


# ---------------------------------------------------------------------------
# Audit
# ---------------------------------------------------------------------------

@app.get("/v1/agents/{agent_id}/audit")
async def agent_audit(
    agent_id: str,
    limit: int = Query(default=50, ge=1, le=500),
    auth=Depends(verify_auth),
):
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.audit import AuditSystem
        audit = AuditSystem(backend)
        events = audit.replay(agent_id)
        return {"agent_id": agent_id, "events": events[:limit], "count": len(events)}
    except Exception:
        return {"agent_id": agent_id, "events": [], "count": 0}


@app.post("/v1/agents/{agent_id}/decision")
async def log_decision(agent_id: str, req: DecisionLogRequest, auth=Depends(verify_auth)):
    runtime = _get_runtime(agent_id, auth)
    runtime.log_decision(req.decision, req.reasoning, req.context)
    return {"agent_id": agent_id, "logged": True}


# ---------------------------------------------------------------------------
# Recovery
# ---------------------------------------------------------------------------

@app.post("/v1/agents/{agent_id}/recover")
async def recover_agent(agent_id: str, auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    if not backend:
        raise HTTPException(status_code=503, detail="Backend not available")
    try:
        from synrix_runtime.core.recovery import RecoveryOrchestrator
        orch = RecoveryOrchestrator(backend)
        result = orch.full_recovery(agent_id)
        from dataclasses import asdict
        return asdict(result)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Recovery failed: {e}")


@app.get("/v1/recovery/history")
async def recovery_history(auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.core.recovery import RecoveryOrchestrator
        orch = RecoveryOrchestrator(backend)
        return {
            "history": orch.get_all_recovery_history(),
            "stats": orch.get_recovery_stats(),
        }
    except Exception:
        return {"history": [], "stats": {}}


# ---------------------------------------------------------------------------
# Metrics
# ---------------------------------------------------------------------------

@app.get("/v1/agents/{agent_id}/metrics")
async def agent_metrics(agent_id: str, auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.metrics import MetricsCollector
        mc = MetricsCollector(backend)
        m = mc.get_agent_metrics(agent_id)
        return {
            "agent_id": agent_id,
            "total_operations": m.total_operations,
            "total_writes": m.total_writes,
            "total_reads": m.total_reads,
            "total_queries": m.total_queries,
            "avg_write_latency_us": m.avg_write_latency_us,
            "avg_read_latency_us": m.avg_read_latency_us,
            "crash_count": m.crash_count,
            "recovery_count": m.recovery_count,
            "performance_score": m.performance_score,
            "uptime_seconds": m.uptime_seconds,
            "error_rate": m.error_rate,
            "memory_node_count": m.memory_node_count,
        }
    except Exception:
        return {"agent_id": agent_id, "error": "Metrics not available"}


@app.get("/v1/metrics/system")
async def system_metrics(auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.metrics import MetricsCollector
        mc = MetricsCollector(backend)
        m = mc.get_system_metrics()
        return {
            "total_agents": m.total_agents,
            "active_agents": m.active_agents,
            "total_operations": m.total_operations,
            "system_uptime_seconds": m.system_uptime_seconds,
            "mean_recovery_time_us": m.mean_recovery_time_us,
            "total_crashes": m.total_crashes,
            "total_recoveries": m.total_recoveries,
        }
    except Exception:
        return {"error": "Metrics not available"}


# ---------------------------------------------------------------------------
# Webhook Ingest — any language, any framework
# ---------------------------------------------------------------------------

class IngestEvent(_PydanticBase):
    agent_id: str
    event_type: str = "memory"  # memory, conversation, task, custom
    key: Optional[str] = None
    value: Any = None
    tags: Optional[list] = None
    metadata: Optional[dict] = None
    timestamp: Optional[float] = None


class BatchIngestRequest(_PydanticBase):
    events: List[IngestEvent]


@app.post("/v1/ingest")
async def ingest_event(event: IngestEvent, auth=Depends(verify_auth)):
    """
    Universal ingest endpoint — send events from any agent, any language.

    Works with Node.js, Go, Rust, Ruby, Java, or any HTTP client.

    Example (curl):
        curl -X POST https://api.octopoda.dev/v1/ingest \\
          -H "Authorization: Bearer sk-octopoda-..." \\
          -H "Content-Type: application/json" \\
          -d '{"agent_id": "my_agent", "key": "user_name", "value": "Alice"}'

    Example (Node.js):
        await fetch("https://api.octopoda.dev/v1/ingest", {
            method: "POST",
            headers: { "Authorization": "Bearer sk-octopoda-...", "Content-Type": "application/json" },
            body: JSON.stringify({ agent_id: "my_agent", key: "user_name", value: "Alice" })
        });
    """
    _validate_agent_id(event.agent_id)
    if event.key:
        _validate_key(event.key)

    runtime = _get_runtime(event.agent_id, auth)

    key = event.key or f"ingest:{event.event_type}:{int((event.timestamp or time.time()) * 1000)}"
    value = event.value or ""

    # Store the event as a memory
    if isinstance(value, dict):
        if event.metadata:
            value["_metadata"] = event.metadata
        if event.event_type != "memory":
            value["_event_type"] = event.event_type
    elif event.metadata or event.event_type != "memory":
        value = {
            "value": value,
            "_event_type": event.event_type,
            "_metadata": event.metadata or {},
        }

    tags = event.tags or []
    if event.event_type not in tags:
        tags.append(event.event_type)

    result = runtime.remember(key, value, tags=tags)

    return {
        "success": result.success,
        "agent_id": event.agent_id,
        "key": key,
        "node_id": result.node_id,
        "latency_us": result.latency_us,
    }


@app.post("/v1/ingest/batch")
async def ingest_batch(req: BatchIngestRequest, auth=Depends(verify_auth)):
    """
    Batch ingest — send multiple events in one request.

    Example:
        curl -X POST https://api.octopoda.dev/v1/ingest/batch \\
          -H "Authorization: Bearer sk-octopoda-..." \\
          -d '{"events": [
            {"agent_id": "bot", "key": "name", "value": "Alice"},
            {"agent_id": "bot", "key": "role", "value": "Engineer"}
          ]}'
    """
    if len(req.events) > 100:
        raise HTTPException(status_code=400, detail="Maximum 100 events per batch")

    results = []
    for event in req.events:
        _validate_agent_id(event.agent_id)
        if event.key:
            _validate_key(event.key)

        runtime = _get_runtime(event.agent_id, auth)
        key = event.key or f"ingest:{event.event_type}:{int((event.timestamp or time.time()) * 1000)}"
        value = event.value or ""

        tags = event.tags or []
        if event.event_type not in tags:
            tags.append(event.event_type)

        result = runtime.remember(key, value, tags=tags)
        results.append({
            "agent_id": event.agent_id,
            "key": key,
            "success": result.success,
            "node_id": result.node_id,
        })

    return {
        "processed": len(results),
        "results": results,
    }


# ---------------------------------------------------------------------------
# Dashboard API: SSE Streaming (real-time updates for Loveable frontend)
# ---------------------------------------------------------------------------

def _sse_event_generator(backend):
    """Generate SSE events every second for the React dashboard."""
    import json as _json
    last_event_ts = time.time()

    while True:
        events = []
        try:
            # Agent update (with enriched metrics) — tenant-isolated
            try:
                agents = _get_agents_from_backend(backend)
                try:
                    from synrix_runtime.monitoring.metrics import MetricsCollector
                    collector = MetricsCollector(backend)
                    for a in agents:
                        agent_id = a.get("agent_id", "")
                        try:
                            m = collector.get_agent_metrics(agent_id)
                            a["performance_score"] = m.performance_score
                            a["total_operations"] = m.total_operations
                            a["avg_write_latency_us"] = m.avg_write_latency_us
                            a["avg_read_latency_us"] = m.avg_read_latency_us
                            a["memory_node_count"] = m.memory_node_count
                            a["crash_count"] = m.crash_count
                            a["uptime_seconds"] = m.uptime_seconds
                            a["error_rate"] = m.error_rate
                        except Exception:
                            pass
                        a["status"] = a.get("state", "unknown")
                except Exception:
                    for a in agents:
                        a["status"] = a.get("state", "unknown")
                events.append(("agent_update", {"agents": agents, "timestamp": time.time()}))
            except Exception:
                events.append(("agent_update", {"agents": [], "timestamp": time.time()}))

            # System metrics — tenant-isolated
            try:
                from synrix_runtime.monitoring.metrics import MetricsCollector
                collector = MetricsCollector(backend)
                system = collector.get_system_metrics()
                events.append(("metrics_update", {
                    "total_agents": system.total_agents,
                    "active_agents": system.active_agents,
                    "total_operations": system.total_operations,
                    "mean_recovery_time_us": system.mean_recovery_time_us,
                    "total_crashes": system.total_crashes,
                    "total_recoveries": system.total_recoveries,
                    "uptime_seconds": system.system_uptime_seconds,
                    "timestamp": time.time(),
                }))
            except Exception:
                pass

            # Anomalies
            try:
                from synrix_runtime.monitoring.anomaly import AnomalyDetector
                detector = AnomalyDetector(backend)
                anomalies = detector.get_all_anomalies()
                if anomalies:
                    events.append(("anomaly_alert", {"anomalies": anomalies[:5], "timestamp": time.time()}))
            except Exception:
                pass

            # Recent recoveries
            try:
                from synrix_runtime.core.recovery import RecoveryOrchestrator
                orchestrator = RecoveryOrchestrator(backend)
                recoveries = orchestrator.get_all_recovery_history()
                recent = [r for r in recoveries if isinstance(r, dict) and r.get("timestamp", 0) > last_event_ts - 10]
                if recent:
                    events.append(("recovery_event", {"recoveries": recent, "timestamp": time.time()}))
            except Exception:
                pass

            # Heartbeat
            events.append(("system_heartbeat", {"alive": True, "timestamp": time.time()}))

            last_event_ts = time.time()

            for event_type, data in events:
                yield f"event: {event_type}\ndata: {_json.dumps(data)}\n\n"

        except GeneratorExit:
            break
        except Exception as e:
            yield f"event: error\ndata: {_json.dumps({'error': str(e)})}\n\n"

        time.sleep(1)


@app.get("/v1/stream/events")
async def sse_stream(auth=Depends(verify_auth)):
    """
    Server-Sent Events stream for real-time dashboard updates.

    Events emitted every ~1 second:
    - agent_update: all agents with health scores, metrics, state
    - metrics_update: system-wide metrics
    - anomaly_alert: active anomalies (crash loops, latency spikes)
    - recovery_event: recent crash recoveries
    - system_heartbeat: keepalive ping

    Usage (JavaScript):
        const es = new EventSource('/v1/stream/events', {
            headers: { 'Authorization': 'Bearer sk-octopoda-...' }
        });
        es.addEventListener('agent_update', (e) => {
            const data = JSON.parse(e.data);
            updateAgentList(data.agents);
        });
    """
    backend = _get_tenant_backend(auth)
    return StreamingResponse(
        _sse_event_generator(backend),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",
        },
    )


# ---------------------------------------------------------------------------
# Dashboard API: Anomaly Detection
# ---------------------------------------------------------------------------

@app.get("/v1/anomalies")
async def get_anomalies(auth=Depends(verify_auth)):
    """Get all active anomalies across agents (crash loops, latency spikes, idle agents)."""
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.anomaly import AnomalyDetector
        detector = AnomalyDetector(backend)
        return {"anomalies": detector.get_all_anomalies()}
    except Exception:
        return {"anomalies": []}


# ---------------------------------------------------------------------------
# Dashboard API: Metrics Time-Series (for Chart.js graphs)
# ---------------------------------------------------------------------------

@app.get("/v1/agents/{agent_id}/metrics/timeseries")
async def agent_metrics_timeseries(
    agent_id: str,
    minutes: int = Query(default=60, ge=1, le=1440),
    type: str = Query(default="write", description="Metric type: write, read, query"),
    auth=Depends(verify_auth),
):
    """
    Get time-series metrics for Chart.js performance graphs.

    Returns data points over the last N minutes for the specified metric type.
    Used by the Performance tab to render latency/throughput charts.
    """
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.metrics import MetricsCollector
        collector = MetricsCollector(backend)
        series = collector.get_time_series(agent_id, type, minutes)
        return {"agent_id": agent_id, "type": type, "minutes": minutes, "series": series}
    except Exception:
        return {"agent_id": agent_id, "type": type, "minutes": minutes, "series": []}


@app.get("/v1/metrics/timeseries")
async def system_metrics_timeseries(
    agent_id: str = Query(default="", description="Agent ID (empty for system-wide)"),
    minutes: int = Query(default=60, ge=1, le=1440),
    type: str = Query(default="write", description="Metric type: write, read, query"),
    auth=Depends(verify_auth),
):
    """System-wide or per-agent time-series metrics."""
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.metrics import MetricsCollector
        collector = MetricsCollector(backend)
        series = collector.get_time_series(agent_id, type, minutes)
        return {"agent_id": agent_id, "type": type, "minutes": minutes, "series": series}
    except Exception:
        return {"agent_id": agent_id, "type": type, "minutes": minutes, "series": []}


# ---------------------------------------------------------------------------
# Dashboard API: Global Audit Timeline + Explain Decision
# ---------------------------------------------------------------------------

@app.get("/v1/audit/timeline")
async def audit_timeline(
    limit: int = Query(default=50, ge=1, le=500),
    auth=Depends(verify_auth),
):
    """
    Global audit timeline — all events across all agents, newest first.

    Used by the Audit tab to show a chronological view of everything
    happening across the system.
    """
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.audit import AuditSystem
        audit = AuditSystem(backend)
        return {"events": audit.get_global_timeline(limit), "limit": limit}
    except Exception:
        return {"events": [], "limit": limit}


@app.get("/v1/audit/explain/{agent_id}/{timestamp}")
async def audit_explain(agent_id: str, timestamp: float, auth=Depends(verify_auth)):
    """
    Explain a decision — show what the agent knew at that exact moment.

    Returns the full causal chain: what was queried, what was decided,
    what was written, and the memory snapshot at decision time.
    """
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.audit import AuditSystem
        audit = AuditSystem(backend)
        return audit.explain_decision(agent_id, timestamp)
    except Exception:
        return {"agent_id": agent_id, "timestamp": timestamp, "explanation": None}


@app.get("/v1/agents/{agent_id}/audit/replay")
async def agent_audit_replay(
    agent_id: str,
    from_ts: Optional[float] = Query(default=None, alias="from", description="Start timestamp"),
    to_ts: Optional[float] = Query(default=None, alias="to", description="End timestamp"),
    auth=Depends(verify_auth),
):
    """Replay agent audit events within a time range."""
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.audit import AuditSystem
        audit = AuditSystem(backend)
        events = audit.replay(agent_id, from_ts=from_ts, to_ts=to_ts)
        return {"agent_id": agent_id, "events": events, "count": len(events)}
    except Exception:
        return {"agent_id": agent_id, "events": [], "count": 0}


# ---------------------------------------------------------------------------
# Dashboard API: Performance Breakdown
# ---------------------------------------------------------------------------

@app.get("/v1/agents/{agent_id}/performance")
async def agent_performance(agent_id: str, auth=Depends(verify_auth)):
    """
    Detailed performance breakdown for an agent.

    Returns per-operation-type latency stats, used by the Performance tab
    for detailed charts and analysis.
    """
    backend = _get_tenant_backend(auth)
    try:
        from synrix_runtime.monitoring.metrics import MetricsCollector
        collector = MetricsCollector(backend)
        m = collector.get_agent_metrics(agent_id)
        breakdown = collector.get_performance_breakdown(agent_id)
        return {
            "agent_id": agent_id,
            "metrics": {
                "total_operations": m.total_operations,
                "total_writes": m.total_writes,
                "total_reads": m.total_reads,
                "total_queries": m.total_queries,
                "avg_write_latency_us": m.avg_write_latency_us,
                "avg_read_latency_us": m.avg_read_latency_us,
                "avg_query_latency_us": m.avg_query_latency_us,
                "crash_count": m.crash_count,
                "recovery_count": m.recovery_count,
                "performance_score": m.performance_score,
                "uptime_seconds": m.uptime_seconds,
                "error_rate": m.error_rate,
                "memory_node_count": m.memory_node_count,
                "operations_per_minute": m.operations_per_minute,
            },
            "breakdown": breakdown,
        }
    except Exception:
        return {"agent_id": agent_id, "metrics": {}, "breakdown": {}}


# ---------------------------------------------------------------------------
# Raw operations
# ---------------------------------------------------------------------------

@app.post("/v1/raw/write")
async def raw_write(req: RawWriteRequest, auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    if backend:
        start = time.perf_counter_ns()
        node_id = backend.write(req.key, req.value, req.metadata)
        latency_us = (time.perf_counter_ns() - start) / 1000
        return {"node_id": node_id, "key": req.key, "latency_us": round(latency_us, 1)}
    raise HTTPException(status_code=503, detail="Backend not available")


@app.get("/v1/raw/read/{key:path}")
async def raw_read(key: str, auth=Depends(verify_auth)):
    backend = _get_tenant_backend(auth)
    if backend:
        start = time.perf_counter_ns()
        result = backend.read(key)
        latency_us = (time.perf_counter_ns() - start) / 1000
        if result:
            return {"key": key, "data": result.get("data", {}), "latency_us": round(latency_us, 1), "found": True}
        return {"key": key, "data": None, "latency_us": round(latency_us, 1), "found": False}
    raise HTTPException(status_code=503, detail="Backend not available")


@app.get("/v1/raw/query")
async def raw_query(
    prefix: str = "",
    limit: int = Query(default=100, ge=1, le=1000),
    auth=Depends(verify_auth),
):
    backend = _get_tenant_backend(auth)
    if backend:
        start = time.perf_counter_ns()
        results = backend.query_prefix(prefix, limit=limit)
        latency_us = (time.perf_counter_ns() - start) / 1000
        return {"items": results, "count": len(results), "latency_us": round(latency_us, 1)}
    raise HTTPException(status_code=503, detail="Backend not available")


# ---------------------------------------------------------------------------
# License Info
# ---------------------------------------------------------------------------

@app.get("/v1/license")
async def license_info(auth=Depends(verify_auth)):
    """Check current license tier, agent count, and limits."""
    backend = _get_tenant_backend(auth)
    agents_list = _get_agents_from_backend(backend)
    tenant_id = _get_tenant_id(auth)
    plan = auth.get("plan", "free") if auth else "free"
    max_agents = auth.get("max_agents", 100) if auth else 100
    max_mem = auth.get("max_memories_per_agent", 100000) if auth else 100000
    return {
        "tier": plan,
        "max_agents": max_agents,
        "max_memories_per_agent": max_mem,
        "current_agents": len(agents_list),
        "agents": [
            {"agent_id": a.get("agent_id", ""), "state": a.get("state", "unknown")}
            for a in agents_list
        ],
    }


# ---------------------------------------------------------------------------
# API Key Management (admin only)
# ---------------------------------------------------------------------------

@app.post("/v1/admin/keys")
async def create_api_key(auth=Depends(verify_auth)):
    """Create a new API key for the authenticated tenant (own account only)."""
    tenant_id = _get_tenant_id(auth)
    if _auth_manager:
        raw_key = _auth_manager.create_key(tenant_id=tenant_id)
        return {"api_key": raw_key, "tenant_id": tenant_id, "warning": "Save this key - it won't be shown again"}
    raise HTTPException(status_code=503, detail="Auth not configured")


# ---------------------------------------------------------------------------
# Per-Tenant LLM Settings
# ---------------------------------------------------------------------------
# In-memory tenant settings (persisted to tenant DB on write)
_tenant_settings: dict = {}  # tenant_id -> {llm_provider, openai_api_key, ...}

# Platform free tier: 100 LLM extractions per tenant before downgrade to embedding-only
_PLATFORM_FREE_LIMIT = int(os.environ.get("OCTOPODA_PLATFORM_FREE_LIMIT", "100"))


def _get_tenant_settings(tenant_id: str) -> dict:
    """Get tenant LLM settings from cache or DB.

    New tenants default to 'platform' provider (free tier with 100 LLM extractions).
    """
    if tenant_id in _tenant_settings:
        return _tenant_settings[tenant_id]
    # Try to load from tenant DB
    try:
        from synrix_runtime.api.tenant import TenantManager
        tm = TenantManager.get_instance()
        backend = tm.get_backend(tenant_id)
        result = backend.read("__tenant_settings__")
        if result and "data" in result:
            settings = result["data"].get("value", {})
            if isinstance(settings, dict):
                _tenant_settings[tenant_id] = settings
                return settings
    except Exception:
        pass
    # New tenant — default to platform free tier
    defaults = {"llm_provider": "platform", "platform_extractions_used": 0}
    _tenant_settings[tenant_id] = defaults
    return defaults


def _increment_platform_usage(tenant_id: str):
    """Increment platform free tier extraction counter for a tenant."""
    settings = _get_tenant_settings(tenant_id)
    if settings.get("llm_provider") == "platform":
        settings["platform_extractions_used"] = settings.get("platform_extractions_used", 0) + 1
        _save_tenant_settings(tenant_id, settings)


def _save_tenant_settings(tenant_id: str, settings: dict):
    """Persist tenant settings to DB."""
    _tenant_settings[tenant_id] = settings
    try:
        from synrix_runtime.api.tenant import TenantManager
        tm = TenantManager.get_instance()
        backend = tm.get_backend(tenant_id)
        backend.write("__tenant_settings__", {"value": settings}, metadata={"type": "settings"})
    except Exception:
        pass


@app.get("/v1/settings")
async def get_settings(auth=Depends(verify_auth)):
    """Get current LLM and feature settings for your account."""
    tenant_id = _get_tenant_id(auth)
    settings = _get_tenant_settings(tenant_id)
    # Never return full API keys — mask them
    safe = dict(settings)
    for key in ("openai_api_key", "anthropic_api_key"):
        if key in safe and safe[key]:
            safe[key] = safe[key][:8] + "..." + safe[key][-4:]
    provider = safe.get("llm_provider", "platform")
    result = {
        "llm_provider": provider,
        "openai_api_key": safe.get("openai_api_key", ""),
        "openai_model": safe.get("openai_model", "gpt-4o-mini"),
        "openai_base_url": safe.get("openai_base_url", "https://api.openai.com/v1"),
        "anthropic_api_key": safe.get("anthropic_api_key", ""),
        "anthropic_model": safe.get("anthropic_model", "claude-haiku-4-5-20251001"),
        "ollama_model": safe.get("ollama_model", "llama3.2"),
    }
    # Show platform free tier usage if applicable
    if provider == "platform":
        used = settings.get("platform_extractions_used", 0)
        result["platform_extractions_used"] = used
        result["platform_extractions_limit"] = _PLATFORM_FREE_LIMIT
        result["platform_extractions_remaining"] = max(0, _PLATFORM_FREE_LIMIT - used)
    return result


@app.put("/v1/settings")
async def update_settings(req: dict, auth=Depends(verify_auth)):
    """Update LLM provider and API keys for your account.

    Supported fields:
        llm_provider: "ollama" | "openai" | "anthropic" | "none"
        openai_api_key: your OpenAI API key (or any OpenAI-compatible provider key)
        openai_model: model name (default: gpt-4o-mini)
        openai_base_url: API base URL (default: https://api.openai.com/v1)
            — Use this for Groq, Together, Mistral, or any OpenAI-compatible API
        anthropic_api_key: your Anthropic API key
        anthropic_model: model name (default: claude-haiku-4-5-20251001)
        ollama_model: Ollama model name (default: llama3.2)
    """
    tenant_id = _get_tenant_id(auth)
    settings = _get_tenant_settings(tenant_id)

    allowed_fields = {
        "llm_provider", "openai_api_key", "openai_model", "openai_base_url",
        "anthropic_api_key", "anthropic_model", "ollama_model",
    }
    allowed_providers = {"platform", "ollama", "openai", "anthropic", "none"}

    for key, value in req.items():
        if key in allowed_fields:
            if key == "llm_provider" and value not in allowed_providers:
                raise HTTPException(
                    status_code=422,
                    detail=f"Invalid provider '{value}'. Must be one of: {', '.join(allowed_providers)}"
                )
            settings[key] = value

    _save_tenant_settings(tenant_id, settings)

    # Evict cached runtimes for this tenant so they pick up the new LLM config
    keys_to_evict = [k for k in _agent_runtimes if k.startswith(f"{tenant_id}:")]
    for k in keys_to_evict:
        _agent_runtimes.pop(k, None)

    return {"updated": True, "llm_provider": settings.get("llm_provider", "platform")}
