#!/usr/bin/env python3
"""
Neuron Council Service — LLM anti-confabulation layer.

Fires 3 parallel Ollama calls and aggregates votes to produce a
confidence score + tags for any claim before it enters memory.

Usage:
    python3 council_service.py [--port 7771]
"""

import argparse
import asyncio
import time
import uuid
from typing import Optional

import httpx
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

OLLAMA_BASE = "http://localhost:11434/api/generate"
COUNCIL_MODELS = ["neuron:latest", "dolphin3:8b", "neuron-ft:latest"]
MODEL_TIMEOUT = 45.0  # seconds per model (models may need to load from cold)

SYSTEM_PROMPT_TEMPLATE = """\
You are a fact-checker. You will be given a claim.
Your job: assess if it is accurate, internally consistent, and grounded in reality.
Respond with EXACTLY ONE WORD:
- "plausible" if the claim seems accurate and well-grounded
- "uncertain" if you cannot determine accuracy or the claim is ambiguous
- "confabulation" if the claim appears to contain invented facts or clear errors

Claim: {claim}
Context: {context}

Your verdict (one word only):"""

VALID_VERDICTS = {"plausible", "uncertain", "confabulation"}

# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------

app = FastAPI(
    title="Neuron Council Service",
    description="LLM-council anti-confabulation layer for Neuron soul",
    version="1.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)


# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------

class VerifyRequest(BaseModel):
    claim: str
    context: Optional[str] = ""


class VerifyResponse(BaseModel):
    id: str
    claim: str
    confidence: float
    council_votes: list[str]
    summary: str
    tags: list[str]
    latency_ms: int


# ---------------------------------------------------------------------------
# Core logic
# ---------------------------------------------------------------------------

async def query_model(client: httpx.AsyncClient, model: str, prompt: str) -> str:
    """
    Query a single Ollama model. Returns "plausible", "uncertain", or "confabulation".
    Returns "uncertain" on timeout. Raises httpx.ConnectError on connection failure.
    """
    payload = {
        "model": model,
        "prompt": prompt,
        "stream": False,
    }
    try:
        resp = await client.post(OLLAMA_BASE, json=payload, timeout=MODEL_TIMEOUT)
        resp.raise_for_status()
        data = resp.json()
        raw = data.get("response", "").strip().lower().split()[0] if data.get("response", "").strip() else "uncertain"
        # Normalise to one of the three valid verdicts
        if raw not in VALID_VERDICTS:
            return "uncertain"
        return raw
    except httpx.TimeoutException:
        return "uncertain"


async def run_council(claim: str, context: str) -> VerifyResponse:
    start = time.monotonic()
    prompt = SYSTEM_PROMPT_TEMPLATE.format(claim=claim, context=context)

    # Quick connectivity check — one tiny HEAD request to Ollama
    try:
        async with httpx.AsyncClient() as probe:
            await probe.get("http://localhost:11434/", timeout=2.0)
    except (httpx.ConnectError, httpx.TimeoutException):
        latency_ms = int((time.monotonic() - start) * 1000)
        return VerifyResponse(
            id=str(uuid.uuid4()),
            claim=claim,
            confidence=0.5,
            council_votes=[],
            summary="Ollama is unavailable; council could not convene.",
            tags=["council-unavailable"],
            latency_ms=latency_ms,
        )

    # Fire all 3 model calls in parallel
    async with httpx.AsyncClient() as client:
        tasks = [query_model(client, m, prompt) for m in COUNCIL_MODELS]
        votes: list[str] = await asyncio.gather(*tasks)

    plausible_count = votes.count("plausible")
    latency_ms = int((time.monotonic() - start) * 1000)

    # Voting rules
    if plausible_count == 3:
        confidence = 0.85
        tags = ["verified"]
        summary = "3/3 council members agree this is plausible."
    elif plausible_count == 2:
        confidence = 0.65
        tags = ["council-split"]
        summary = "2/3 council members agree this is plausible."
    elif plausible_count == 1:
        confidence = 0.30
        tags = ["unverified", "council-flagged"]
        summary = "1/3 council members found this plausible."
    else:
        confidence = 0.30
        tags = ["unverified", "council-flagged"]
        summary = "0/3 council members found this plausible."

    return VerifyResponse(
        id=str(uuid.uuid4()),
        claim=claim,
        confidence=confidence,
        council_votes=votes,
        summary=summary,
        tags=tags,
        latency_ms=latency_ms,
    )


# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------

@app.post("/api/neuron/council/verify", response_model=VerifyResponse)
async def verify(req: VerifyRequest):
    return await run_council(req.claim, req.context or "")


@app.get("/healthz")
async def health():
    return {"status": "ok", "service": "council"}


# ---------------------------------------------------------------------------
# Startup warm-up: pre-load all council models so first real call is fast
# ---------------------------------------------------------------------------

@app.on_event("startup")
async def warmup_models():
    """
    Send a trivial prompt to each council model at startup.
    This forces Ollama to load the models into GPU memory so the first
    real council call does not pay the cold-load latency penalty.
    """
    print("[council] Warming up council models...")
    warmup_prompt = "Reply with one word: ready"
    async with httpx.AsyncClient() as client:
        tasks = [
            client.post(
                OLLAMA_BASE,
                json={"model": m, "prompt": warmup_prompt, "stream": False},
                timeout=60.0,
            )
            for m in COUNCIL_MODELS
        ]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        for model, result in zip(COUNCIL_MODELS, results):
            if isinstance(result, Exception):
                print(f"[council] warm-up failed for {model}: {result}")
            else:
                print(f"[council] {model} warm and ready")
    print("[council] All models warmed up.")


# ---------------------------------------------------------------------------
# Entrypoint
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Neuron Council Service")
    parser.add_argument("--port", type=int, default=7771, help="Port to listen on")
    parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
    args = parser.parse_args()

    print(f"[council] Starting on {args.host}:{args.port}")
    uvicorn.run(app, host=args.host, port=args.port, log_level="info")
