# gateway.py — FastAPI + Moodle + Gemini + RAG

from __future__ import annotations

import os
import re
import logging
from pathlib import Path

import requests
from dotenv import load_dotenv

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

from rag import Retriever


# ==========================================================
# CONFIG
# ==========================================================

BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")

MODEL_API_URL = os.getenv("MODEL_API_URL", "").strip()
MODEL_API_KEY = os.getenv("MODEL_API_KEY", "").strip()

MODEL_NAME = os.getenv(
    "MODEL_MODEL",
    os.getenv("MODEL_NAME", "gemini-2.5-flash")
).strip()

REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))

ALLOWED_ORIGINS = [
    x.strip()
    for x in os.getenv("ALLOWED_ORIGINS", "*").split(",")
    if x.strip()
]


# ==========================================================
# LOGGING
# ==========================================================

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)

logger = logging.getLogger("assistant")


# ==========================================================
# FASTAPI
# ==========================================================

app = FastAPI(
    title="Assistant Gateway",
    version="1.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS if ALLOWED_ORIGINS else ["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

session = requests.Session()


# ==========================================================
# REQUEST MODEL
# ==========================================================

class ChatRequest(BaseModel):
    courseid: int
    question: str
    course_name: str = "Curso Moodle"


# ==========================================================
# HEALTH
# ==========================================================

@app.get("/")
def home():
    return {
        "status": "ok",
        "service": "assistant",
        "model": MODEL_NAME,
    }


@app.get("/health")
def health():
    return {"ok": True}


@app.get("/courses/{courseid}/rag-status")
def rag_status(courseid: int):
    try:
        retriever = Retriever(courseid)
        chunks = len(retriever.chunks)

        return {
            "ok": True,
            "courseid": courseid,
            "chunks": chunks,
            "rag_available": chunks > 0,
        }

    except Exception:
        return {
            "ok": False,
            "courseid": courseid,
            "chunks": 0,
            "rag_available": False,
        }


# ==========================================================
# HELPERS
# ==========================================================

def clean_answer(text: str) -> str:
    text = text or ""

    text = text.replace("###", "")
    text = text.replace("**", "")
    text = text.replace("__", "")

    text = re.sub(r"\n{3,}", "\n\n", text)
    text = text.strip()

    if len(text) > 1800:
        text = text[:1800].strip() + "..."

    return text


def build_tutor_prompt(
    course_name: str,
    question: str,
    passages: list[dict],
) -> str:
    context_parts = []

    for i, passage in enumerate(passages, start=1):
        text = passage.get("text", "").strip()
        source = passage.get("source", "").strip()
        section = passage.get("section", "").strip()

        if not text:
            continue

        context_parts.append(
            f"[Trecho {i}]\n"
            f"Título: {source}\n"
            f"Secção: {section}\n"
            f"Conteúdo:\n{text}"
        )

    context = "\n\n".join(context_parts)

    return f"""
Curso: {course_name}

Pergunta do estudante:
{question}

Contexto recuperado dos materiais do curso:
{context}

Instruções:
- Responda diretamente à pergunta do estudante.
- Use os materiais recuperados como contexto principal.
- Quando o contexto não trouxer uma definição completa, complemente com conhecimento académico geral.
- Não diga "os materiais dizem", "o contexto diz" ou "o material contém".
- Não mostre nomes de ficheiros, URLs, chunks, rankings ou metadados.
- Não copie literalmente os trechos recuperados.
- Responda em português europeu.
- Seja claro, didático e objetivo.
- Use no máximo 3 parágrafos curtos.
""".strip()


# ==========================================================
# GEMINI CLIENT
# ==========================================================

def call_llm_openai(prompt: str) -> str:
    if not MODEL_API_URL:
        return "MODEL_API_URL não configurado."

    if not MODEL_API_KEY:
        return "MODEL_API_KEY não configurado."

    if not MODEL_NAME:
        return "MODEL_MODEL não configurado."

    headers = {
        "Authorization": f"Bearer {MODEL_API_KEY}",
        "Content-Type": "application/json",
    }

    payload = {
        "model": MODEL_NAME,
        "messages": [
            {
                "role": "system",
                "content": (
                    "Você é um tutor virtual universitário integrado ao Moodle. "
                    "Explique conceitos de forma natural, clara e pedagógica. "
                    "Use o contexto do curso como base principal, mas pode complementar "
                    "com conhecimento académico geral quando o contexto estiver incompleto. "
                    "Nunca mostre fontes técnicas, JSON, URLs, chunks ou metadados."
                ),
            },
            {
                "role": "user",
                "content": prompt,
            },
        ],
        "temperature": 0.4,
    }

    try:
        response = session.post(
            MODEL_API_URL,
            headers=headers,
            json=payload,
            timeout=REQUEST_TIMEOUT,
        )

        if response.status_code >= 400:
            logger.error(response.text)
            raise HTTPException(
                status_code=502,
                detail=f"Erro Gemini: {response.status_code} - {response.text}",
            )

        data = response.json()

        answer = (
            data.get("choices", [{}])[0]
            .get("message", {})
            .get("content", "")
            .strip()
        )

        return clean_answer(answer or "O Gemini respondeu vazio.")

    except HTTPException:
        raise

    except Exception as e:
        logger.error(str(e))
        raise HTTPException(
            status_code=502,
            detail=f"Falha ao contactar Gemini: {e}",
        )


# ==========================================================
# CHAT ENDPOINT
# ==========================================================

@app.post("/chat")
def chat(req: ChatRequest):
    question = req.question.strip()

    if not question:
        raise HTTPException(
            status_code=400,
            detail="Pergunta vazia.",
        )

    try:
        retriever = Retriever(req.courseid)
        passages = retriever.search(question, k=6)

        if not passages:
            prompt = build_tutor_prompt(
                course_name=req.course_name,
                question=question,
                passages=[],
            )

            answer = call_llm_openai(prompt)

            return {"answer": answer}

        prompt = build_tutor_prompt(
            course_name=req.course_name,
            question=question,
            passages=passages,
        )

        answer = call_llm_openai(prompt)

        return {"answer": answer}

    except FileNotFoundError:
        raise HTTPException(
            status_code=404,
            detail=(
                f"O índice do curso {req.courseid} "
                f"não existe. Execute ingest.py primeiro."
            ),
        )

    except Exception as e:
        logger.error(str(e))
        raise HTTPException(
            status_code=500,
            detail=str(e),
        )


# ==========================================================
# ASK ALIAS
# ==========================================================

@app.post("/ask")
def ask(req: ChatRequest):
    return chat(req)


# ==========================================================
# MAIN
# ==========================================================

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(
        "gateway:app",
        host="0.0.0.0",
        port=8000,
        reload=True,
    )
