#!/usr/bin/env python3
"""Der Hobba-Connector für Claude Code — ein MCP-Server (stdio).

Der Server selbst weiß nichts über Aufgaben: Er reicht jeden Werkzeug-Aufruf
als JSON-Befehl an den Kurzbefehl „Hobba-Connector" weiter (`shortcuts run`),
und der besteht aus genau einer Aktion — dem ConnectorIntent der App. Alle
Regeln (Papierkorb, Fokus, Tags, Wiederholungen, Pro-Limit) wohnen damit
weiter in Hobba; hier steht nur der Draht.

Einrichten (einmalig, siehe hobba-mcp-README.md):
  1. Hobba mit dem ConnectorIntent installiert und einmal gestartet.
  2. Kurzbefehl „Hobba-Connector" angelegt: eine Aktion „Hobba-Connector",
     Parameter „Befehl (JSON)" = Kurzbefehl-Eingabe.
  3. claude mcp add -s user hobba -- python3 <dieser Pfad>

Nur Standardbibliothek — läuft mit jedem macOS-python3.
"""

import json
import subprocess
import sys
import os
import uuid

SHORTCUT = "Hobba-Connector"
TIMEOUT = 90  # Sekunden — großzügig, der erste Aufruf startet die App im Hintergrund.

# Bewusst /tmp statt tempfile, relative Pfade mit cwd — und die Antwort über
# STDOUT statt über `-o`: Das Ausgabedatei-Flag schreibt aus Unterprozessen
# (Python → shortcuts) schlicht nichts, rc 0 und keine Datei, egal ob /tmp,
# /private/tmp oder $TMPDIR; ohne `-o` liefert `shortcuts run` das Ergebnis
# zuverlässig auf stdout (gemessen 25.08.2026, alle Formen nebeneinander).
TMP = "/tmp/hobba-connector"

EINRICHTUNG = (
    "Der Kurzbefehl „Hobba-Connector“ fehlt oder antwortet nicht. Einmalige "
    "Einrichtung: Kurzbefehle-App → neuer Kurzbefehl namens „Hobba-Connector“ "
    "mit genau einer Aktion „Hobba-Connector“ (aus Hobba), Parameter "
    "„Befehl (JSON)“ = Kurzbefehl-Eingabe. Voraussetzung: eine Hobba-Fassung "
    "mit dem Connector (Builds ab dem 25.08.2026) ist installiert und wurde einmal gestartet."
)


class ConnectorError(Exception):
    pass


def run_shortcut(command: dict) -> str:
    """Reicht einen Befehl an den Kurzbefehl weiter und liefert dessen Antwort."""
    payload = json.dumps(command, ensure_ascii=False)
    os.makedirs(TMP, exist_ok=True)
    in_name = uuid.uuid4().hex + ".json"
    in_path = os.path.join(TMP, in_name)
    try:
        with open(in_path, "w", encoding="utf-8") as handle:
            handle.write(payload)
        try:
            result = subprocess.run(
                ["shortcuts", "run", SHORTCUT, "-i", in_name],
                capture_output=True, text=True, timeout=TIMEOUT, cwd=TMP,
            )
        except FileNotFoundError:
            raise ConnectorError("`shortcuts` nicht gefunden — das gibt es nur auf macOS.")
        except subprocess.TimeoutExpired:
            raise ConnectorError(
                f"Der Kurzbefehl hat nach {TIMEOUT} s nicht geantwortet. "
                "Läuft in der Kurzbefehle-App eine Rückfrage oder ein Dialog?"
            )
        if result.returncode != 0:
            detail = (result.stderr or result.stdout or "").strip()
            raise ConnectorError(f"{EINRICHTUNG}\n\nSystemmeldung: {detail}" if detail
                                 else EINRICHTUNG)
        answer = result.stdout.strip()
        if not answer:
            raise ConnectorError(
                "Der Kurzbefehl lief, gab aber nichts zurück — besteht er wirklich aus "
                "genau der einen Aktion „Hobba-Connector“, und ist der Parameter "
                "auf „Kurzbefehl-Eingabe“ gebunden?"
            )
        return answer
    finally:
        try:
            os.remove(in_path)
        except OSError:
            pass


# --- Werkzeuge -------------------------------------------------------------

def schema(properties: dict, required: list) -> dict:
    return {"type": "object", "properties": properties, "required": required}

ID = {"type": "string", "description": "UUID aus hobba_uebersicht"}
TAG = {"type": "string",
       "description": "„heute“ oder „JJJJ-MM-TT“"}

TOOLS = [
    {
        "name": "hobba_uebersicht",
        "description": (
            "Der Bestand, wie Hobba ihn zeigt: die Listen Heute, Eingang, "
            "Demnächst und Irgendwann sowie alle offenen Projekte mit ihren "
            "Abschnitten und Aufgaben (inkl. UUIDs für die anderen Werkzeuge)."
        ),
        "inputSchema": schema({}, []),
    },
    {
        "name": "hobba_aufgabe_anlegen",
        "description": (
            "Legt eine Aufgabe an. Der Text darf Datum, Uhrzeit, Frist und "
            "Wiederholung natürlich enthalten („Milch kaufen morgen 8 Uhr“, "
            "„Bericht bis Freitag“, „Müll jeden Dienstag“) — Hobba liest das "
            "selbst. Ohne projekt_id landet sie im Eingang."
        ),
        "inputSchema": schema({
            "text": {"type": "string", "description": "Titel, gern mit Zeitangaben im Text"},
            "projekt_id": ID,
            "abschnitt_id": ID,
            "notiz": {"type": "string"},
            "tag": TAG,
        }, ["text"]),
    },
    {
        "name": "hobba_aufgabe_erledigen",
        "description": "Hakt eine Aufgabe ab; bei Wiederholungen entsteht die nächste Runde.",
        "inputSchema": schema({"id": ID}, ["id"]),
    },
    {
        "name": "hobba_aufgabe_verschieben",
        "description": (
            "Verschiebt oder plant eine Aufgabe um. Nur mitgegebene Felder wirken: "
            "projekt_id (oder eingang=true für den Eingang), abschnitt_id, "
            "tag („heute“, „JJJJ-MM-TT“ oder „keiner“), irgendwann."
        ),
        "inputSchema": schema({
            "id": ID,
            "projekt_id": ID,
            "eingang": {"type": "boolean", "description": "true = aus dem Projekt in den Eingang"},
            "abschnitt_id": ID,
            "tag": {"type": "string", "description": "„heute“, „JJJJ-MM-TT“ oder „keiner“"},
            "irgendwann": {"type": "boolean"},
        }, ["id"]),
    },
    {
        "name": "hobba_projekt_anlegen",
        "description": (
            "Legt ein Projekt an — auf Wunsch samt Abschnitten und Aufgaben in einem "
            "Zug (Abschnitte sind zugleich die Brett-Spalten). Existiert der Name "
            "schon, wird das vorhandene Projekt weiterbefüllt. bereich = Abschnitt "
            "der Übersicht (z. B. „Arbeit“), nur wenn es ihn gibt."
        ),
        "inputSchema": schema({
            "name": {"type": "string"},
            "bereich": {"type": "string"},
            "aufgaben": {
                "type": "array",
                "items": schema({
                    "titel": {"type": "string"},
                    "abschnitt": {"type": "string",
                                  "description": "Abschnitts-/Spaltenname, entsteht bei Bedarf"},
                    "notiz": {"type": "string"},
                    "tag": TAG,
                }, ["titel"]),
            },
        }, ["name"]),
    },
    {
        "name": "hobba_abschnitt_anlegen",
        "description": "Legt einen Abschnitt (= Brett-Spalte) in einem Projekt an.",
        "inputSchema": schema({"projekt_id": ID, "name": {"type": "string"}},
                              ["projekt_id", "name"]),
    },
    {
        "name": "hobba_abschnitt_umbenennen",
        "description": "Benennt einen Abschnitt um.",
        "inputSchema": schema({"id": ID, "name": {"type": "string"}}, ["id", "name"]),
    },
]

BEFEHLE = {
    "hobba_uebersicht": "snapshot",
    "hobba_aufgabe_anlegen": "aufgabe_anlegen",
    "hobba_aufgabe_erledigen": "aufgabe_erledigen",
    "hobba_aufgabe_verschieben": "aufgabe_verschieben",
    "hobba_projekt_anlegen": "projekt_anlegen",
    "hobba_abschnitt_anlegen": "abschnitt_anlegen",
    "hobba_abschnitt_umbenennen": "abschnitt_umbenennen",
}


def call_tool(name: str, arguments: dict) -> str:
    befehl = BEFEHLE.get(name)
    if befehl is None:
        raise ConnectorError(f"Unbekanntes Werkzeug: {name}")
    command = {"befehl": befehl}
    command.update(arguments or {})
    return run_shortcut(command)


# --- MCP (stdio, zeilenweises JSON-RPC) ------------------------------------

def reply(request_id, result=None, error=None):
    message = {"jsonrpc": "2.0", "id": request_id}
    if error is not None:
        message["error"] = error
    else:
        message["result"] = result
    sys.stdout.write(json.dumps(message, ensure_ascii=False) + "\n")
    sys.stdout.flush()


def main():
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        try:
            request = json.loads(line)
        except json.JSONDecodeError:
            continue
        method = request.get("method", "")
        request_id = request.get("id")
        params = request.get("params") or {}

        if method == "initialize":
            reply(request_id, {
                "protocolVersion": params.get("protocolVersion", "2024-11-05"),
                "capabilities": {"tools": {}},
                "serverInfo": {"name": "hobba", "version": "1.0"},
            })
        elif method == "tools/list":
            reply(request_id, {"tools": TOOLS})
        elif method == "tools/call":
            try:
                answer = call_tool(params.get("name", ""), params.get("arguments") or {})
                parsed = None
                try:
                    parsed = json.loads(answer)
                except json.JSONDecodeError:
                    pass
                is_error = isinstance(parsed, dict) and "fehler" in parsed
                reply(request_id, {
                    "content": [{"type": "text", "text": answer}],
                    "isError": is_error,
                })
            except ConnectorError as exc:
                reply(request_id, {
                    "content": [{"type": "text", "text": str(exc)}],
                    "isError": True,
                })
        elif method == "ping":
            reply(request_id, {})
        elif request_id is not None:
            reply(request_id, error={"code": -32601, "message": f"Unbekannte Methode: {method}"})
        # Notifications (ohne id) brauchen keine Antwort.


if __name__ == "__main__":
    main()
