#!/usr/bin/env python3
"""PageHawk tiered text-generation backend — the single seam Smart Rename / Organize
call to turn a document's text into a proposed name/folder/tags.

Tiers, in order (Jason 2026-07-12 — self-contained by default, no multi-GB download):
  1. Apple Foundation Models (DEFAULT) — the on-device ~3B Apple-Intelligence model via
     the bundled `phk-fm` helper. Zero download beyond the OS, free, fully private.
  2. BYO API key (OpenAI / Anthropic / Gemini) — a Settings field; the quality upgrade
     and the fallback on Macs without Apple-Intelligence hardware.
  3. Ollama — the existing local path (pagehawk_ai), kept optional, never required.

Backend choice: config `smart_rename_backend` in ~/Library/Application Support/PageHawk/
config.json (or env PHK_SMART_RENAME_BACKEND) ∈ {auto, foundation, apikey, ollama};
default "auto" tries Foundation → API key → Ollama and uses the first that answers.
Everything is stdlib-only so it runs under the app's /usr/bin/python3.
"""
import os, json, subprocess, urllib.request

_HERE = os.path.dirname(os.path.abspath(__file__))
_FM_BIN = os.path.join(_HERE, "phk-fm")
_CONFIG = os.path.expanduser("~/Library/Application Support/PageHawk/config.json")

_DEFAULT_MODELS = {
    "openai": "gpt-4o-mini",
    "anthropic": "claude-3-5-haiku-latest",
    "gemini": "gemini-2.0-flash",
}


# ---------------------------------------------------------------- config
def _cfg():
    try:
        with open(_CONFIG, "r", encoding="utf-8") as f:
            return json.load(f) or {}
    except Exception:
        return {}


def _api_key():
    c = _cfg()
    key = (os.environ.get("PHK_SMART_RENAME_API_KEY")
           or c.get("smart_rename_api_key") or "").strip()
    provider = (c.get("smart_rename_provider") or "").strip().lower()
    if key and not provider:
        provider = _infer_provider(key)
    return key, provider


def _infer_provider(key):
    if key.startswith("sk-ant"):
        return "anthropic"
    if key.startswith("AIza"):
        return "gemini"
    if key.startswith("sk-"):
        return "openai"
    return "openai"


def _resolve_order():
    choice = (os.environ.get("PHK_SMART_RENAME_BACKEND")
              or _cfg().get("smart_rename_backend") or "auto").strip().lower()
    if choice in ("foundation", "apikey", "ollama"):
        return [choice]
    return ["foundation", "apikey", "ollama"]   # auto


# ---------------------------------------------------------------- Apple Foundation Models
def fm_available():
    return os.path.exists(_FM_BIN) and os.access(_FM_BIN, os.X_OK)


def _fm_generate(prompt, instructions, timeout):
    args = [_FM_BIN]
    if instructions:
        args.append(instructions)
    r = subprocess.run(args, input=prompt.encode("utf-8"),
                       stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
    if r.returncode != 0:
        raise RuntimeError("phk-fm exit %d: %s"
                           % (r.returncode, r.stderr.decode("utf-8", "ignore")[:200]))
    out = r.stdout.decode("utf-8", "ignore").strip()
    if not out:
        raise RuntimeError("phk-fm returned nothing")
    return out


# ---------------------------------------------------------------- BYO API key
def _apikey_generate(prompt, instructions, key, provider, timeout):
    model = (_cfg().get("smart_rename_model") or _DEFAULT_MODELS.get(provider, "gpt-4o-mini")).strip()
    sysmsg = instructions or "You are a precise document-filing assistant. Return only what is asked."
    if provider == "anthropic":
        req = urllib.request.Request(
            "https://api.anthropic.com/v1/messages",
            data=json.dumps({"model": model, "max_tokens": 400, "temperature": 0,
                             "system": sysmsg,
                             "messages": [{"role": "user", "content": prompt}]}).encode(),
            headers={"content-type": "application/json", "x-api-key": key,
                     "anthropic-version": "2023-06-01"})
        d = json.loads(urllib.request.urlopen(req, timeout=timeout).read().decode("utf-8", "ignore"))
        parts = d.get("content") or []
        return "".join(p.get("text", "") for p in parts if isinstance(p, dict)).strip()
    if provider == "gemini":
        url = ("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s"
               % (model, key))
        req = urllib.request.Request(
            url,
            data=json.dumps({"system_instruction": {"parts": [{"text": sysmsg}]},
                             "contents": [{"parts": [{"text": prompt}]}],
                             "generationConfig": {"temperature": 0}}).encode(),
            headers={"content-type": "application/json"})
        d = json.loads(urllib.request.urlopen(req, timeout=timeout).read().decode("utf-8", "ignore"))
        cand = (d.get("candidates") or [{}])[0]
        parts = (cand.get("content") or {}).get("parts") or []
        return "".join(p.get("text", "") for p in parts if isinstance(p, dict)).strip()
    # default: OpenAI chat/completions
    req = urllib.request.Request(
        "https://api.openai.com/v1/chat/completions",
        data=json.dumps({"model": model, "temperature": 0,
                         "messages": [{"role": "system", "content": sysmsg},
                                      {"role": "user", "content": prompt}]}).encode(),
        headers={"content-type": "application/json", "authorization": "Bearer " + key})
    d = json.loads(urllib.request.urlopen(req, timeout=timeout).read().decode("utf-8", "ignore"))
    return (d.get("choices") or [{}])[0].get("message", {}).get("content", "").strip()


# ---------------------------------------------------------------- Ollama
def _ollama_generate(prompt, num_predict, temperature, want_json, timeout):
    import pagehawk_ai as ai
    model = ai.pick_model()
    if not model:
        raise RuntimeError("ollama not running")
    return ai._generate(prompt, model, num_predict=num_predict, temperature=temperature,
                        fmt=("json" if want_json else None), timeout=timeout)


# ---------------------------------------------------------------- public API
def generate(prompt, instructions=None, want_json=False,
             num_predict=400, temperature=0.0, timeout=120):
    """Return the model's completion text, trying backends in the resolved order.
    Raises RuntimeError only if every eligible backend fails."""
    errs = []
    for b in _resolve_order():
        try:
            if b == "foundation":
                if not fm_available():
                    errs.append("foundation: helper not bundled"); continue
                return _fm_generate(prompt, instructions, timeout=min(timeout, 90))
            if b == "apikey":
                key, provider = _api_key()
                if not key:
                    errs.append("apikey: no key configured"); continue
                return _apikey_generate(prompt, instructions, key, provider, timeout=min(timeout, 60))
            if b == "ollama":
                return _ollama_generate(prompt, num_predict, temperature, want_json, timeout)
        except Exception as e:
            errs.append("%s: %s" % (b, e))
    raise RuntimeError("no Smart Rename backend answered (" + " | ".join(errs) + ")")


def status():
    """What backends are ready, for the UI / diagnostics. running=True if any answers."""
    key, provider = _api_key()
    ol = False
    try:
        import pagehawk_ai as ai
        ol = bool(ai.pick_model())
    except Exception:
        ol = False
    fm = fm_available()
    order = _resolve_order()
    running = (("foundation" in order and fm) or ("apikey" in order and bool(key))
               or ("ollama" in order and ol))
    if "foundation" in order and fm:
        label = "On-device (Apple Intelligence)"
    elif "apikey" in order and key:
        label = "Your %s key" % (provider or "API")
    elif "ollama" in order and ol:
        label = "Ollama (local)"
    else:
        label = "No AI backend available"
    return {"running": bool(running), "backend": order[0] if len(order) == 1 else "auto",
            "foundation": fm, "apikey": bool(key), "provider": provider,
            "ollama": ol, "label": label}


if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1 and sys.argv[1] == "status":
        print(json.dumps(status()))
    else:
        print(generate(sys.stdin.read()))
