#!/usr/bin/env python3
"""
CyberBrain26 SAFE-OI / FULL-OI API – LiteLLM Phase 1.

Běžná konverzace jde přímo přes LiteLLM, nikoli přes Open Interpreter.
SAFE i FULL generují návrhy; provádění kódu je v této fázi záměrně zakázané.
"""

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

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

PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from auto_memory import remember_if_relevant
from memory_core import remember, retrieve_active_context

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("/tmp/cb26-safeoi-api.log"),
        logging.StreamHandler(),
    ],
)
logger = logging.getLogger("safe_oi_api")

PROXY_URL = os.getenv("CB26_PROXY_URL", "http://127.0.0.1:4000").rstrip("/")
PROXY_KEY = os.getenv(
    "CB26_PROXY_KEY",
    os.getenv("LITELLM_MASTER_KEY", "sk-cb26-local"),
)
DEFAULT_MODEL = os.getenv("CB26_DEFAULT_MODEL", "groq-gpt-oss-120b")
HOST = "127.0.0.1"
PORT = 7682
MAX_TOKENS = 1024
HISTORY_LIMIT = 12
MEMORY_LIMIT = 8
REQUEST_TIMEOUT = 90.0

MODEL_ALIASES = {
    "default": "groq-gpt-oss-120b",
    "fast": "gemini-flash-lite",
    "gemini": "gemini-flash",
    "code": "mistral-codestral",
    "long": "openrouter-nemotron-3-ultra-550b-free",
    "vision": "openrouter-gemma-4-31b-free",
    "code-free": "openrouter-north-mini-code-free",
}

app = FastAPI(title="CyberBrain26 SAFE-OI API", version="0.6-phase1")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:8080", "http://127.0.0.1:8080"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

sessions: Dict[str, dict] = {}
pending_approvals: Dict[str, dict] = {}
executed_approvals: Dict[str, dict] = {}


class ChatRequest(BaseModel):
    thread_id: Optional[str] = None
    message: str
    model: str = DEFAULT_MODEL
    mode: str = "SAFE"
    depth: str = "normal"
    confirm_full: bool = False


class ApproveRequest(BaseModel):
    action: str = "approve"


def resolve_model(model: str) -> str:
    return MODEL_ALIASES.get(model, model)


def get_or_create_session(thread_id: Optional[str], model: str, mode: str) -> str:
    if not thread_id or thread_id not in sessions:
        thread_id = thread_id or str(uuid.uuid4())
        sessions[thread_id] = {
            "history": [],
            "mode": mode.upper(),
            "model": model,
            "pending_approval": None,
        }
        logger.info("Nová session: %s | mode=%s", thread_id, mode)
    return thread_id


def mode_instruction(mode: str) -> str:
    common = (
        "Jsi CyberBrain26, užitečný AI asistent. "
        "Tvoje jméno je CyberBrain26. "
        "Vždy odpovídej česky, pokud uživatel výslovně nepožádá o jiný jazyk. "
        "Na konkrétní otázky odpovídej přímo, věcně a bez obecné uvítací fráze. "
        "Pokud uživatel požaduje jednu krátkou větu, napiš pouze tuto větu. "
        "Nepředstavuj se jako Open Interpreter a netvrď, že jsi něco provedl, "
        "pokud to nebylo skutečně vykonáno."
    )
    if mode == "SAFE":
        return (
            common
            + " Jsi v SAFE režimu. Kód ani příkazy nikdy nespouštěj. "
            "U požadavků vyžadujících práci se systémem nejprve popiš plán "
            "a případný kód vlož do jednoho označeného code blocku. "
            "Systém zobrazí samostatné potvrzení."
        )
    if mode == "FULL":
        return (
            common
            + " Jsi ve FULL režimu. V této bezpečné první fázi také nic "
            "nespouštěj. Pro práci se systémem popiš plán a případný kód "
            "vlož do jednoho označeného code blocku. "
            "Provedení bude dostupné až po samostatném bezpečném runneru."
        )
    return common


def extract_code_from_response(text: str) -> tuple[Optional[str], Optional[str], list]:
    code_blocks = re.findall(r"```(\w+)?\n(.*?)```", text, re.DOTALL)
    if not code_blocks:
        return None, None, []
    language, code = code_blocks[-1]
    return language or "text", code.strip(), code_blocks


def classify_action(code: str, language: str) -> tuple[str, list[str]]:
    lang = (language or "").strip().lower()
    if lang in {"bash", "sh", "shell", "zsh"}:
        first = code.strip().split()[0] if code.strip() else ""
        readonly = {"pwd", "ls", "dir", "find", "cat", "head", "tail", "grep", "echo", "stat", "wc"}
        if first in readonly:
            return "read", ["pouze čtení"]
        return "write", ["shell příkaz"]
    if lang in {"python", "py"}:
        risky = ("open(", "write(", "Path(", "remove(", "rmdir", "unlink", "rmtree", "shutil",
                 "subprocess", "os.system", "os.popen", "exec(", "eval(")
        if any(token in code for token in risky):
            return "write", ["Python kód se systémovým nebo souborovým efektem"]
        return "read", ["Python kód bez zjevného zápisu"]
    return "unknown", ["neznámý jazyk nebo typ kódu"]


def build_messages(session: dict, normalized_message: str, mode: str, depth: str) -> List[dict]:
    messages: List[dict] = [{"role": "system", "content": mode_instruction(mode)}]

    try:
        records = retrieve_active_context(query=normalized_message, limit=MEMORY_LIMIT)
        if records:
            memory_text = "\n".join(f"- {item['content']}" for item in records)
            messages.append({
                "role": "system",
                "content": (
                    "Relevantní dlouhodobá paměť CyberBrain26. Použij ji jako kontext. "
                    "Netvrď, že je něco uložené, pokud to není v tomto seznamu:\n"
                    + memory_text
                ),
            })
    except Exception as exc:
        logger.warning("Nelze načíst paměť: %s", exc)

    for item in session["history"][-HISTORY_LIMIT:]:
        role = item.get("role")
        content = item.get("content")
        if role in {"user", "assistant"} and isinstance(content, str) and content:
            messages.append({"role": role, "content": content})

    prefix = "[Přemýšlej důkladně] " if depth == "deep" else ""
    messages.append({"role": "user", "content": prefix + normalized_message})
    return messages


def call_proxy(model: str, messages: List[dict]) -> str:
    try:
        with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
            response = client.post(
                f"{PROXY_URL}/v1/chat/completions",
                headers={
                    "Authorization": f"Bearer {PROXY_KEY}",
                    "Content-Type": "application/json",
                },
                json={
                    "model": model,
                    "messages": messages,
                    "max_tokens": MAX_TOKENS,
                },
            )
    except httpx.RequestError as exc:
        raise HTTPException(status_code=502, detail=f"Nelze se spojit s LiteLLM proxy: {exc}") from exc

    if response.status_code != 200:
        raise HTTPException(
            status_code=502,
            detail=f"Proxy HTTP {response.status_code}: {response.text[:1000]}",
        )

    try:
        data = response.json()
        return data["choices"][0]["message"]["content"]
    except (ValueError, KeyError, IndexError, TypeError) as exc:
        raise HTTPException(status_code=502, detail=f"Neočekávaná odpověď proxy: {exc}") from exc


@app.get("/health")
def health():
    return {
        "status": "ok",
        "service": "safe_oi_api",
        "version": "0.6-phase1",
        "sessions": len(sessions),
        "proxy": PROXY_URL,
        "execution": "disabled-phase1",
    }


@app.post("/chat")
def chat(req: ChatRequest):
    mode = req.mode.upper()
    if mode not in {"SAFE", "FULL"}:
        raise HTTPException(status_code=400, detail="mode musí být SAFE nebo FULL")
    if mode == "FULL" and not req.confirm_full:
        raise HTTPException(status_code=403, detail="FULL-OI vyžaduje confirm_full=true")

    normalized_message = req.message.strip()
    if not normalized_message:
        raise HTTPException(status_code=400, detail="Zadej zprávu.")

    resolved_model = resolve_model(req.model)
    thread_id = get_or_create_session(req.thread_id, resolved_model, mode)
    session = sessions[thread_id]
    session["mode"] = mode
    session["model"] = resolved_model

    memory_saved = None
    if normalized_message.casefold().startswith("zapamatuj si"):
        memory_content = normalized_message[len("zapamatuj si"):].lstrip(" ,:-")
        if not memory_content:
            raise HTTPException(
                status_code=400,
                detail="Za příkazem „zapamatuj si“ musí následovat text k uložení.",
            )
        try:
            record = remember(
                content=memory_content,
                kind="fact",
                source=f"{mode.lower()}-oi-explicit",
                confidence="user_confirmed",
            )
            memory_saved = {
                "id": record["id"],
                "content": record["content"],
                "kind": record["metadata"]["kind"],
            }
        except Exception as exc:
            raise HTTPException(status_code=500, detail=f"Nepodařilo se uložit paměť: {exc}") from exc

    messages = build_messages(session, normalized_message, mode, req.depth)
    logger.info("[%s] %s | %s | %s", thread_id, mode, resolved_model, normalized_message[:120])

    reply = call_proxy(resolved_model, messages)

    session["history"].append({"role": "user", "content": normalized_message})
    session["history"].append({"role": "assistant", "content": reply})

    auto_memory_saved = None
    if not memory_saved:
        try:
            auto_memory_saved = remember_if_relevant(
                normalized_message,
                source=f"{mode.lower()}-oi-auto",
            )
        except Exception as exc:
            logger.warning("Automatické uložení paměti selhalo: %s", exc)

    approval_id = None
    has_approval = False
    language, code, _ = extract_code_from_response(reply)
    if code:
        category, reasons = classify_action(code, language)
        if category != "read":
            approval_id = str(uuid.uuid4())
            pending_approvals[approval_id] = {
                "thread_id": thread_id,
                "code": code,
                "language": language,
                "category": category,
                "reasons": reasons,
                "mode": mode,
                "status": "pending",
                "created_at": datetime.now().isoformat(),
            }
            session["pending_approval"] = approval_id
            has_approval = True
            reply += (
                f"\n\n---\n**Čeká na potvrzení**\n\nTyp: {category}"
                f"\nDůvod: {', '.join(reasons)}"
                "\n\nV této fázi bude schválení pouze zaznamenáno; "
                "provádění kódu je zatím bezpečně vypnuté."
            )

    result = {
        "thread_id": thread_id,
        "reply": reply,
        "mode": mode,
        "model": req.model,
        "auto_run": False,
        "has_approval": has_approval,
        "approval_id": approval_id,
    }
    if memory_saved:
        result["memory_saved"] = memory_saved
    if auto_memory_saved:
        result["auto_memory_saved"] = auto_memory_saved
    return result


@app.get("/approvals")
def list_approvals():
    return {
        "count": len(pending_approvals),
        "approvals": [
            {
                "approval_id": aid,
                "category": item.get("category"),
                "status": item.get("status"),
                "mode": item.get("mode"),
                "created_at": item.get("created_at"),
            }
            for aid, item in pending_approvals.items()
        ],
    }


@app.get("/approvals/{approval_id}")
def get_approval(approval_id: str):
    approval = pending_approvals.get(approval_id)
    if not approval:
        raise HTTPException(status_code=404, detail="Approval not found")
    return approval


@app.post("/approvals/{approval_id}")
def approve_action(approval_id: str, req: ApproveRequest):
    approval = pending_approvals.get(approval_id)
    if not approval:
        raise HTTPException(status_code=404, detail="Approval not found")

    thread_id = approval["thread_id"]
    session = sessions.get(thread_id)

    if req.action == "deny":
        approval["status"] = "denied"
        if session:
            session["pending_approval"] = None
        return {"status": "denied", "approval_id": approval_id}

    if req.action != "approve":
        raise HTTPException(status_code=400, detail="Neznámá akce")

    approval["status"] = "approved-pending-runner"
    if session:
        session["pending_approval"] = None

    result = (
        "Schválení bylo zaznamenáno, ale kód nebyl spuštěn. "
        "SAFE/FULL LiteLLM Fáze 1 má provádění záměrně vypnuté, "
        "dokud nebude přidán neinteraktivní runner s timeouty a omezeným rozsahem."
    )

    executed_approvals[approval_id] = {
        "code": approval["code"],
        "language": approval["language"],
        "result": result,
        "executed_at": datetime.now().isoformat(),
        "execution": "not-run-phase1",
    }

    return {
        "status": "approved",
        "approval_id": approval_id,
        "result": result,
    }


@app.get("/executed/{approval_id}")
def get_executed(approval_id: str):
    result = executed_approvals.get(approval_id)
    if not result:
        raise HTTPException(status_code=404, detail="Execution not found")
    return result


@app.get("/sessions")
def list_sessions():
    return {
        "count": len(sessions),
        "sessions": [
            {
                "thread_id": thread_id,
                "mode": session.get("mode"),
                "model": session.get("model"),
                "messages": len(session.get("history", [])),
                "has_pending": session.get("pending_approval") is not None,
            }
            for thread_id, session in sessions.items()
        ],
    }


@app.delete("/sessions/{thread_id}")
def delete_session(thread_id: str):
    if thread_id in sessions:
        del sessions[thread_id]
        return {"status": "deleted"}
    raise HTTPException(status_code=404, detail="Session not found")


if __name__ == "__main__":
    logger.info("Starting SAFE/FULL LiteLLM Phase 1 API on http://%s:%s", HOST, PORT)
    uvicorn.run(app, host=HOST, port=PORT, log_level="info")
