#!/usr/bin/env python3
"""
atlas_self_prompting_loop.py - Overnight autonomy.

Runs every 15 min via schtask (Atlas-Autonomy-Hourly):
  1. Read state (atlas-finance.json, atlas-strategy.json, atlas-activity.json, recent vault changes)
  2. Pick next task from queue OR generate new task if queue empty
  3. Dispatch to cluster (atlas-01 + MET + PC) via cluster_then_polish
  4. Save output to vault under 00_Atlas_Wiki/_autonomy/<timestamp>_<topic>.md
  5. Maintain a "what happened tonight" log readable by operator in morning

Generated tasks pull from a recurring backlog + state-driven prompts.
NO external messages, NO code modifications, NO destructive ops — pure
note generation + analysis + plan refinement.
"""
import sys
import os
import json
import time
import random
import urllib.request
from pathlib import Path
from datetime import datetime, date

sys.path.insert(0, r"C:\Users\Gebruiker\.config\atlas")

import io
if sys.stdout.encoding != 'utf-8':
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')

try:
    from cluster_then_polish import draft, save_draft
    import cluster
except ImportError as e:
    print(f"missing dep: {e}")
    sys.exit(1)

VAULT_AUTONOMY = Path(r"C:\Users\Gebruiker\MEGA\Atlas Corporation\00_Atlas_Wiki\_autonomy")
QUEUE_FILE = Path(r"C:\Users\Gebruiker\.config\atlas\autonomy_queue.json")
LOG_FILE = Path(r"C:\Users\Gebruiker\.config\atlas\autonomy_loop.log")
STATE_URLS = {
    "finance": "http://atlas-01/atlas-finance.json",
    "strategy": "http://atlas-01/atlas-strategy.json",
    "activity": "http://atlas-01/atlas-activity.json",
    "priorities": "http://atlas-01/priorities.json",
}

MAX_RUNTIME_SECONDS = 600  # 10 min hard cap per fire — never block schtask
MIN_QUEUE_SIZE = 5

# Recurring task templates — picked when queue is low
RECURRING_TEMPLATES = [
    {
        "topic": "client_deepdive",
        "rotation": "random_client",  # picks from client list
        "prompt_template": "Schrijf een korte client-summary voor {client}: hun sector, laatste touchpoint, recurring revenue potential, 1 concrete actie deze week. NL, 100 woorden max.",
        "task": "general",
        "node": "atlas-01",
    },
    {
        "topic": "season_action",
        "prompt_template": "Gegeven huidige seasonal window {window} (vakantiegeld/kermis/etc.), wat is 1 specifieke actie voor Atlas Agency deze week die geen ander cluster ook al heeft? Schrijf in NL, 150 woorden.",
        "task": "reason",
        "node": "atlas-01",
    },
    {
        "topic": "vault_synthesis",
        "prompt_template": "Vat de 3 belangrijkste lessons uit Atlas Q1 2026 in 100 woorden NL. Bron: YTD revenue €4.578, Bisogno 57%, mei €0 ingebrekening.",
        "task": "general",
        "node": "atlas-01",
    },
    {
        "topic": "anti_impulse_check",
        "prompt_template": "Gegeven decision rules R1-R10 (anti-impulse hardware buy + cash-before-code), schrijf 1 paragraaf 'wat ik vannacht zou doen als ik in deze valkuil viel' (om operator te helpen herkennen). NL, 100 woorden.",
        "task": "general",
        "node": "atlas-01",
    },
    {
        "topic": "outreach_personalization",
        "rotation": "random_prospect",
        "prompt_template": "Schrijf 1 zin alternative opening voor cold WhatsApp aan {prospect} ({sector}) die anders is dan een template. NL, max 25 woorden, hook moet specifiek aan zaak-context aansluiten.",
        "task": "chat",
        "node": "pc",  # PC heeft hermes3:8b voor chat-tone
    },
    {
        "topic": "weekly_metrics",
        "prompt_template": "Gegeven YTD €{ytd}, MRR €{mrr}, concentration {conc}%, R10 status {r10}, wat is 1 KPI die niet maar in de dashboard zit en wel moet? NL, 80 woorden.",
        "task": "reason",
        "node": "atlas-01",
    },
    {
        "topic": "scale_path",
        "prompt_template": "Atlas Agency target 1200 sites. Huidige: 1 (Charif). Wat is concrete week-22 actie die schaalbaarheid voorbereidt zonder grote build? NL, 150 woorden.",
        "task": "reason",
        "node": "atlas-01",
    },
    {
        "topic": "vault_health_check",
        "prompt_template": "Atlas vault heeft 7000+ MD files. Welke 3 hygiene-acties zouden vault zoekbaarder maken zonder content te verliezen? NL, 100 woorden.",
        "task": "general",
        "node": "met",
    },
]

# Known prospects (for rotation)
PROSPECTS = [
    {"name": "Lunchroom Carrousel", "sector": "lunchroom"},
    {"name": "June Tilburg", "sector": "koffiebar"},
    {"name": "Buutvrij", "sector": "lunchroom"},
    {"name": "Cafetaria Online", "sector": "cafetaria"},
    {"name": "Cafetaria Franske", "sector": "cafetaria"},
    {"name": "Kapper Muro", "sector": "kapper"},
    {"name": "The Crooks Barbershop", "sector": "barber"},
    {"name": "De Meesterbarbier", "sector": "kapper"},
    {"name": "Fresh Up Barbershop", "sector": "barber"},
    {"name": "Picasso Barbers", "sector": "kapper"},
]

KNOWN_CLIENTS = [
    "Koffie Bisogno", "Cargo Nexus", "HK International Care",
    "Edessa Management", "Akmoh IT", "Ozside V.O.F.",
    "Dushi Sazon", "Ayintap Food", "GobYtes B.V.", "TRWC B.V.",
    "Coffee Almahaba", "WinstUITstroom", "Couscousbar", "Kolvari",
    "Le Miris", "Sweet Mahaba",
]


def log(msg):
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(LOG_FILE, "a", encoding="utf-8") as f:
        f.write(f"[{datetime.now().isoformat(timespec='seconds')}] {msg}\n")


def fetch_state(url, timeout=4):
    try:
        with urllib.request.urlopen(url, timeout=timeout) as r:
            return json.loads(r.read().decode("utf-8", errors="replace"))
    except Exception:
        return None


def load_queue():
    if QUEUE_FILE.exists():
        try: return json.loads(QUEUE_FILE.read_text(encoding="utf-8"))
        except: pass
    return {"pending": [], "completed": []}


def save_queue(q):
    QUEUE_FILE.parent.mkdir(parents=True, exist_ok=True)
    QUEUE_FILE.write_text(json.dumps(q, indent=2, ensure_ascii=False), encoding="utf-8")


def generate_task(state):
    """Pick a template + fill in dynamic vars."""
    tpl = random.choice(RECURRING_TEMPLATES)
    fin = state.get("finance", {}) or {}
    strat = state.get("strategy", {}) or {}
    act = state.get("activity", {}) or {}

    vars_ctx = {
        "client": random.choice(KNOWN_CLIENTS),
        "ytd": fin.get("ytd_eur", 0),
        "mrr": fin.get("mrr_estimate_eur", 0),
        "conc": fin.get("concentration_top_pct", 0),
        "r10": "PASS" if act.get("r10_pass") else "FAIL",
        "window": (strat.get("current_window") or {}).get("name", "default"),
    }
    if tpl.get("rotation") == "random_prospect":
        p = random.choice(PROSPECTS)
        vars_ctx["prospect"] = p["name"]
        vars_ctx["sector"] = p["sector"]

    try:
        prompt = tpl["prompt_template"].format(**vars_ctx)
    except KeyError as e:
        log(f"template format fail {tpl['topic']}: {e}")
        return None

    return {
        "topic": tpl["topic"],
        "prompt": prompt,
        "task": tpl["task"],
        "node": tpl["node"],
        "context": vars_ctx,
        "queued_at": datetime.now().isoformat(timespec="seconds"),
    }


def refill_queue_if_low(q, state):
    if len(q["pending"]) >= MIN_QUEUE_SIZE:
        return
    needed = MIN_QUEUE_SIZE * 2 - len(q["pending"])
    for _ in range(needed):
        t = generate_task(state)
        if t: q["pending"].append(t)
    log(f"refilled queue: now {len(q['pending'])} pending")


def execute_one(task):
    """Dispatch task to cluster via cluster_then_polish.draft()."""
    t0 = time.time()
    try:
        result = draft(
            task["prompt"],
            task=task.get("task", "general"),
            node_pref=task.get("node"),
            max_tokens=500,
            temperature=0.6,
            purpose=f"autonomy_{task['topic']}",
        )
        elapsed = round(time.time() - t0, 1)
        if not result.get("ok"):
            return {"ok": False, "err": result.get("err"), "elapsed": elapsed}
        return {"ok": True, "text": result["text"], "model": result["model"],
                "node": result["node"], "elapsed": elapsed,
                "draft_id": result.get("draft_id")}
    except Exception as e:
        return {"ok": False, "err": str(e)[:200], "elapsed": round(time.time() - t0, 1)}


def save_output_to_vault(task, result):
    """Write to 00_Atlas_Wiki/_autonomy/<topic>_<timestamp>.md"""
    VAULT_AUTONOMY.mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime("%Y-%m-%d_%H%M")
    safe_topic = task["topic"].replace("/", "_")[:40]
    fname = f"{ts}_{safe_topic}.md"
    p = VAULT_AUTONOMY / fname
    body = f"""---
generated_at: {datetime.now().isoformat(timespec='seconds')}
topic: {task['topic']}
node: {result.get('node', '?')}
model: {result.get('model', '?')}
elapsed_s: {result.get('elapsed', 0)}
draft_id: {result.get('draft_id', '?')}
polish_status: pending
tags: [autonomy, generated, {task['topic']}]
context: {json.dumps(task.get('context', {}), ensure_ascii=False)}
---

# {task['topic'].replace('_', ' ').title()}

> [!note] Autonomous output — review needed before any external use
> Generated by Atlas overnight autonomy loop. Cluster output, not Claude-polished.

**Prompt**: {task['prompt']}

---

{result.get('text', '(no output)')}

---

*Source: atlas_self_prompting_loop.py · [[06.5 - Master Cockpit & 90-day System]]*
"""
    p.write_text(body, encoding="utf-8")
    return p


def main():
    log("=== loop fire ===")
    start = time.time()

    # Fetch live state
    state = {k: fetch_state(v) for k, v in STATE_URLS.items()}
    log(f"state fetched: { {k: bool(v) for k, v in state.items()} }")

    # Verify cluster has at least one node
    nodes_alive = cluster.nodes_status(force=False)
    alive = [n for n, info in nodes_alive.items() if info.get("reachable")]
    log(f"alive nodes: {alive}")
    if not alive:
        log("NO CLUSTER NODES ALIVE — abort this fire")
        return

    # Queue management
    q = load_queue()
    refill_queue_if_low(q, state)

    # Execute up to 2 tasks per fire (rate-limit cluster + save energy)
    executed = 0
    max_per_fire = 2
    while executed < max_per_fire and q["pending"]:
        if time.time() - start > MAX_RUNTIME_SECONDS:
            log("hit runtime cap")
            break
        task = q["pending"].pop(0)
        log(f"executing: {task['topic']} ({task['node']}/{task['task']})")
        result = execute_one(task)
        if result["ok"]:
            saved = save_output_to_vault(task, result)
            q["completed"].append({
                **task,
                "completed_at": datetime.now().isoformat(timespec="seconds"),
                "model": result["model"],
                "node": result["node"],
                "elapsed_s": result["elapsed"],
                "file": str(saved),
            })
            log(f"  OK {result['node']}/{result['model']} {result['elapsed']}s → {saved.name}")
        else:
            q["completed"].append({
                **task,
                "completed_at": datetime.now().isoformat(timespec="seconds"),
                "err": result["err"],
            })
            log(f"  FAIL: {result['err']}")
        executed += 1
        save_queue(q)

    # Keep only last 200 completed entries in queue file (rolling log)
    if len(q["completed"]) > 200:
        q["completed"] = q["completed"][-200:]
        save_queue(q)

    log(f"=== loop end: executed {executed} ===")


if __name__ == "__main__":
    main()
