#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
PULSE Neural API — Universal gateway to the Byzantine-resilient AI Brain.

App setup, CORS, middleware, and router mounting.
Brain endpoints: pulse_api_brain.py
Agent endpoints: pulse_api_agents.py
System endpoints: pulse_api_system.py
"""

import sys
import asyncio
import time
import re
import threading
from pathlib import Path

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent

# When run directly, ensure pulse package is importable
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))
from collections import defaultdict
from typing import Dict

from fastapi import FastAPI, HTTPException, BackgroundTasks, Header, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field

import hmac
import ipaddress
import os

from pulse.core.pulse_crypto import PulseCrypto
from pulse.daemons.compliance_validator import HARM_PATTERNS, PII_PATTERNS

try:
    from pulse.api.pulse_coordinator import BrainCoordinator
except ImportError:
    pass

# --- Router imports ---
from pulse.api.pulse_api_brain import router as brain_router
from pulse.api.pulse_api_agents import router as agents_router
from pulse.api.pulse_api_system import router as system_router
from pulse.api.pulse_api_license import router as license_router

# --- Pydantic Models ---
class EvidencePayload(BaseModel):
    evidence: str = Field(..., examples=["User prefers Python for automation."])
    domain: str = Field(..., examples=["general"])
    agent_id: str = Field(..., examples=["external_agent_001"])
    type: str = "observation"
    visibility: str = "public"
    pattern: str = "uncategorized"

class BrainHealthResponse(BaseModel):
    status: str
    synaptic_density: int
    queue_depth: int
    uptime_seconds: float

class BrainStatsResponse(BaseModel):
    status: str
    total_synapses: int
    signed_synapses: int
    integrity_ratio: float
    memory_velocity_ops_sec: float
    active_agents: int
    queue_depth: int
    uptime_seconds: float

# --- Shared Resources ---
COORDINATOR = BrainCoordinator()
CRYPTO = PulseCrypto()
API_START_TIME = None

# --- Rate Limiter ---
class RateLimiter:
    def __init__(self, max_requests: int = 10, window_seconds: int = 60):
        self.max_requests = max_requests
        self.window = window_seconds
        self._hits: Dict[str, list] = defaultdict(list)
        self._lock = threading.Lock()

    def is_allowed(self, key: str) -> bool:
        now = time.time()
        cutoff = now - self.window
        with self._lock:
            self._hits[key] = [t for t in self._hits[key] if t > cutoff]
            if len(self._hits[key]) >= self.max_requests:
                return False
            self._hits[key].append(now)
            return True

RATE_LIMITER = RateLimiter(max_requests=10, window_seconds=60)
SAFE_DOMAIN_RE = re.compile(r'^[a-z0-9_]{1,64}$')

# --- Compliance Gate ---
def check_evidence_compliance(content: str) -> list:
    violations = []
    for harm_type, pattern in HARM_PATTERNS.items():
        if pattern.search(content):
            violations.append(f"TITAN_VIOLATION:{harm_type}")
    for pii_type, pattern in PII_PATTERNS.items():
        if pattern.search(content):
            violations.append(f"PII_DETECTED:{pii_type}")
    return violations

# --- Auth Middleware ---
PULSE_API_TOKEN = os.environ.get("PULSE_API_TOKEN", "")


async def _auth_middleware(request: Request, call_next):
    """Check X-Pulse-Token on all /v1/ endpoints. Localhost is trusted."""
    path = request.url.path
    if PULSE_API_TOKEN and path.startswith("/v1/"):
        # Localhost is trusted — no auth needed
        client_ip = request.client.host if request.client else ""
        try:
            is_local = ipaddress.ip_address(client_ip).is_loopback
        except ValueError:
            is_local = False

        if not is_local:
            token = request.headers.get("X-Pulse-Token", "")
            if not hmac.compare_digest(token, PULSE_API_TOKEN):
                return JSONResponse(status_code=401,
                    content={"detail": "Remote access requires X-Pulse-Token header"})
    return await call_next(request)


# --- App ---
app = FastAPI(
    title="PULSE Neural API",
    description="The universal gateway to the Byzantine-resilient AI Brain.",
    version="7.0.0"
)

# Auth middleware (before CORS so preflight still works)
from starlette.middleware.base import BaseHTTPMiddleware
app.add_middleware(BaseHTTPMiddleware, dispatch=_auth_middleware)

# --- CORS ---
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:3000", "http://127.0.0.1:3000",
        "http://localhost:5173", "http://127.0.0.1:5173",
        "http://localhost:5174", "http://127.0.0.1:5174",
    ],
    allow_credentials=True,
    allow_methods=["POST", "GET"],
    allow_headers=["X-Pulse-Signature", "X-Pulse-Token", "Content-Type", "Authorization"],
)

# --- Mount routers ---
app.include_router(brain_router)
app.include_router(agents_router)
app.include_router(system_router)
app.include_router(license_router)

# --- Healthz (no auth required) ---
@app.get("/healthz")
async def healthz():
    """Health check endpoint — no auth, no rate limit."""
    return {"status": "ok"}

# --- Core endpoints (evidence, health, stats, consolidate, tasks) ---

@app.on_event("startup")
async def startup_event():
    global API_START_TIME
    API_START_TIME = time.time()
    asyncio.create_task(COORDINATOR.worker())
    print("[API] Neural Gateway Active. Coordinator worker started.")

@app.post("/v1/evidence", status_code=202)
async def push_evidence(req: EvidencePayload, request: Request,
                        x_pulse_signature: str = Header(..., alias="X-Pulse-Signature")):
    client_ip = request.client.host if request.client else "unknown"
    if not RATE_LIMITER.is_allowed(client_ip):
        raise HTTPException(status_code=429, detail="Rate limit exceeded. Max 10 requests per minute.")
    body = await request.body()
    if not CRYPTO.verify_string(body.decode(), x_pulse_signature, req.agent_id):
        raise HTTPException(status_code=401, detail="Invalid cryptographic signature.")
    if not SAFE_DOMAIN_RE.match(req.domain):
        raise HTTPException(status_code=400, detail="Invalid domain format.")
    violations = check_evidence_compliance(req.evidence)
    if violations:
        raise HTTPException(status_code=422, detail=f"Evidence rejected: {', '.join(violations)}")

    file_name = f"{req.domain}_captured.json" if req.domain != "session" else "current_session.json"
    base_path = (ROOT_DIR / "Hippocampus" / "Evidence" / "Sessions").resolve()
    target_path = base_path / file_name
    if not str(target_path.resolve()).startswith(str(base_path)):
        raise HTTPException(status_code=403, detail="Path traversal detected.")

    payload = req.__dict__
    payload["timestamp"] = asyncio.get_event_loop().time()
    payload["compliance_cleared"] = True
    await COORDINATOR.enqueue_evidence(target_path, [payload], req.agent_id)
    return {"status": "accepted", "id": req.agent_id, "domain": req.domain}

@app.get("/v1/brain/health", response_model=BrainHealthResponse)
async def get_health(request: Request):
    client_ip = request.client.host if request.client else "unknown"
    if not RATE_LIMITER.is_allowed(client_ip):
        raise HTTPException(status_code=429, detail="Rate limit exceeded.")
    uptime = time.time() - API_START_TIME if API_START_TIME else 0
    return BrainHealthResponse(
        status="online", synaptic_density=100,
        queue_depth=COORDINATOR.queue.qsize(), uptime_seconds=uptime,
    )

@app.get("/v1/brain/stats", response_model=BrainStatsResponse)
async def get_stats(request: Request):
    client_ip = request.client.host if request.client else "unknown"
    if not RATE_LIMITER.is_allowed(client_ip):
        raise HTTPException(status_code=429, detail="Rate limit exceeded.")
    try:
        from pulse.core.brain_utils import get_brain_density
        density_data = get_brain_density()
    except Exception:
        density_data = {"total_items": 0, "signed_items": 0, "integrity_ratio": 0.0}
    uptime = time.time() - API_START_TIME if API_START_TIME else 0
    return BrainStatsResponse(
        status="active",
        total_synapses=density_data.get("total_items", 0),
        signed_synapses=density_data.get("signed_items", 0),
        integrity_ratio=density_data.get("integrity_ratio", 0.0),
        memory_velocity_ops_sec=0.5,
        active_agents=3,
        queue_depth=COORDINATOR.queue.qsize(),
        uptime_seconds=uptime,
    )

@app.post("/v1/brain/consolidate", status_code=200)
async def trigger_consolidation(request: Request, background_tasks: BackgroundTasks):
    client_ip = request.client.host if request.client else "unknown"
    if not RATE_LIMITER.is_allowed(client_ip):
        raise HTTPException(status_code=429, detail="Rate limit exceeded.")
    def _run_dedup():
        try:
            from pulse.daemons.synthesis.pulse_dedup import run_full_dedup
            run_full_dedup(dry_run=False, verbose=False)
        except Exception as e:
            print(f"[API] Consolidation error: {e}")
    background_tasks.add_task(_run_dedup)
    return {"status": "consolidation_started"}

@app.get("/v1/brain/tasks")
async def get_tasks(request: Request):
    client_ip = request.client.host if request.client else "unknown"
    if not RATE_LIMITER.is_allowed(client_ip):
        raise HTTPException(status_code=429, detail="Rate limit exceeded.")
    try:
        from pulse.core.brain_utils import load_json_dict, TASK_BOARD_FILE
        board = load_json_dict(TASK_BOARD_FILE)
        return {"status": "ok", "dispatches": board.get("dispatches", [])}
    except Exception as e:
        return {"status": "error", "detail": str(e)}


if __name__ == "__main__":
    import uvicorn
    host = os.environ.get("PULSE_API_HOST", "127.0.0.1")
    port = int(os.environ.get("PULSE_API_PORT", "8000"))
    uvicorn.run(app, host=host, port=port)
