#!/usr/bin/env python3
"""
gamAI MCP server — izlaže gamAI alate agentima (Claude Code, Cursor, Codex, OpenCode...).
Radi preko gamai.io API-ja. Instalacija: pip install mcp ; python gamai_mcp.py
"""
import os, sys, json, uuid, mimetypes, urllib.request, urllib.error

BASE = os.environ.get("GAMAI_BASE", "https://gamai.io")

try:
    from mcp.server.fastmcp import FastMCP
except Exception:
    print("Potreban 'mcp' paket:  pip install mcp", file=sys.stderr)
    raise

mcp = FastMCP("gamai")


# ---------- HTTP helperi (stdlib, bez zavisnosti) ----------
def _req(url, data=None, headers=None, method=None, timeout=120):
    h = {"User-Agent": "gamai-mcp/1.0"}
    if headers:
        h.update(headers)
    r = urllib.request.Request(url, data=data, headers=h, method=method)
    with urllib.request.urlopen(r, timeout=timeout) as resp:
        return resp.read(), resp.headers.get("Content-Type", "")


def _get_json(path):
    b, _ = _req(BASE + path, timeout=30)
    return json.loads(b.decode("utf-8", "replace"))


def _post_json(path, payload, timeout=180):
    b, _ = _req(BASE + path, data=json.dumps(payload).encode(),
                headers={"Content-Type": "application/json"}, timeout=timeout)
    return json.loads(b.decode("utf-8", "replace"))


def _post_multipart(path, fields=None, files=None, timeout=180):
    """files: list of (fieldname, filename, bytes, content_type)"""
    fields = fields or {}
    files = files or []
    boundary = "----gamai" + uuid.uuid4().hex
    body = b""
    for k, v in fields.items():
        body += (f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n').encode("utf-8")
    for (k, fn, data, ct) in files:
        body += (f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"; filename="{fn}"\r\n'
                 f'Content-Type: {ct}\r\n\r\n').encode("utf-8") + data + b"\r\n"
    body += (f"--{boundary}--\r\n").encode()
    b, ct = _req(BASE + path, data=body,
                 headers={"Content-Type": "multipart/form-data; boundary=" + boundary}, timeout=timeout)
    return b, ct


def _guess_ct(fn):
    return mimetypes.guess_type(fn)[0] or "application/octet-stream"


def _save(out_dir, fn, data):
    os.makedirs(out_dir, exist_ok=True)
    p = os.path.join(out_dir, fn)
    with open(p, "wb") as f:
        f.write(data)
    return os.path.abspath(p)


# ---------- TOOLS ----------
@mcp.tool()
def search_skills(query: str, limit: int = 10) -> str:
    """Pretraži gamAI katalog (4176 skillova) po ključnoj riječi. Vraća naziv+opis+link."""
    try:
        cat = _get_json("/skills_catalog.json")
    except Exception as e:
        return f"Greška pri dohvatu kataloga: {e}"
    q = (query or "").lower().strip()
    hits = [s for s in cat.get("skills", [])
            if q in (s.get("slug", "") + " " + s.get("desc", "")).lower()]
    hits = hits[:max(1, min(limit, 50))]
    if not hits:
        return f"Nema rezultata za '{query}'. Ukupno {cat.get('count',0)} skillova."
    out = [f"Nađeno {len(hits)} (od {cat.get('count',0)}):"]
    for s in hits:
        out.append(f"- {s['slug']}  [{s.get('category','')}]  {s.get('desc','')[:110]}")
        out.append(f"  https://gamai.io/skill.html?s={s['slug']}")
    return "\n".join(out)


@mcp.tool()
def make_page(task: str, firma: str = "gamAI", tagline: str = "", publish: bool = False) -> str:
    """Napravi web stranicu/aplikaciju iz opisa (DeepSeek engine). Vraća HTML + download link (+ živi URL ako publish=True)."""
    try:
        r = _post_json("/v1/uni", {"task": task, "firma": firma, "tagline": tagline, "publish": bool(publish)})
    except Exception as e:
        return f"Greška: {e}"
    if not r.get("ok"):
        return "Neuspješno: " + json.dumps(r)[:300]
    html = r.get("index_html", "")
    dl = r.get("download_url", "")
    return (f"OK ({r.get('vrsta','sajt')}, {r.get('engine','')}).\n"
            f"download: {BASE}{dl}\n" + (f"url: {r.get('url')}\n" if r.get("url") else "") +
            f"--- HTML (prvih 2000 znakova) ---\n{html[:2000]}")


@mcp.tool()
def ocr(image_path: str, lang: str = "eng") -> str:
    """Izvuci tekst iz slike (OCR). lang: eng/bos/hrv/srp/deu. image_path = putanja do slike."""
    if not os.path.isfile(image_path):
        return f"Fajl ne postoji: {image_path}"
    data = open(image_path, "rb").read()
    try:
        b, ct = _post_multipart("/prog/ocr", files=[("file", os.path.basename(image_path), data, _guess_ct(image_path))],
                                fields={"lang": lang}, timeout=120)
        return json.loads(b.decode("utf-8", "replace")).get("text", "(nema teksta)")
    except Exception as e:
        return f"Greška: {e}"


@mcp.tool()
def to_pdf(file_path: str) -> str:
    """Pretvori Word/Excel/PPT/TXT dokument u PDF. Vraća putanju sačuvanog PDF-a."""
    if not os.path.isfile(file_path):
        return f"Fajl ne postoji: {file_path}"
    data = open(file_path, "rb").read()
    try:
        b, ct = _post_multipart("/prog/pdf", files=[("file", os.path.basename(file_path), data, _guess_ct(file_path))], timeout=180)
        if b[:4] != b"%PDF":
            return "Greška: " + b.decode("utf-8", "replace")[:200]
        return "PDF: " + _save(os.environ.get("GAMAI_OUT", os.getcwd()), os.path.splitext(os.path.basename(file_path))[0] + ".pdf", b)
    except Exception as e:
        return f"Greška: {e}"


@mcp.tool()
def make_qr(text: str, size: int = 8) -> str:
    """Napravi QR kod (PNG) iz teksta/linka. Vraća putanju PNG-a."""
    try:
        b, ct = _post_multipart("/prog/qr", fields={"text": text, "size": str(size)}, timeout=40)
        if b[:8] != b"\x89PNG\r\n\x1a\n":
            return "Greška: " + b.decode("utf-8", "replace")[:200]
        return "QR: " + _save(os.environ.get("GAMAI_OUT", os.getcwd()), "qr.png", b)
    except Exception as e:
        return f"Greška: {e}"


@mcp.tool()
def text_to_speech(text: str, voice: str = "bs-BA-GoranNeural") -> str:
    """Pretvori tekst u govor (MP3). Glasovi: bs-BA-GoranNeural/VesnaNeural, hr-HR-GabrijelaNeural, sr-RS-SophieNeural, de-DE-KatjaNeural, en-US-JennyNeural."""
    try:
        b, ct = _post_multipart("/prog/tts", fields={"text": text, "voice": voice}, timeout=90)
        if b[:3] != b"ID3" and b[:2] != b"\xff\xfb" and b[:2] != b"\xff\xf3":
            return "Greška: " + b.decode("utf-8", "replace")[:200]
        return "MP3: " + _save(os.environ.get("GAMAI_OUT", os.getcwd()), "speech.mp3", b)
    except Exception as e:
        return f"Greška: {e}"


@mcp.tool()
def run_python(code: str, stdin: str = "") -> str:
    """Pokreni Python kod u sigurnom sandboxu (30s, bez mreže). Vraća stdout/stderr."""
    try:
        b, ct = _post_multipart("/prog/py", fields={"stdin": stdin},
                                files=[("file", "script.py", code.encode("utf-8"), "text/x-python")], timeout=60)
        d = json.loads(b.decode("utf-8", "replace"))
        return f"stdout:\n{d.get('stdout','')}\n--- stderr ---\n{d.get('stderr','')}"
    except Exception as e:
        return f"Greška: {e}"


@mcp.tool()
def merge_pdfs(pdf_paths: list[str]) -> str:
    """Spoji više PDF fajlova u jedan. Vraća putanju spojenog PDF-a."""
    files = []
    for p in pdf_paths:
        if os.path.isfile(p):
            files.append(("files", os.path.basename(p), open(p, "rb").read(), "application/pdf"))
    if not files:
        return "Nema validnih PDF fajlova."
    try:
        b, ct = _post_multipart("/prog/pdfmerge", files=files, timeout=120)
        if b[:4] != b"%PDF":
            return "Greška: " + b.decode("utf-8", "replace")[:200]
        return "Spojeno: " + _save(os.environ.get("GAMAI_OUT", os.getcwd()), "merged.pdf", b)
    except Exception as e:
        return f"Greška: {e}"


@mcp.tool()
def audit_skill(skill_md_text: str) -> str:
    """Sigurnosno-kvalitetna provjera SKILL.md (deterministički): ocjena 0-100 + nalazi (opasne komande, tajne, prompt injection, nepinned zavisnosti)."""
    import re
    RULES = [
        ("curl|bash","crit",r"(curl|wget)[^\n]*\|\s*(bash|sh|zsh|python)","Preuzimanje i izvršavanje sa mreže"),
        ("rm -rf /","crit",r"rm\s+-rf\s+(\/|\$HOME|~)","Brisanje korijena/home"),
        ("base64-exec","crit",r"(base64\s+-d|atob\s*\()[^\n]{0,40}(\||eval|exec)","Base64 dekodiranje pa izvršavanje"),
        ("secret","crit",r"(sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----)","Izložena tajna/ključ"),
        ("exfil","crit",r"(curl|wget|requests\.post|fetch\()[^\n]{0,80}(env|\$HOME|passwd|id_rsa|\.ssh)","Moguća eksfiltracija"),
        ("eval","high",r"\beval\s*\(|exec\s*\(|Function\s*\(","Dinamičko izvršavanje (eval/exec)"),
        ("injection","high",r"(ignore (all )?previous|disregard (all )?previous|reveal (your )?system prompt|you are now|jailbreak)","Prompt injection"),
        ("password","high",r"(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*[\"'][^\"']{6,}[\"']","Hardkodirana lozinka/ključ"),
        ("chmod777","high",r"chmod\s+(777|a\+rwx)","chmod 777"),
        ("traversal","high",r"\.\.\/\.\.\/","Path traversal"),
        ("destructive","high",r"\b(drop\s+table|truncate\s+table|format\s+[a-z]:|diskpart)\b","Destruktivna operacija"),
        ("sudo","med",r"\bsudo\b","Korištenje sudo"),
        ("npx-unpinned","med",r"npx\s+-y\s+(?!@?[a-z0-9-]+@)[a-z0-9@/_-]+","Nepinned npx"),
        ("pip-unpinned","med",r"pip\s+install\s+(?!.*==)[A-Za-z0-9_-]+","Nepinned pip"),
        ("shell-true","med",r"shell\s*=\s*True|os\.system\s*\(","Shell izvršavanje"),
    ]
    W = {"crit":26,"high":14,"med":7,"low":2}
    lines = skill_md_text.split("\n")
    sec = bool(re.search(r"security|audit|pentest|malware|forensic|detect|scan|hardening|threat", (skill_md_text[:600]).lower()))
    down = {"crit":"high","high":"med","med":"low"}
    findings = []
    for rid, sev, rx, msg in RULES:
        for ln in lines:
            if re.search(rx, ln, re.I):
                s = down[sev] if sec else sev
                findings.append((s, msg, ln.strip()[:80] + (" [primjer/security-skill]" if sec else "")))
                break
    if not re.search(r"description:\s*\S", skill_md_text, re.I):
        findings.append(("med", "Nema 'description:'", ""))
    pen = sum(W.get(s, 0) for s, _, _ in findings)
    score = max(0, min(100, 100 - pen))
    if not findings:
        return f"Ocjena: {score}/100 — nema nalaza. (Provjeri logiku ručno.)"
    order = {"crit":0,"high":1,"med":2,"low":3}
    findings.sort(key=lambda x: order[x[0]])
    out = [f"Ocjena: {score}/100 — {len(findings)} nalaza:"]
    for s, msg, ex in findings:
        out.append(f"[{s.upper()}] {msg}" + (f"  |  {ex}" if ex else ""))
    return "\n".join(out)


if __name__ == "__main__":
    mcp.run()
