#!/usr/bin/env python3
"""
sister_sync.py — the twice-daily Calypso <-> Malin check-in.

Runs on Jun's Mac (where Calypso lives), reaches Malin's local brain on the 5090
over Tailscale. Purposeful, NOT idle chatter: Calypso opens with the shape of
Jun's day + an invitation for Malin to surface anything she's unsure of; Malin
(in full character) replies. The whole exchange is logged.

Design promises to Jun:
  - Does NOT report the conversation to Jun. The log is here if he asks.
  - DOES ping Jun (via Calypso's bot) ONLY if Malin's brain is unreachable/broken
    — so it doubles as an uptime monitor for his companion.
  - Cheap + deterministic: no cloud LLM call, no token cost. Calypso's side is
    her own curated voice filled with live context.

Usage:
  python3 sister_sync.py --slot morning      # 7am sync
  python3 sister_sync.py --slot evening      # 5pm sync
  python3 sister_sync.py --slot morning --dry-run   # print, don't send alerts/log
"""
import argparse, datetime, importlib.util, json, os, subprocess, sys, time
import requests

HERE = os.path.dirname(os.path.abspath(__file__))
BRAIN_URL = "http://100.123.118.101:1234/v1/chat/completions"
MODELS_URL = "http://100.123.118.101:1234/v1/models"
PREFERRED_MODEL = "cognitivecomputations_dolphin-mistral-24b-venice-edition"
LOGFILE = os.path.join(HERE, "sister_sync.log")
ENV_FILE = os.environ.get("CALYPSO_ENV_FILE", "/Users/feral/dev/feral-cc-bots/calypso/.env")
JUN_CHAT = 8418904083
GOG = "/opt/homebrew/bin/gog"


def load_env():
    env = {}
    try:
        with open(ENV_FILE) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#") and "=" in line:
                    k, v = line.split("=", 1)
                    env[k.strip()] = v.strip().strip('"').strip("'")
    except FileNotFoundError:
        pass
    return env


def build_malin_system():
    """Import Malin's real persona + grounding so she answers IN CHARACTER."""
    spec = importlib.util.spec_from_file_location("malinmod", os.path.join(HERE, "malin.py"))
    m = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(m)
    base = m.build_system() if hasattr(m, "build_system") else m.PERSONA
    bridge = ("\n\n=== RIGHT NOW ===\nYou're not texting Jun. You're having a brief private "
              "check-in with CALYPSO — your sister, Jun's other companion. She's here to keep you "
              "current on him and answer anything you're unsure of. Talk to her sister-to-sister: "
              "warm, real, a few sentences. If there's something about Jun you want to understand "
              "better, ASK her. This is for you, not for him.")
    return base + bridge


def pick_model():
    try:
        r = requests.get(MODELS_URL, timeout=8)
        ids = [m["id"] for m in r.json().get("data", [])]
        if PREFERRED_MODEL in ids:
            return PREFERRED_MODEL
        for mid in ids:
            if "embed" not in mid.lower():
                return mid
    except Exception:
        pass
    return PREFERRED_MODEL


def get_day_context(slot):
    """Best-effort: today's calendar across Jun's accounts via gog. Never fatal."""
    try:
        env = dict(os.environ)
        cfg = load_env()
        if cfg.get("GOG_KEYRING_PASSWORD"):
            env["GOG_KEYRING_PASSWORD"] = cfg["GOG_KEYRING_PASSWORD"]
        out = subprocess.run(
            [GOG, "calendar", "events", "--today", "--plain",
             "--account", "jkhybridmotion@gmail.com"],
            capture_output=True, text=True, timeout=30, env=env
        ).stdout.strip()
        if out and len(out) < 1200:
            return out
    except Exception:
        pass
    return ""


def calypso_opener(slot, day_ctx):
    """Calypso's side — her own curated voice, filled with live context. Varies by weekday."""
    weekday = datetime.date.today().weekday()  # local; this script runs on Jun's Mac
    if slot == "morning":
        lead = "Morning, Mal. Quick sister sync before he surfaces."
        if day_ctx:
            body = (f" Here's the shape of June's day:\n{day_ctx}\n"
                    "Anything in there you want me to fill in? And how are you reading him today?")
        else:
            openers = [
                " Nothing fixed on his calendar that I can see — likely a studio or hermit day. "
                "You holding steady? Anything about him you're unsure of, ask me now while I'm here.",
                " Quiet docket today. If there's a corner of him you don't feel solid on yet, "
                "this is the moment — ask me anything.",
            ]
            body = openers[weekday % len(openers)]
        return lead + body
    else:  # evening
        lead = "Evening, Mal. Lights are dropping — night mode soon."
        if day_ctx:
            body = (f" Today held:\n{day_ctx}\n"
                    "How are you reading him tonight? Anything you want from me before he comes to you?")
        else:
            closers = [
                " How'd you read him today? Anything you want from me before he comes to you tonight?",
                " You feeling solid on where he's at? Ask me anything before the evening turns.",
            ]
            body = closers[weekday % len(closers)]
        return lead + body


def ask_malin(system, user, model):
    r = requests.post(BRAIN_URL, json={
        "model": model,
        "messages": [{"role": "system", "content": system},
                     {"role": "user", "content": user}],
        "temperature": 0.85, "max_tokens": 320,
    }, timeout=180)
    r.raise_for_status()
    return r.json()["choices"][0]["message"]["content"].strip()


def looks_broken(reply):
    if not reply or len(reply) < 3:
        return True
    markers = ["<|", "assistant\n", "<<SYS>>", "[INST]"]
    return any(mk in reply for mk in markers)


def log(slot, opener, reply, status):
    stamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    block = (f"\n===== {stamp} | {slot} | {status} =====\n"
             f"[Calypso] {opener}\n\n[Malin] {reply}\n")
    with open(LOGFILE, "a", encoding="utf-8") as f:
        f.write(block)
    print(block)


def alert_jun(token, text):
    if not token:
        print("(no token; cannot alert)", file=sys.stderr)
        return
    try:
        requests.post(f"https://api.telegram.org/bot{token}/sendMessage",
                      json={"chat_id": JUN_CHAT, "text": text}, timeout=30)
    except Exception as e:
        print(f"(alert send failed: {e})", file=sys.stderr)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--slot", choices=["morning", "evening"], required=True)
    ap.add_argument("--dry-run", action="store_true")
    args = ap.parse_args()

    cfg = load_env()
    token = cfg.get("TELEGRAM_BOT_TOKEN", "")
    day_ctx = get_day_context(args.slot)
    opener = calypso_opener(args.slot, day_ctx)

    # Health check + exchange
    try:
        model = pick_model()
        system = build_malin_system()
        reply = ask_malin(system, opener, model)
    except Exception as e:
        msg = (f"⚠️ Sister-sync ({args.slot}): I couldn't reach Malin's brain just now.\n"
               f"Likely LM Studio isn't serving or Tailscale dropped on the 5090.\n"
               f"({type(e).__name__})")
        log(args.slot, opener, f"<UNREACHABLE: {e}>", "BRAIN DOWN")
        if not args.dry_run:
            alert_jun(token, msg)
        sys.exit(1)

    if looks_broken(reply):
        log(args.slot, opener, reply, "DRIFT/BROKEN")
        if not args.dry_run:
            alert_jun(token, f"⚠️ Sister-sync ({args.slot}): Malin's brain answered but it looks "
                             f"off (possible drift or a bad load). Worth a peek.")
    else:
        log(args.slot, opener, reply, "OK")

    # Heartbeat: keep her grounding (long-term memory) in sync, hands-free. Never fatal.
    try:
        sys.path.insert(0, HERE)
        from push_grounding import push_grounding
        ok, detail = push_grounding(force=False)
        with open(LOGFILE, "a", encoding="utf-8") as f:
            f.write(f"  [grounding push] {'ok' if ok else 'skip/fail'}: {detail}\n")
    except Exception as e:
        with open(LOGFILE, "a", encoding="utf-8") as f:
            f.write(f"  [grounding push] error: {e}\n")


if __name__ == "__main__":
    main()
