from fastapi import FastAPI, Query, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.openapi.utils import get_openapi
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from sentence_transformers import SentenceTransformer
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct, PointIdsList
from uuid import uuid4
import requests
import os
from datetime import datetime, date

# Version hochgezählt
__version__ = "1.1.1"
print(f"[DEBUG] llm_api.py version {__version__} loaded from {__file__}", flush=True)

# Ollama-Konfiguration
OLLAMA_URL   = os.getenv("OLLAMA_URL", "http://localhost:11434/api/generate")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "mistral:latest")

# FastAPI-Instanz
app = FastAPI(
    title="KI Trainerassistent API",
    description="Lokale API für Trainingsplanung",
    version=__version__,
    docs_url="/docs",
    redoc_url="/redoc",
    openapi_url="/openapi.json"
)

# Globaler Fehlerhandler
@app.exception_handler(Exception)
async def unicorn_exception_handler(request: Request, exc: Exception):
    return JSONResponse(status_code=500, content={"detail": "Interner Serverfehler. Bitte später erneut versuchen."})

# ------------------------
# Modelle für Embed/Search
# ------------------------
class ChunkInput(BaseModel):
    text: str
    source: str
    source_type: str = ""
    title: str       = ""
    version: str     = ""
    related_to: str  = ""
    tags: List[str]  = []
    owner: str       = ""
    context_tag: Optional[str] = None
    imported_at: Optional[str] = None
    chunk_index:   Optional[int] = None
    category:      Optional[str] = None

class EmbedRequest(BaseModel):
    chunks: List[ChunkInput]
    collection: str = "default"

class PromptRequest(BaseModel):
    query: str
    context_limit: int = 3
    collection: str    = "default"

class EmbedResponse(BaseModel):
    status: str
    count: int
    collection: str

class SearchResultItem(BaseModel):
    score: float = Field(..., ge=0)
    text: str

class PromptResponse(BaseModel):
    answer: str
    context: str
    collection: str

class DeleteResponse(BaseModel):
    status: str
    count: int
    collection: str
    source:  Optional[str] = None
    type:    Optional[str] = None

# ------------------------------------
# Neue Modelle für Exercises & Plans
# ------------------------------------
class Exercise(BaseModel):
    id: str = Field(default_factory=lambda: str(uuid4()))
    title: str
    summary: str
    short_description: str
    keywords: List[str] = []
    link: Optional[str] = None

    discipline: str
    group: Optional[str] = None
    age_group: str
    target_group: str
    min_participants: int
    duration_minutes: int

    capabilities: Dict[str,int] = {}
    category: str

    purpose: str
    execution: str
    notes: str
    preparation: str
    method: str
    equipment: List[str] = []

class PhaseExercise(BaseModel):
    exercise_id: str
    cond_load:   Dict[str, Any] = {}
    coord_load:  Dict[str, Any] = {}
    instructions: str = ""

class PlanPhase(BaseModel):
    name: str
    duration_minutes: int
    method: str
    method_notes: str = ""
    exercises: List[PhaseExercise]

class TrainingPlan(BaseModel):
    id: str = Field(default_factory=lambda: str(uuid4()))
    title: str
    short_description: str

    collection: str
    discipline: str
    group:      Optional[str] = None
    dojo:       str
    date:       date
    plan_duration_weeks: int
    focus_areas: List[str] = []
    predecessor_plan_id: Optional[str] = None

    age_group: str
    created_at: datetime = Field(default_factory=datetime.utcnow)

    phases: List[PlanPhase]

# ----------------------------------
# Embedding-Modell und Qdrant-Client
# ----------------------------------
model  = SentenceTransformer("all-MiniLM-L6-v2")
qdrant = QdrantClient(
    host=os.getenv("QDRANT_HOST", "localhost"),
    port=int(os.getenv("QDRANT_PORT", 6333))
)

# Ensure Exercise-Collection exists
if not qdrant.collection_exists("exercises"):
    qdrant.recreate_collection(
        collection_name="exercises",
        vectors_config=VectorParams(
            size=model.get_sentence_embedding_dimension(),
            distance=Distance.COSINE
        )
    )

# ----------------------
# Endpunkte für Exercises
# ----------------------
@app.post("/exercise", response_model=Exercise)
def create_exercise(ex: Exercise):
    vec = model.encode(f"{ex.title}. {ex.summary}").tolist()
    point = PointStruct(id=ex.id, vector=vec, payload=ex.dict())
    qdrant.upsert(collection_name="exercises", points=[point])
    return ex

@app.get("/exercise", response_model=List[Exercise])
def list_exercises(
    discipline: Optional[str] = Query(None),
    group:      Optional[str] = Query(None),
    tags:       Optional[str] = Query(None)  # kommagetrennt
):
    filters = []
    if discipline:
        filters.append({"key":"discipline","match":{"value":discipline}})
    if group:
        filters.append({"key":"group","match":{"value":group}})
    if tags:
        for t in tags.split(","):
            filters.append({"key":"keywords","match":{"value":t.strip()}})
    if filters:
        pts, _ = qdrant.scroll(
            collection_name="exercises",
            scroll_filter={"must": filters},
            limit=10000
        )
    else:
        pts, _ = qdrant.scroll(collection_name="exercises", limit=10000)
    return [Exercise(**pt.payload) for pt in pts]

# -----------------
# Bestehende Endpunkte
# -----------------
@app.post("/embed")
def embed_texts(data: EmbedRequest):
    collection_name = data.collection
    if not qdrant.collection_exists(collection_name):
        qdrant.recreate_collection(
            collection_name=collection_name,
            vectors_config=VectorParams(size=384, distance=Distance.COSINE)
        )
    embeddings = model.encode([c.text for c in data.chunks]).tolist()
    points = []
    for i, chunk in enumerate(data.chunks):
        payload = {
            "text": chunk.text,
            "source": chunk.source,
            "source_type": chunk.source_type,
            "title": chunk.title,
            "version": chunk.version,
            "related_to": chunk.related_to,
            "tags": chunk.tags,
            "owner": chunk.owner,
            "context_tag": chunk.context_tag,
            "imported_at": chunk.imported_at or datetime.utcnow().isoformat(),
            "chunk_index": chunk.chunk_index,
            "category": chunk.category or data.collection
        }
        points.append(PointStruct(id=str(uuid4()), vector=embeddings[i], payload=payload))
    qdrant.upsert(collection_name=collection_name, points=points)
    return {"status":"✅ embeddings saved","count":len(points),"collection":collection_name}

@app.get("/search", response_model=List[SearchResultItem])
def search_text(query: str = Query(..., min_length=1), limit: int = Query(3, ge=1), collection: str = Query("default")):
    vec = model.encode(query).tolist()
    res = qdrant.search(collection_name=collection, query_vector=vec, limit=limit)
    return [SearchResultItem(score=r.score, text=r.payload['text']) for r in res]

@app.post("/prompt", response_model=PromptResponse)
def prompt(data: PromptRequest):
    if not data.query.strip(): raise HTTPException(status_code=400, detail="'query' darf nicht leer sein.")
    if not (1 <= data.context_limit <= 10): raise HTTPException(status_code=400, detail="'context_limit' muss zwischen 1 und 10 liegen.")
    hits = qdrant.search(
        collection_name=data.collection,
        query_vector=model.encode(data.query).tolist(),
        limit=data.context_limit
    )
    context = "\n".join(h.payload['text'] for h in hits)
    payload = {"model":OLLAMA_MODEL,"prompt":f"Context:\n{context}\nQuestion: {data.query}","stream":False}
    try:
        r = requests.post(OLLAMA_URL, json=payload, timeout=30); r.raise_for_status()
    except Exception:
        raise HTTPException(status_code=502, detail="LLM-Service-Fehler.")
    return PromptResponse(answer=r.json().get("response",""), context=context, collection=data.collection)

@app.delete("/delete-source", response_model=DeleteResponse)
def delete_by_source(
    collection: str = Query(...),
    source: Optional[str] = Query(None),
    type:   Optional[str] = Query(None),
    owner:  Optional[str] = Query(None),
    category: Optional[str] = Query(None)
):
    if not qdrant.collection_exists(collection):
        raise HTTPException(status_code=404, detail=f"Collection '{collection}' nicht gefunden.")
    filt = []
    if source:   filt.append({"key":"source","match":{"value":source}})
    if type:     filt.append({"key":"type","match":{"value":type}})
    if owner:    filt.append({"key":"owner","match":{"value":owner}})
    if category: filt.append({"key":"category","match":{"value":category}})
    if not filt:
        raise HTTPException(status_code=400, detail="Mindestens ein Filterparameter muss angegeben werden.")
    pts, _ = qdrant.scroll(collection_name=collection, scroll_filter={"must":filt}, limit=10000)
    ids = [str(p.id) for p in pts]
    if not ids:
        return DeleteResponse(status="🔍 Keine passenden Einträge gefunden.", count=0, collection=collection)
    qdrant.delete(collection_name=collection, points_selector=PointIdsList(points=ids))
    return DeleteResponse(status="🗑️ gelöscht", count=len(ids), collection=collection)

@app.delete("/delete-collection", response_model=DeleteResponse)
def delete_collection(collection: str = Query(...)):
    if not qdrant.collection_exists(collection):
        raise HTTPException(status_code=404, detail=f"Collection '{collection}' nicht gefunden.")
    qdrant.delete_collection(collection_name=collection)
    return DeleteResponse(status="🗑️ gelöscht", count=0, collection=collection)

# ------------------------
# Endpunkte für TrainingPlans
# ------------------------
@app.post("/plan", response_model=TrainingPlan)
def create_plan(plan: TrainingPlan):
    coll = "training_plans"
    if not qdrant.collection_exists(coll):
        qdrant.recreate_collection(
            collection_name=coll,
            vectors_config=VectorParams(
                size=model.get_sentence_embedding_dimension(),
                distance=Distance.COSINE
            )
        )
    vec = model.encode(f"{plan.title}. {plan.short_description}").tolist()
    payload = plan.dict()
    qdrant.upsert(collection_name=coll, points=[PointStruct(id=plan.id, vector=vec, payload=payload)])
    return plan

@app.get("/plan", response_model=List[TrainingPlan])
def list_plans(
    collection: str = Query("training_plans"),
    discipline: Optional[str] = Query(None),
    group:      Optional[str] = Query(None),
    dojo:       Optional[str] = Query(None)
):
    if not qdrant.collection_exists(collection):
        return []
    pts, _ = qdrant.scroll(collection_name=collection, limit=10000)
    result = []
    for p in pts:
        pl = TrainingPlan(**p.payload)
        if discipline and pl.discipline != discipline: continue
        if group      and pl.group      != group:      continue
        if dojo       and pl.dojo       != dojo:       continue
        result.append(pl)
    return result
