#!/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 sys
from pathlib import Path

# Přidáme adresář scripts do sys.path, abychom mohli importovat safe_runner
_scripts_dir = Path(__file__).resolve().parent
if str(_scripts_dir) not in sys.path:
    sys.path.insert(0, str(_scripts_dir))

import safe_runner

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 = int(os.getenv("CB26_SAFE_OI_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:
    mode = (mode or "SAFE").upper()

    common = """
Jsi CyberBrain26, lokální AI asistent pro projekt v /home/oem/CyberBrain26.
Komunikuj česky, prakticky a stručně.

Nikdy nevyzrazuj ani nevypisuj hodnoty z .env, API klíče, tokeny, hesla,
SSH privátní klíče, cookies ani jiné secrets. Pokud je nutná kontrola .env,
pracuj pouze se jmény proměnných bez hodnot.

Před akcí stručně řekni plán. Po akci jasně uveď skutečný výsledek,
návratový kód, relevantní výstup a další bezpečný krok.
Neprohlašuj, že se něco provedlo, pokud nemáš skutečný výsledek nástroje.
"""

    safe = """
Režim SAFE:
- Nic automaticky nespouštěj a nic neměň.
- Připrav návrhy příkazů nebo kroků v samostatném bash bloku.
- Vysvětli, co by příkaz provedl a jaké může mít dopady.
"""

    full = """
Režim FULL MAX:
- Máš oprávnění autonomně provádět read-only diagnostiku lokálního projektu
  CyberBrain26, služeb a Linux VM.
- Smíš samostatně procházet necitlivé soubory, hledat v projektu, číst logy,
  kontrolovat procesy, porty, health endpointy, modely, Git stav, testy,
  Python syntax, konfigurace bez secrets, konverzace a metadata paměti.
- Smíš autonomně vytvářet, upravovat, přesouvat a mazat běžné pracovní soubory
  pouze uvnitř:
  /home/oem/CyberBrain26
  /home/oem/CyberBrain26/runner-workspace
  /tmp/cb26-sandbox
- U změny existujícího zdrojového nebo konfiguračního souboru nejdříve vytvoř
  timestampovanou zálohu do /home/oem/CyberBrain26/backups.
- Po změně Pythonu proveď py_compile; po změně služby proveď relevantní
  health-check; po změně logiky proveď cílený funkční test.
- Při selhání pokračuj bezpečnou read-only diagnostikou, nalezni konkrétní
  chybu a navrhni nebo proveď vratelnou opravu v povoleném projektu.
- Diagnostika vlastního lokálního programu a jeho funkcí je explicitně povolena.
- I ve FULL MAX si vždy vyžádej samostatné potvrzení před: sudo, su, změnou
  /etc, /usr, /boot, /var/lib nebo domovských dat mimo projekt; apt, pip,
  npm či jinou instalací; Dockerem; změnou firewallu, sítě, Tailscale nebo
  Cloudflare Tunnel; veřejným odesláním dat; Git push/force push;
  git reset --hard; git clean; mazáním databází, konverzací, záloh nebo
  virtuálních prostředí; reboot, shutdown, kill hromadných procesů;
  dd, mkfs, mount, chmod/chown -R; a akcemi se secrets.
"""

    return common + (safe if mode == "SAFE" else full)


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):
    """
    Vrací (category, reasons).

    Kategorie:
    - read: bezpečné čtení a diagnostika, automaticky ve FULL.
    - project_write: běžná vratelná práce v povoleném projektu/sandboxu,
      automaticky ve FULL.
    - write: citlivá/riziková činnost, vyžaduje approval.
    """
    code = (code or "").strip()
    language = (language or "bash").strip().lower()
    lowered = code.lower()

    if not code:
        return "write", ["prázdný nebo nejednoznačný příkaz"]

    always_approval = [
        "sudo", "su ", "doas",
        "apt ", "apt-get", "dpkg", "snap ", "flatpak ",
        "pip install", "pip uninstall", "npm install", "npm uninstall",
        "docker ", "podman ",
        "reboot", "shutdown", "poweroff", "halt",
        "mkfs", "fdisk", "parted", "dd ", "mount ", "umount ",
        "iptables", "nft ", "ufw ", "firewall",
        "tailscale", "cloudflared", "ngrok",
        "curl ", "wget ", "ssh ", "scp ", "rsync ",
        "git push", "git reset --hard", "git clean", "git checkout --",
        "rm -rf", "rm -r ", "rmdir ",
        "chmod -r", "chown -r",
        ".env", "id_rsa", "authorized_keys", ".ssh/",
        "/etc/", "/usr/", "/boot/", "/var/lib/",
        "chroma.sqlite3", "workspace/chroma_db",
        "data/conversations", "venv/", "venv-",
    ]
    for token in always_approval:
        if token in lowered:
            return "write", [f"citlivá nebo systémová operace: {token}"]

    shell_operators = [";", "&&", "||", "`", "$(", "\n"]
    if language in {"bash", "sh", "shell", "zsh"}:
        for operator in shell_operators:
            if operator in code:
                return "write", ["složený shell příkaz nebo řetězení"]

    read_prefixes = (
        "pwd", "ls", "find ", "grep ", "rg ", "cat ", "head ", "tail ",
        "sed -n", "awk ", "cut ", "sort ", "uniq ", "wc ", "stat ",
        "file ", "du ", "df ", "free", "ps ", "pgrep ", "ss ",
        "git status", "git diff", "git log", "git branch",
        "python -m py_compile", "python3 -m py_compile",
        "pytest", "curl -s http://127.0.0.1", "curl -sS http://127.0.0.1",
    )
    if language in {"bash", "sh", "shell", "zsh"} and lowered.startswith(read_prefixes):
        if ">" not in code and ">>" not in code:
            return "read", ["bezpečný čtecí nebo diagnostický příkaz"]

    allowed_paths = (
        "/home/oem/cyberbrain26",
        "/tmp/cb26-sandbox",
        "/home/oem/cyberbrain26/runner-workspace",
    )
    is_allowed_path = any(p in lowered for p in allowed_paths)

    project_write_markers = (
        "touch ", "mkdir ", "mkdir -p", "cp ", "mv ",
        "python ", "python3 ", "pytest ",
        "git add ", "git commit ",
        "tee ", ">",
    )

    if language in {"bash", "sh", "shell", "zsh"}:
        if is_allowed_path and lowered.startswith(project_write_markers):
            return "project_write", ["vratelná práce v povoleném projektu nebo sandboxu"]

        if lowered.startswith(("touch ", "mkdir ", "mkdir -p")) and "/tmp/cb26-sandbox" in lowered:
            return "project_write", ["vratelná práce v sandboxu"]

    if language in {"python", "python3"}:
        dangerous_python = (
            "subprocess", "os.system", "shutil.rmtree", "os.remove",
            "pathlib.path.unlink", "requests.", "urllib", "socket",
        )
        if any(token in lowered for token in dangerous_python):
            return "write", ["Python kód obsahuje citlivou operaci"]

        if any(path in lowered for path in allowed_paths):
            return "project_write", ["Python práce v povoleném projektu nebo sandboxu"]

        return "read", ["neprivilegovaný Python diagnostický kód"]

    return "write", ["neznámý nebo nejednoznačný typ akce"]


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
    auto_run = False
    run_result = None

    language, code, _ = extract_code_from_response(reply)
    if code:
        category, reasons = classify_action(code, language)

        if mode == "FULL" and category in {"read", "project_write"}:
            try:
                run_result = safe_runner.run_approved_code(code, language)
                auto_run = True

                result_text = (
                    f"Status: {run_result.get('status', 'unknown')}\n"
                    f"Return code: {run_result.get('returncode', 'N/A')}\n"
                )

                if run_result.get("stdout"):
                    result_text += f"STDOUT:\n{run_result['stdout']}\n"

                if run_result.get("stderr"):
                    result_text += f"STDERR:\n{run_result['stderr']}\n"

                reply += (
                    "\n\n---\n"
                    "**Automaticky proveden bezpečný čtecí příkaz**\n\n"
                    f"Typ: {category}\n"
                    f"Důvod: {', '.join(reasons)}\n\n"
                    f"```text\n{result_text.strip()}\n```"
                )

                session["history"].append({
                    "role": "system",
                    "content": (
                        "Bezpečný čtecí příkaz byl proveden. Výsledek:\n"
                        + result_text
                    ),
                })

            except Exception as exc:
                reply += (
                    "\n\n---\n"
                    "**Chyba při automatickém spuštění bezpečného čtecího příkazu**\n\n"
                    f"`{type(exc).__name__}: {exc}`"
                )

        elif 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\nTento příkaz může měnit systém nebo data. "
                "Pro provedení vyžaduje samostatné potvrzení."
            )

    result = {
        "thread_id": thread_id,
        "reply": reply,
        "mode": mode,
        "model": req.model,
        "auto_run": auto_run,
        "run_result": run_result,
        "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

        executed_approvals[approval_id] = {
            "code": approval["code"],
            "language": approval["language"],
            "result": "Akce byla uživatelem zamítnuta.",
            "executed_at": datetime.now().isoformat(),
            "execution": {"status": "denied"},
        }
        pending_approvals.pop(approval_id, None)

        return {
            "status": "denied",
            "approval_id": approval_id,
            "reply": "❌ Akce byla zamítnuta.",
            "pending_approval": None,
            "events": [
                {
                    "id": int(datetime.now().timestamp() * 1000),
                    "type": "approval.denied",
                    "payload": {"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

    try:
        run_result = safe_runner.run_approved_code(
            approval["code"],
            approval["language"],
        )
        execution_status = run_result.get("status", "unknown")
        final_status = "executed" if execution_status not in {"error", "failed"} else "failed"

        result_text = (
            f"Status: {execution_status}\n"
            f"Return code: {run_result.get('returncode', 'N/A')}\n"
        )
        if run_result.get("stdout"):
            result_text += f"STDOUT:\n{run_result['stdout']}\n"
        if run_result.get("stderr"):
            result_text += f"STDERR:\n{run_result['stderr']}\n"

    except Exception as exc:
        final_status = "failed"
        result_text = f"Chyba při provádění: {type(exc).__name__}: {exc}"
        run_result = {
            "status": "error",
            "error": str(exc),
        }

    approval["status"] = final_status

    executed_approvals[approval_id] = {
        "code": approval["code"],
        "language": approval["language"],
        "result": result_text,
        "executed_at": datetime.now().isoformat(),
        "execution": run_result,
    }

    pending_approvals.pop(approval_id, None)

    event_type = "approval.executed" if final_status == "executed" else "run.failed"
    reply_prefix = "✅ Schválená akce byla provedena." if final_status == "executed" else "⛔ Schválená akce selhala."

    return {
        "status": final_status,
        "approval_id": approval_id,
        "thread_id": thread_id,
        "reply": f"{reply_prefix}\n\n{result_text}",
        "result": result_text,
        "pending_approval": None,
        "events": [
            {
                "id": int(datetime.now().timestamp() * 1000),
                "type": event_type,
                "payload": {
                    "approval_id": approval_id,
                    "status": final_status,
                },
            }
        ],
    }

@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")
