# /// script
# requires-python = ">=3.9"
# dependencies = ["openpyxl", "python-docx", "python-pptx"]
# ///
"""Server MCP pentru citit fisiere Office: Excel (.xlsx), Word (.docx), PowerPoint (.pptx).

Doar citire. Doua unelte, ca sa nu umple contextul: serverele gata facute au 29 de
unelte (8360 de tokene, ~33 s de prefill in plus la FIECARE cerere), din care 13 de
scriere. Astea doua acopera citirea si cautarea.

Dependintele sunt declarate mai sus in stil PEP 723: `uv run office_mcp.py` le instaleaza
si le pastreaza in cache singur, fara mediu virtual de intretinut.
"""
import json, os, sys, glob

MAX = 60000  # taiem iesirea: un xlsx mare ar umple tot contextul


def _tabel_md(randuri):
    """Randuri de text -> tabel markdown. Prima linie devine antet."""
    randuri = [[("" if c is None else str(c)).replace("|", "\\|").replace("\n", " ")
                for c in r] for r in randuri]
    randuri = [r for r in randuri if any(c.strip() for c in r)]
    if not randuri:
        return ""
    n = max(len(r) for r in randuri)
    randuri = [r + [""] * (n - len(r)) for r in randuri]
    out = ["| " + " | ".join(randuri[0]) + " |", "|" + "---|" * n]
    out += ["| " + " | ".join(r) + " |" for r in randuri[1:]]
    return "\n".join(out)


def citeste_xlsx(cale, foaie=None, max_randuri=500):
    import openpyxl
    w = openpyxl.load_workbook(cale, data_only=True, read_only=True)
    parti = []
    for nume in w.sheetnames:
        if foaie and nume != foaie:
            continue
        s = w[nume]
        randuri, taiat = [], False
        for i, r in enumerate(s.iter_rows(values_only=True)):
            if i >= max_randuri:
                taiat = True
                break
            randuri.append(list(r))
        parti.append("## Foaia: %s" % nume)
        parti.append(_tabel_md(randuri) or "(goala)")
        if taiat:
            parti.append("_...taiat la %d randuri_" % max_randuri)
    w.close()
    if foaie and not parti:
        return "Nu exista foaia '%s'. Foi disponibile: %s" % (foaie, ", ".join(w.sheetnames))
    return "\n\n".join(parti)


def citeste_docx(cale, max_randuri=500):
    from docx import Document
    from docx.table import Table
    from docx.text.paragraph import Paragraph
    d = Document(cale)
    parti = []
    # parcurgem corpul in ordinea reala, ca tabelele sa ramana la locul lor in text.
    # markitdown pierde continutul tabelelor din .docx -- de aceea le luam noi.
    for copil in d.element.body.iterchildren():
        if copil.tag.endswith("}p"):
            p = Paragraph(copil, d)
            t = p.text.strip()
            if not t:
                continue
            stil = (p.style.name or "").lower()
            if stil.startswith("heading"):
                nivel = "".join(ch for ch in stil if ch.isdigit()) or "1"
                parti.append("#" * min(int(nivel) + 1, 6) + " " + t)
            elif stil.startswith("title"):
                parti.append("# " + t)
            else:
                parti.append(t)
        elif copil.tag.endswith("}tbl"):
            tb = Table(copil, d)
            randuri = [[c.text for c in r.cells] for r in tb.rows[:max_randuri]]
            md = _tabel_md(randuri)
            if md:
                parti.append(md)
    return "\n\n".join(parti) or "(document gol)"


def citeste_pptx(cale, max_randuri=500):
    from pptx import Presentation
    p = Presentation(cale)
    parti = []
    for i, sl in enumerate(p.slides, 1):
        buc = ["## Slide %d" % i]
        for f in sl.shapes:
            if f.has_table:
                randuri = [[c.text for c in r.cells] for r in f.table.rows[:max_randuri]]
                md = _tabel_md(randuri)
                if md:
                    buc.append(md)
            elif f.has_text_frame:
                t = "\n".join(x.text for x in f.text_frame.paragraphs if x.text.strip())
                if t.strip():
                    buc.append(t)
        if sl.has_notes_slide:
            n = (sl.notes_slide.notes_text_frame.text or "").strip()
            if n:
                buc.append("_Note vorbitor:_ " + n)
        parti.append("\n\n".join(buc))
    return "\n\n".join(parti) or "(prezentare goala)"


def citeste(cale, foaie=None, max_randuri=500):
    cale = os.path.expanduser(cale)
    if not os.path.exists(cale):
        return "Fisierul nu exista: %s" % cale
    ext = os.path.splitext(cale)[1].lower()
    try:
        if ext in (".xlsx", ".xlsm"):
            text = citeste_xlsx(cale, foaie, max_randuri)
        elif ext == ".docx":
            text = citeste_docx(cale, max_randuri)
        elif ext == ".pptx":
            text = citeste_pptx(cale, max_randuri)
        elif ext in (".xls", ".doc", ".ppt"):
            return ("Format vechi (%s), neacceptat. Salveaza fisierul ca %sx "
                    "sau converteste-l cu LibreOffice: "
                    "soffice --headless --convert-to %sx \"%s\""
                    % (ext, ext, ext, cale))
        else:
            return "Extensie neacceptata: %s (accept .xlsx, .docx, .pptx)" % ext
    except Exception as e:
        return "Nu am putut citi %s: %s: %s" % (cale, type(e).__name__, e)
    antet = "# %s\n\n" % os.path.basename(cale)
    if len(text) > MAX:
        text = text[:MAX] + "\n\n_...iesire taiata la %d caractere. Cere o foaie anume " \
                            "cu parametrul `foaie`, sau scade `max_randuri`._" % MAX
    return antet + text


def cauta(director, text, adancime=3):
    """Cauta un text in toate fisierele Office dintr-un director."""
    director = os.path.expanduser(director)
    if not os.path.isdir(director):
        return "Nu e un director: %s" % director
    tinte = []
    for ext in ("xlsx", "xlsm", "docx", "pptx"):
        for a in range(1, adancime + 1):
            tinte += glob.glob(os.path.join(director, *(["*"] * (a - 1)), "*." + ext))
    tinte = sorted(set(tinte))
    jos, gasite = text.lower(), []
    for f in tinte[:200]:
        continut = citeste(f, max_randuri=2000)
        linii = [l.strip() for l in continut.splitlines() if jos in l.lower()]
        if linii:
            gasite.append("**%s** (%d potriviri)\n%s" % (
                f, len(linii), "\n".join("  - " + l[:200] for l in linii[:5])))
    if not gasite:
        return "Nimic pentru '%s' in %d fisiere Office din %s" % (text, len(tinte), director)
    return "Gasit in %d din %d fisiere:\n\n%s" % (len(gasite), len(tinte), "\n\n".join(gasite))


UNELTE = [
    {"name": "citeste",
     "description": ("Citeste un fisier Office (.xlsx, .docx, .pptx) si il intoarce ca text "
                     "markdown: foile si celulele din Excel, paragrafele SI TABELELE din Word, "
                     "slide-urile si notele din PowerPoint."),
     "inputSchema": {"type": "object", "properties": {
         "cale": {"type": "string", "description": "calea catre fisier"},
         "foaie": {"type": "string", "description": "doar la Excel: numele unei singure foi"},
         "max_randuri": {"type": "integer", "description": "limita de randuri per foaie/tabel (implicit 500)"}},
         "required": ["cale"]}},
    {"name": "cauta",
     "description": ("Cauta un text in toate fisierele Office dintr-un director (recursiv). "
                     "Intoarce fisierele si liniile care se potrivesc."),
     "inputSchema": {"type": "object", "properties": {
         "director": {"type": "string", "description": "directorul in care se cauta"},
         "text": {"type": "string", "description": "textul cautat, fara diferenta de majuscule"},
         "adancime": {"type": "integer", "description": "cate niveluri de subdirectoare (implicit 3)"}},
         "required": ["director", "text"]}},
]


def raspunde(id_, rezultat=None, eroare=None):
    m = {"jsonrpc": "2.0", "id": id_}
    if eroare is not None:
        m["error"] = eroare
    else:
        m["result"] = rezultat
    sys.stdout.write(json.dumps(m) + "\n")
    sys.stdout.flush()


def main():
    for linie in sys.stdin:
        linie = linie.strip()
        if not linie:
            continue
        try:
            c = json.loads(linie)
        except Exception:
            continue
        m, id_ = c.get("method"), c.get("id")
        if m == "initialize":
            raspunde(id_, {"protocolVersion": "2024-11-05",
                           "capabilities": {"tools": {}},
                           "serverInfo": {"name": "office", "version": "1.0"}})
        elif m == "tools/list":
            raspunde(id_, {"tools": UNELTE})
        elif m == "tools/call":
            p = c.get("params", {})
            nume, a = p.get("name"), p.get("arguments", {})
            try:
                if nume == "citeste":
                    t = citeste(a.get("cale", ""), a.get("foaie"),
                                int(a.get("max_randuri", 500)))
                elif nume == "cauta":
                    t = cauta(a.get("director", ""), a.get("text", ""),
                              int(a.get("adancime", 3)))
                else:
                    t = "unealta necunoscuta: %s" % nume
            except Exception as e:
                t = "eroare: %s: %s" % (type(e).__name__, e)
            raspunde(id_, {"content": [{"type": "text", "text": t}]})
        elif m and m.startswith("notifications/"):
            pass
        elif id_ is not None:
            raspunde(id_, eroare={"code": -32601, "message": "metoda necunoscuta: %s" % m})


if __name__ == "__main__":
    main()
