#!/usr/bin/env python3
"""
Dev-Server + Prod-API-Container: serviert index.html + POST /api/match (KI-Matching via Mistral Small).
Lokal:  set -a; source ../../../.env; set +a; python3 dev_server.py
Check:  python3 dev_server.py --check
Prod (Container): HOST=0.0.0.0 gesetzt via Dockerfile, CORS auf ALLOWED_ORIGIN beschränkt.
"""
import json, os, sys, urllib.request, urllib.error
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

import yaml

HOST = os.environ.get("HOST", "127.0.0.1")
PORT = 8791
ALLOWED_ORIGIN = os.environ.get("ALLOWED_ORIGIN", "https://captain-ai.de")
MAX_INPUT_CHARS = 15_000
MISTRAL_URL = "https://api.mistral.ai/v1/chat/completions"
MISTRAL_MODEL = "mistral-small-latest"

# ponytail: clean_text/is_described aus build.py kopiert statt importiert —
# build.py baut beim Import sofort das HTML (kein main-Guard).
def clean_text(s):
    if not s:
        return s
    s = s.replace('\\n', ' ').replace('\n', ' ')
    s = s.strip().lstrip('"“„').strip()
    return ' '.join(s.split())

def is_described(p):
    t = clean_text(p.get('teaser'))
    return bool(t) and t not in ('—', '-', '–')

def build_catalog():
    """Kompakter Katalog für den Prompt. IDs = Reihenfolge in projects.yaml (wie build.py)."""
    projects = yaml.safe_load((Path(__file__).parent / "projects.yaml").read_text())
    catalog = []
    for i, p in enumerate(projects, 1):
        if not is_described(p):
            continue  # ohne Beschreibung kein sinnvolles Matching
        catalog.append({
            'id': i,
            'titel': p.get('title', ''),
            'org': p.get('org', ''),
            'teaser': clean_text(p.get('teaser', '')),
            'thema': p.get('thema', ''),
            'region': p.get('bundesland') or p.get('region') or 'DE-weit',
        })
    return catalog

CATALOG = build_catalog()
VALID_IDS = {c['id'] for c in CATALOG}

SYSTEM_PROMPT = """Du bist Matching-Assistent für ein Verzeichnis von Förder- und Unterstützungsangeboten \
des Programms "Nachhaltig wirken" (für gemeinwohlorientierte Unternehmen in Deutschland).
Der User beschreibt sein Bedürfnis in eigenen Worten oder fügt Text seiner Website ein.
Wähle aus dem Katalog die 3 bis 5 am besten passenden Angebote.
Berücksichtige inhaltliche Passung (Thema, Zielgruppe) und, falls erkennbar, die Region.
Antworte NUR mit JSON: {"matches": [{"id": <nummer>, "reason": "<ein Satz auf Deutsch, warum das Angebot passt, den User direkt ansprechen (du-Form)>"}]}
Nutze ausschließlich IDs aus dem Katalog. Bei völlig unpassenden Anfragen gib eine leere matches-Liste zurück."""

def call_mistral(user_text):
    catalog_json = json.dumps(CATALOG, ensure_ascii=False)
    payload = {
        "model": MISTRAL_MODEL,
        "temperature": 0.2,
        "response_format": {"type": "json_object"},
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": f"KATALOG:\n{catalog_json}\n\nANFRAGE DES USERS:\n{user_text}"},
        ],
    }
    req = urllib.request.Request(
        MISTRAL_URL,
        data=json.dumps(payload).encode(),
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {os.environ['MISTRAL_API_KEY']}",
        },
    )
    with urllib.request.urlopen(req, timeout=10) as resp:
        body = json.load(resp)
    content = json.loads(body["choices"][0]["message"]["content"])
    matches = content.get("matches", [])
    # Nur existierende IDs, max 5, reason als String erzwingen
    out = []
    for m in matches:
        if isinstance(m, dict) and m.get("id") in VALID_IDS:
            out.append({"id": m["id"], "reason": str(m.get("reason", ""))})
    return out[:5]

class Handler(SimpleHTTPRequestHandler):
    def do_OPTIONS(self):
        self.send_response(204)
        self._cors_headers()
        self.end_headers()

    def _cors_headers(self):
        self.send_header("Access-Control-Allow-Origin", ALLOWED_ORIGIN)
        self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")

    def do_POST(self):
        if self.path != "/api/match":
            self.send_error(404)
            return
        try:
            length = int(self.headers.get("Content-Length", 0))
            data = json.loads(self.rfile.read(length))
            text = (data.get("text") or "").strip()[:MAX_INPUT_CHARS]
        except Exception:
            self._json(400, {"error": "Ungültiger Request-Body"})
            return
        if not text:
            self._json(400, {"error": "Kein Text übergeben"})
            return
        try:
            matches = call_mistral(text)
            self._json(200, {"matches": matches})
        except KeyError:
            self._json(500, {"error": "MISTRAL_API_KEY fehlt in der Umgebung"})
        except Exception as e:
            self._json(502, {"error": f"Mistral-Anfrage fehlgeschlagen: {e}"})

    def _json(self, status, obj):
        body = json.dumps(obj, ensure_ascii=False).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self._cors_headers()
        self.end_headers()
        self.wfile.write(body)

def check():
    assert len(CATALOG) > 80, f"Katalog zu klein: {len(CATALOG)}"
    assert all(c['teaser'] for c in CATALOG), "Unbeschriebene Einträge im Katalog"
    assert all(c['id'] in VALID_IDS for c in CATALOG)
    ids = [c['id'] for c in CATALOG]
    assert len(ids) == len(set(ids)), "Doppelte IDs"
    print(f"OK — Katalog: {len(CATALOG)} beschriebene Projekte, IDs valide")

if __name__ == "__main__":
    if "--check" in sys.argv:
        check()
        sys.exit(0)
    os.chdir(Path(__file__).parent)  # SimpleHTTPRequestHandler serviert dieses Verzeichnis
    print(f"Server: http://{HOST}:{PORT}  ({len(CATALOG)} Projekte im Katalog, Key: {'gesetzt' if os.environ.get('MISTRAL_API_KEY') else 'FEHLT'}, CORS: {ALLOWED_ORIGIN})")
    ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
