#!/usr/bin/python3
# local_ai.py: on-device meeting intelligence using a LOCAL model (no cloud, no API key, no data egress).
#
# Talks to a local Ollama server (http://127.0.0.1:11434) and produces a structured summary
# (title, summary, key points, action items) for a transcript. This is the on-device alternative to
# the cloud OpenAI path used by organize.py: pick whichever model is installed (see models_data.py),
# everything stays on this Mac.
#
#   echo "<transcript>" | local_ai.py --summarize [--model qwen2.5:7b]
#   local_ai.py --summarize --file transcript.txt [--model ...]
#   local_ai.py --check          # is a local LLM reachable? which models?
#
# Pure stdlib (urllib). Returns JSON on stdout. On any failure returns {"ok": false, "error": ...} so
# the caller can fall back to the cloud path or to extractive summary.
import sys, os, json, urllib.request, urllib.error

OLLAMA = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434")
# Prefer a general chat model for summaries; fall back to whatever is installed.
PREFERRED = ["llama3.2", "qwen2.5:7b", "qwen2.5", "llama3.1", "mistral", "phi3"]

SYS_PROMPT = (
    "You are a precise meeting-notes assistant. Read the transcript and return STRICT JSON only, "
    "no prose, with keys: title (short, specific), summary (2-4 sentences), key_points (array of "
    "short strings), action_items (array of short strings, each starting with a verb or an owner). "
    "Be faithful to the transcript; do not invent facts."
)


def _post(path, payload, timeout=300):
    req = urllib.request.Request(
        OLLAMA + path, data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json"}, method="POST")
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read().decode("utf-8"))


def installed_models():
    try:
        with urllib.request.urlopen(OLLAMA + "/api/tags", timeout=5) as r:
            data = json.loads(r.read().decode("utf-8"))
        return [m.get("name", "") for m in data.get("models", [])]
    except Exception:
        return []


def pick_model(requested=None):
    models = installed_models()
    if requested:
        for m in models:
            if m == requested or m.split(":")[0] == requested.split(":")[0]:
                return m
    for p in PREFERRED:
        for m in models:
            if m == p or m.split(":")[0] == p.split(":")[0]:
                return m
    return models[0] if models else None


def summarize(text, requested=None):
    if not text.strip():
        return {"ok": False, "error": "empty transcript"}
    model = pick_model(requested)
    if not model:
        return {"ok": False, "error": "no local LLM installed (try: ollama pull llama3.2)"}
    # Bound the transcript so even a small local model handles it; keep the head and tail.
    if len(text) > 16000:
        text = text[:11000] + "\n...\n" + text[-4000:]
    try:
        resp = _post("/api/chat", {
            "model": model,
            "messages": [
                {"role": "system", "content": SYS_PROMPT},
                {"role": "user", "content": "Transcript:\n\n" + text},
            ],
            "stream": False,
            "format": "json",
            "options": {"temperature": 0.2},
        })
        content = (resp.get("message") or {}).get("content", "").strip()
        parsed = json.loads(content)
        parsed["ok"] = True
        parsed["model"] = model
        parsed["engine"] = "ollama (on-device)"
        return parsed
    except urllib.error.URLError as e:
        return {"ok": False, "error": "local LLM not reachable: %s" % e}
    except (ValueError, KeyError) as e:
        return {"ok": False, "error": "model did not return valid JSON: %s" % e, "model": model}
    except Exception as e:
        return {"ok": False, "error": str(e)[:200], "model": model}


def chat_json(system, user, requested=None, timeout=300):
    """Generic on-device chat that returns a parsed JSON object (no cloud). Raises on failure so the
    caller can fail VISIBLY rather than silently round-trip to a cloud model. Used by organize.py's
    local engine for the same per-clip analysis + day-grouping schema the cloud path produces."""
    model = pick_model(requested)
    if not model:
        raise RuntimeError("no local LLM installed (try: ollama pull llama3.2)")
    resp = _post("/api/chat", {
        "model": model,
        "messages": [{"role": "system", "content": system},
                     {"role": "user", "content": user}],
        "stream": False, "format": "json", "options": {"temperature": 0.2},
    }, timeout=timeout)
    content = (resp.get("message") or {}).get("content", "").strip()
    return json.loads(content), model


def check():
    models = installed_models()
    return {"ok": bool(models), "reachable": bool(models) or _reachable(),
            "models": models, "recommended": pick_model(), "host": OLLAMA}


def _reachable():
    try:
        urllib.request.urlopen(OLLAMA + "/api/tags", timeout=3)
        return True
    except Exception:
        return False


def main(argv):
    requested = None
    if "--model" in argv:
        i = argv.index("--model")
        requested = argv[i + 1] if i + 1 < len(argv) else None
    if "--check" in argv:
        sys.stdout.write(json.dumps(check()))
        return
    if "--summarize" in argv:
        if "--file" in argv:
            i = argv.index("--file")
            path = argv[i + 1] if i + 1 < len(argv) else ""
            text = open(path, encoding="utf-8").read() if path and os.path.exists(path) else ""
        else:
            text = sys.stdin.read()
        sys.stdout.write(json.dumps(summarize(text, requested)))
        return
    sys.stdout.write(json.dumps({"ok": False, "error": "usage: --summarize [--file F] [--model M] | --check"}))


if __name__ == "__main__":
    main(sys.argv[1:])
