#!/usr/bin/env python3
"""
Team Digest — per-member morning briefs ("others" audience)
===========================================================
Each member of the owner's team (an assistant, a chief of staff, …) can get their own
morning email — their delegated tasks, what the owner is waiting on, and (optionally) an
area of the owner's they can pitch in on — addressed to them, scheduled in THEIR timezone.

The roster is data, not code: it lives in the Config note **Agent/Roster.md** (one
`## <key>` section per recipient — name, owner-tags, help-area, timezone, send hour, reply
on/off, prompt note). Adding a member is a note edit; no code change. See load_members().

Thin by design: reuses the Core digest engine (digest.py) wholesale — the per-area TODO
parser (`collect_todos`), the date bucketer (`classify`), ownership (`is_mine`), the model,
the agent identity, and `send_email`. It adds only the per-member ownership filter, the
member-shaped payload, the (editable) system prompt, and one Claude call. Scheduling + the
once/day gate live in the automation runner, which fires each member in their own tz window.

Usage:
    python3 team_digest.py                 # send every configured member's brief
    python3 team_digest.py --print         # render + print each (no send)
    python3 team_digest.py --dry-run       # print each member's payload (no Claude/send)
    python3 team_digest.py --member <key>  # just one
"""
import os
import re
import sys
import json
import argparse
import datetime as dt
from pathlib import Path

from alexandria_vault.engine import consume_vault_arg, load_fresh
from alexandria_vault.models import AnthropicProvider

# `--vault` must land in VAULT_DIR before any import-time config/vault resolution.
consume_vault_arg()

# Member tasks in these buckets are what the owner is actively waiting on → lead the email.
NEEDS_NOW = {"overdue", "due_today", "check_due", "due_soon"}

MEMBER_DEFAULTS = {
    "name": "", "role": "", "email_env": "", "email": "", "owners": "",
    "help_area": "", "timezone": "America/New_York", "hour": "9",
    "reply": "false", "prompt_note": "Team Digest Prompt", "from_name": "",
}


def _load_digest():
    return load_fresh("digest")


def _load_state():
    return load_fresh("team_state")


# ---- roster (Config-note driven) -------------------------------------------
def _truthy(v):
    return str(v).strip().lower() in ("1", "true", "yes", "on")


def load_members(vault, roster_note="Roster.md"):
    """Parse the Config roster note into resolved member dicts. Each `## <key>` section is a
    member; `- field: value` bullets set fields (see MEMBER_DEFAULTS / the note's table). A
    member is INCLUDED only if it declares `owners` (so doc/example sections aren't mistaken
    for members) AND its address resolves (inline `email`, else `email_env` from the
    environment) — an entry with an unset email is silently skipped (dormant until
    configured). `owners` is split to a list; `hour`->int; `reply`->bool."""
    path = vault / "Agent" / roster_note
    try:
        text = path.read_text(encoding="utf-8")
    except OSError:
        return []
    raw, cur = [], None
    for line in text.splitlines():
        h = re.match(r"^##\s+(.+?)\s*$", line)
        if h:
            if cur:
                raw.append(cur)
            cur = dict(MEMBER_DEFAULTS, key=h.group(1).strip())
            continue
        if cur is None:
            continue
        m = re.match(r"^\s*-\s*([a-zA-Z_]+)\s*:\s*(.+?)\s*$", line)
        if m and m.group(1) in cur:
            cur[m.group(1)] = m.group(2).strip()
    if cur:
        raw.append(cur)
    d = _load_digest()
    default_from = os.environ.get("DIGEST_FROM_NAME") or d._agent_name()
    out = []
    for mb in raw:
        owners = [o.strip() for o in mb["owners"].split(",") if o.strip()]
        if not owners:
            continue                                    # not a member section
        email = mb["email"].strip() or os.environ.get(mb["email_env"].strip(), "").strip()
        if not email:
            continue                                    # dormant — address not configured
        try:
            hour = int(mb["hour"])
        except (TypeError, ValueError):
            hour = 9
        out.append({
            "key": mb["key"], "name": mb["name"] or mb["key"], "role": mb["role"],
            "email": email, "owners": owners, "help_area": mb["help_area"] or None,
            "timezone": mb["timezone"] or "America/New_York", "hour": hour,
            "reply": _truthy(mb["reply"]),
            "prompt_note": mb["prompt_note"] or "Team Digest Prompt",
            "from_name": mb["from_name"] or default_from,
        })
    return out


def find_member(vault, key):
    for mb in load_members(vault):
        if mb["key"] == key:
            return mb
    return None


# ---- payload ---------------------------------------------------------------
def _owned_by(it, owners_lower):
    for o in it.get("owners") or []:
        n = o.strip().lower()
        if n in owners_lower or any(n.startswith(x) for x in owners_lower):
            return True
    return False


def _slim(it, bucket):
    return {"area": it.get("area"), "text": it.get("text"), "context": it.get("context"),
            "due": it.get("due"), "check": it.get("check"), "tier": it.get("tier"),
            "bucket": bucket}


def build_payload(member, today):
    """The member's payload: their delegated items split by urgency (needs_now vs
    your_tasks), plus every open todo in their `help_area` (where they can help). Reuses
    digest.classify; `today` is pinned onto the freshly-loaded digest module so
    overdue/due-soon is computed against the member's own date, not this machine's clock."""
    d = _load_digest()
    d.TODAY = today
    todos = d.collect_todos()
    owners_lower = {o.lower() for o in member["owners"]}
    mine = [it for it in todos if _owned_by(it, owners_lower)]
    needs_now, your_tasks = [], []
    for it in sorted(mine, key=d._sortkey):
        bucket = d.classify(it)
        (needs_now if bucket in NEEDS_NOW else your_tasks).append(_slim(it, bucket))
    help_items = []
    if member.get("help_area"):
        help_items = [_slim(it, d.classify(it)) for it in sorted(
            (it for it in todos
             if (it.get("area") or "").strip() == member["help_area"] and d.is_mine(it)),
            key=d._sortkey)]
    return {
        "date": today.strftime("%A, %B ") + d.ordinal(today.day) + today.strftime(", %Y"),
        "recipient": member["name"], "role": member.get("role", ""),
        "help_area": member.get("help_area"),
        "needs_now": needs_now, "your_tasks": your_tasks, "help_items": help_items,
        "counts": {"member_total": len(mine), "needs_now": len(needs_now),
                   "help": len(help_items)},
    }


# ---- system prompt (editable note + fallback) ------------------------------
_DEFAULT_TEAM_SYSTEM = """You are {agent}, the owner's chief of staff. Write the MORNING BRIEF for {recipient} ({role}), a member of the owner's team, as a complete email in simple HTML, addressed TO {recipient} on the owner's behalf. Show them what's on their plate for the owner, what the owner is waiting on, and where they can help.

You are given an ALREADY-FILTERED payload. Do NOT invent items. Each item has: area, text (the task), context (who/what/why/blockers), due (hard deadline), check (soft review date), tier (0=critical…4=someday), bucket.

FORMAT: use ONLY <h3>, <ul>/<li>, <b>, <br>. No CSS/Markdown/code fences. Close every tag. Address {recipient} by name; keep it collegial and actionable.

Open with a one-line greeting naming the shape of their day, then a 2-3 sentence paragraph (the only prose), then "<br><br>".

SECTIONS (emit only if non-empty), in order:
1. <h3>What the owner needs from you</h3> — from needs_now (overdue/due-soon/check-due). One <li> each with a real briefing: "<b>{area}</b> — " + task + key context + next step; prefix tier-0 with "<b>CRITICAL</b> "; show "(due {due})"/"(check {check})". Most detail here.
2. <h3>Your task list</h3> — from your_tasks. Group by area; short <li> each: "<b>{area}</b> — {task}" + where it stands + date if any.
3. <h3>Where you can help</h3> — from help_items (the owner's own todos in {help_area}). Only if present. Frame as: if they have bandwidth, good candidates to take off the owner's hands. Short <li> each, overdue/due-soon first; if long, show ~8 and end "<li>…and {N} more on the owner's list.</li>".

Close with one line: "<b>{counts.needs_now}</b> need your attention now · {counts.member_total} tasks on your plate · {counts.help} open where you can help."

You may remind them once, briefly, that they can simply REPLY to this email to report progress or mark something done — {agent} will record it. Keep it tight and skimmable."""


def _load_system(member):
    """Load the member's system prompt from its prompt_note (Agent/<note>.md) or
    $TEAM_SYSTEM_FILE: strip frontmatter + HTML comments, prefer a fenced block. Falls back
    to _DEFAULT_TEAM_SYSTEM. `{agent}`/`{recipient}`/`{role}`/`{help_area}` are filled in."""
    d = _load_digest()
    env = os.environ.get("TEAM_SYSTEM_FILE")
    note = member.get("prompt_note") or "Team Digest Prompt"
    path = Path(env).expanduser() if env else (d.VAULT_DIR / "Agent" / (note + ".md"))
    try:
        text = path.read_text(encoding="utf-8")
    except OSError:
        text = _DEFAULT_TEAM_SYSTEM
    else:
        text = re.sub(r"^\s*---\n.*?\n---\n", "", text, count=1, flags=re.S)
        text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
        fence = re.search(r"```[a-zA-Z]*\n(.*?)```", text, flags=re.S)
        if fence:
            text = fence.group(1)
        text = text.strip() or _DEFAULT_TEAM_SYSTEM
    return text.replace("{agent}", d._agent_name()) \
               .replace("{recipient}", member.get("name", "")) \
               .replace("{role}", member.get("role", "") or "team member") \
               .replace("{help_area}", member.get("help_area") or "")


def write_digest(member, payload):
    # Outbound send: a provider error PROPAGATES (same posture as digest.write_digest).
    d = _load_digest()
    provider = AnthropicProvider()
    reply = provider.chat(
        [{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}],
        system=_load_system(member), model=d.MODEL, max_tokens=6000,
    )
    return reply.unfenced()


def subject_for(member, payload):
    return "\U0001F4EC " + member["name"] + " — Daily Digest — " + payload["date"]


def run(member, mode="send", today=None, sink=print):
    """Build/render/send one member's brief. Modes mirror digest.run_digests
    (send/print/payload/plan). CADENCE lives in the caller (the automation runner owns the
    per-member tz window + once/day gate). Returns status lines (also to `sink` unless None);
    a real send yields a line starting 'sent '. If the member has reply=true, the brief's
    Message-ID is recorded (team_state) so their reply can be matched."""
    d = _load_digest()
    today = today or dt.date.today()
    to = member["email"]
    label = f"{member['name']} digest"

    def emit(line):
        if sink is not None:
            sink(line)
        return [line]

    if mode == "plan":
        return emit(f"would send {label} -> {to}")
    payload = build_payload(member, today)
    subject = subject_for(member, payload)
    if mode == "payload":
        return emit(f"===== {subject}  ->  {to} =====\n"
                    + json.dumps(payload, indent=2, ensure_ascii=False))
    html = write_digest(member, payload)
    if mode == "print":
        return emit(f"===== {subject}  ->  {to} =====\n{html}\n")
    msg_id = d.send_email(html, subject, to=to, to_name=member["name"],
                          from_name=member.get("from_name") or None)   # mode == "send"
    if member.get("reply"):
        try:
            _load_state().record_sent_digest_id(member["key"], msg_id)
        except Exception:
            pass
    return emit(f"sent {label} -> {to}")


def run_all(mode="send", today=None, member_key=None, sink=print):
    d = _load_digest()
    members = load_members(d.VAULT_DIR)
    if member_key:
        members = [m for m in members if m["key"] == member_key]
    results = []
    if not members:
        line = "no team members configured" if not member_key else f"no member '{member_key}'"
        if sink:
            sink(line)
        return [line]
    for mb in members:
        results += run(mb, mode=mode, today=today, sink=sink)
    return results


def main():
    ap = argparse.ArgumentParser(description="Send (or preview) per-member team digests.")
    ap.add_argument("--dry-run", action="store_true", help="print each member payload; no Claude/send")
    ap.add_argument("--print", dest="print_only", action="store_true", help="render + print each; no send")
    ap.add_argument("--member", help="only this member key")
    args = ap.parse_args()
    d = _load_digest()
    if not d.VAULT_DIR.exists():
        sys.exit(f"VAULT_DIR not found: {d.VAULT_DIR}")
    today = dt.date.today()
    iso = os.environ.get("TEAM_TODAY") or os.environ.get("DIGEST_TODAY")
    if iso:
        try:
            today = dt.date.fromisoformat(iso)
        except ValueError:
            pass
    run_all(mode=("payload" if args.dry_run else "print" if args.print_only else "send"),
            today=today, member_key=args.member)


if __name__ == "__main__":
    main()
