#!/usr/bin/env python3
"""
cluster_then_polish.py — two-pass LLM pattern wrapper.

Wraps cluster.ask() with explicit "this is a draft, mark for human/Claude polish"
metadata. Goal: cluster does the heavy lifting (parallel, cheap, always-on),
Claude or human review handles the final polish.

Why: open-source 7B models can produce structured / classified / extracted output
reliably, but produce generic narrative text without grounding. Letting them
"finish" content sent to clients = quality loss. This wrapper bakes in the
two-pass expectation: cluster draft → human/Claude approval before any external use.

Public API:
    draft(prompt, task='general', node_pref=None, **kw) -> {ok, text, model, node, polish_required: True, draft_id, hint}
    save_draft(draft, path) -> writes md with frontmatter
    mark_polished(draft_id) -> updates draft registry

Usage example:
    from cluster_then_polish import draft, save_draft
    d = draft("Schrijf cold-WhatsApp voor lunchroom in Tilburg", task='chat', node_pref='pc')
    save_draft(d, '~/.config/atlas/drafts/lunchroom_outreach.md')
    # User/Claude reviews + edits before sending

Registry: drafts logged to ~/.config/atlas/drafts/_registry.json with
status (draft/polished/sent), creation time, node/model used, prompt hash.
"""
from __future__ import annotations
import sys, json, hashlib, time
from pathlib import Path
from datetime import datetime

HOME = Path.home()
ATLAS = HOME / '.config' / 'atlas'
DRAFTS_DIR = ATLAS / 'drafts'
REGISTRY = DRAFTS_DIR / '_registry.json'

sys.path.insert(0, str(ATLAS))
try:
    import cluster
except ImportError:
    raise RuntimeError("cluster.py not importable; check ~/.config/atlas/cluster.py")


# Task types that should ALWAYS go through polish (narrative, klant-facing)
POLISH_REQUIRED_TASKS = {'chat', 'reason', 'general'}

# Task types where cluster output is directly usable (structured, classified)
POLISH_OPTIONAL_TASKS = {'fast', 'embed', 'code', 'vision'}


def _hash_prompt(prompt: str) -> str:
    return hashlib.sha1(prompt.encode('utf-8')).hexdigest()[:10]


def _load_registry() -> dict:
    DRAFTS_DIR.mkdir(parents=True, exist_ok=True)
    if REGISTRY.exists():
        try:
            return json.loads(REGISTRY.read_text(encoding='utf-8'))
        except Exception:
            pass
    return {"drafts": {}}


def _save_registry(reg: dict) -> None:
    REGISTRY.write_text(json.dumps(reg, indent=2), encoding='utf-8')


def draft(prompt: str, task: str = 'general', node_pref: str | None = None,
          max_tokens: int = 800, temperature: float = 0.5, timeout: int = 180,
          purpose: str = '') -> dict:
    """
    Call cluster.ask() but tag the result as draft requiring polish.

    :param purpose: short label (e.g. 'cold_outreach', 'morning_brief', 'mail_triage')
                    used for the draft filename and routing decisions
    """
    t0 = time.time()
    r = cluster.ask(prompt, task=task, node_pref=node_pref,
                    max_tokens=max_tokens, temperature=temperature, timeout=timeout)
    elapsed = round(time.time() - t0, 1)

    if not r.get('ok'):
        return {
            "ok": False,
            "err": r.get('err', 'cluster error'),
            "polish_required": True,
            "hint": "cluster call failed; do not use any partial output"
        }

    draft_id = f"{int(time.time())}-{_hash_prompt(prompt)}"
    polish_required = task in POLISH_REQUIRED_TASKS

    result = {
        "ok": True,
        "draft_id": draft_id,
        "text": r['text'],
        "model": r.get('model', '?'),
        "node": r.get('node', '?'),
        "elapsed_s": elapsed,
        "task": task,
        "purpose": purpose,
        "polish_required": polish_required,
        "polish_status": "pending" if polish_required else "not_required",
        "created_at": datetime.now().isoformat(timespec='seconds'),
        "hint": (
            "POLISH REQUIRED before external use. Cluster 7B models produce "
            "generic narrative; review for: specificity, sector-fit, tone, "
            "factual claims. Then call mark_polished(draft_id)."
            if polish_required else
            "Structured output, safe to use as-is for triage/classify. "
            "Manual spot-check still recommended."
        )
    }

    # Register
    reg = _load_registry()
    reg['drafts'][draft_id] = {
        "purpose": purpose,
        "task": task,
        "node": result['node'],
        "model": result['model'],
        "created_at": result['created_at'],
        "polish_status": result['polish_status'],
        "prompt_hash": _hash_prompt(prompt),
        "char_count": len(r['text']),
    }
    _save_registry(reg)

    return result


def save_draft(draft_result: dict, path: str | Path) -> Path:
    """Save draft to .md with frontmatter so reviewer knows status."""
    p = Path(path).expanduser()
    p.parent.mkdir(parents=True, exist_ok=True)

    fm = (
        f"---\n"
        f"draft_id: {draft_result.get('draft_id', '?')}\n"
        f"polish_status: {draft_result.get('polish_status', '?')}\n"
        f"node: {draft_result.get('node', '?')}\n"
        f"model: {draft_result.get('model', '?')}\n"
        f"task: {draft_result.get('task', '?')}\n"
        f"purpose: {draft_result.get('purpose', '?')}\n"
        f"created_at: {draft_result.get('created_at', '?')}\n"
        f"polish_required: {draft_result.get('polish_required', False)}\n"
        f"---\n\n"
        f"> **HINT**: {draft_result.get('hint', '')}\n\n"
    )
    p.write_text(fm + draft_result.get('text', ''), encoding='utf-8')
    return p


def mark_polished(draft_id: str, polished_by: str = 'human') -> bool:
    """Update registry to indicate draft has been reviewed/polished."""
    reg = _load_registry()
    if draft_id in reg.get('drafts', {}):
        reg['drafts'][draft_id]['polish_status'] = 'polished'
        reg['drafts'][draft_id]['polished_by'] = polished_by
        reg['drafts'][draft_id]['polished_at'] = datetime.now().isoformat(timespec='seconds')
        _save_registry(reg)
        return True
    return False


def list_pending(purpose_filter: str = '') -> list:
    """Return list of drafts still awaiting polish (polish_status='pending')."""
    reg = _load_registry()
    out = []
    for did, info in reg.get('drafts', {}).items():
        if info.get('polish_status') != 'pending':
            continue
        if purpose_filter and purpose_filter not in info.get('purpose', ''):
            continue
        out.append({"draft_id": did, **info})
    return sorted(out, key=lambda x: x.get('created_at', ''), reverse=True)


if __name__ == '__main__':
    import argparse
    ap = argparse.ArgumentParser()
    ap.add_argument('prompt', nargs='?', help='Prompt text')
    ap.add_argument('-task', default='general')
    ap.add_argument('-node', default=None)
    ap.add_argument('-purpose', default='ad_hoc')
    ap.add_argument('-out', default=None, help='Save draft to this path')
    ap.add_argument('--list-pending', action='store_true')
    args = ap.parse_args()

    if args.list_pending:
        for d in list_pending():
            print(f"{d['draft_id']}  [{d['purpose']}]  {d['model']}@{d['node']}  {d['char_count']}ch  {d['created_at']}")
        sys.exit(0)

    if not args.prompt:
        print("usage: cluster_then_polish.py 'prompt' [-task T] [-node N] [-purpose P] [-out FILE]")
        sys.exit(1)

    d = draft(args.prompt, task=args.task, node_pref=args.node, purpose=args.purpose)
    if not d['ok']:
        print(f"ERROR: {d.get('err')}")
        sys.exit(1)

    print(f"[{d['node']}/{d['model']} {d['elapsed_s']}s] draft_id={d['draft_id']}")
    print(f"polish_status: {d['polish_status']}")
    print(f"--- text ({len(d['text'])} chars) ---")
    print(d['text'])
    print()

    if args.out:
        p = save_draft(d, args.out)
        print(f"saved -> {p}")
