#!/usr/bin/env python3
"""
BloomWealth Board — triage bot.

Runs on a schedule (cron). For every task sitting in Triage that hasn't been
assessed yet, it asks the Claude API for a one-line assessment + priority +
suggested tags, writes that back onto the task, and (by default) moves it to
In Progress so a Claude session can pick it up.

Everything runs through Claude directly — no OpenClaw agent routing.

Config (read from environment, or from /opt/board/.env as KEY=value lines):
  ANTHROPIC_API_KEY   required — the Claude API key
  TRIAGE_MODEL        optional — model id (default: claude-haiku-4-5)
  TELEGRAM_BOT_TOKEN  optional — if set with chat id, sends a ping
  TELEGRAM_CHAT_ID    optional

Usage:
  python3 triage_bot.py            # process unassessed triage tasks
  python3 triage_bot.py --dry-run  # assess + print, but don't write/move

Safe by design: any task it cannot assess is left exactly as-is. The tasks
file is written atomically (temp + rename) so a crash can't corrupt it.
"""
import json
import os
import sys
import urllib.request
import urllib.error
from datetime import datetime, timezone

BOARD_DIR  = os.path.dirname(os.path.abspath(__file__))
TASKS_FILE = os.path.join(BOARD_DIR, 'tasks.json')
LOG_FILE   = os.path.join(BOARD_DIR, '_log.md')
ENV_FILE   = os.path.join(BOARD_DIR, '.env')

# Auto-move assessed tasks Triage -> In Progress. Flip to False for
# human-in-the-loop (assess + annotate, but leave the card in Triage so Tony
# drags it across himself).
AUTO_MOVE = True

API_URL = 'https://api.anthropic.com/v1/messages'

SYSTEM_PROMPT = (
    "You are the triage assistant for Tony's task board. Tony runs several "
    "companies (BloomWealth, Iconic Investors, an Education Hub, an SMSF "
    "company). Every task is executed by Claude directly. Given one task, "
    "reply with STRICT JSON only (no prose, no markdown fences):\n"
    '{"assessment": "<one concise sentence: how to approach it / first step>", '
    '"priority": "low|med|high", '
    '"tags": ["<up to 3 short lowercase labels>"], '
    '"complexity": "small|medium|large"}'
)


# ── config ──────────────────────────────────────────────────────────────────
def load_env():
    """Merge process env with KEY=value lines from .env (env wins)."""
    cfg = {}
    if os.path.exists(ENV_FILE):
        with open(ENV_FILE, encoding='utf-8') as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith('#') or '=' not in line:
                    continue
                k, v = line.split('=', 1)
                cfg[k.strip()] = v.strip().strip('"').strip("'")
    cfg.update({k: v for k, v in os.environ.items() if k in (
        'ANTHROPIC_API_KEY', 'TRIAGE_MODEL', 'TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID'
    )})
    return cfg


# ── claude call ─────────────────────────────────────────────────────────────
def assess(task, api_key, model):
    """Return dict assessment, or None on any failure (task left untouched)."""
    user = f"Title: {task.get('title','')}\nDescription: {task.get('desc','')}\n" \
           f"Company: {task.get('company','')}"
    body = json.dumps({
        'model': model,
        'max_tokens': 300,
        'system': SYSTEM_PROMPT,
        'messages': [{'role': 'user', 'content': user}],
    }).encode('utf-8')
    req = urllib.request.Request(API_URL, data=body, method='POST', headers={
        'content-type': 'application/json',
        'x-api-key': api_key,
        'anthropic-version': '2023-06-01',
    })
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            payload = json.loads(resp.read())
        text = ''.join(b.get('text', '') for b in payload.get('content', [])).strip()
        # tolerate stray ``` fences
        if text.startswith('```'):
            text = text.strip('`')
            text = text[text.find('{'):text.rfind('}') + 1]
        data = json.loads(text)
        prio = str(data.get('priority', 'med')).lower()
        if prio not in ('low', 'med', 'high'):
            prio = 'med'
        tags = [str(t).strip().lower() for t in data.get('tags', []) if str(t).strip()][:3]
        return {
            'assessment': str(data.get('assessment', '')).strip()[:280],
            'priority': prio,
            'tags': tags,
            'complexity': str(data.get('complexity', '')).lower(),
        }
    except (urllib.error.URLError, urllib.error.HTTPError, ValueError, KeyError) as e:
        print(f"  ! assess failed for {task.get('id')}: {e}", file=sys.stderr)
        return None


def notify(cfg, msg):
    token, chat = cfg.get('TELEGRAM_BOT_TOKEN'), cfg.get('TELEGRAM_CHAT_ID')
    if not (token and chat):
        return
    try:
        url = f'https://api.telegram.org/bot{token}/sendMessage'
        data = json.dumps({'chat_id': chat, 'text': msg}).encode('utf-8')
        req = urllib.request.Request(url, data=data, method='POST',
                                     headers={'content-type': 'application/json'})
        urllib.request.urlopen(req, timeout=15).read()
    except Exception as e:  # noqa: BLE001 — notification is best-effort
        print(f"  ! telegram notify failed: {e}", file=sys.stderr)


def write_atomic(path, tasks):
    tmp = path + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f:
        json.dump(tasks, f, indent=2, ensure_ascii=False)
    os.replace(tmp, path)


# ── main ────────────────────────────────────────────────────────────────────
def main():
    dry = '--dry-run' in sys.argv
    cfg = load_env()
    api_key = cfg.get('ANTHROPIC_API_KEY')
    if not api_key:
        print('ANTHROPIC_API_KEY not set (env or /opt/board/.env). Exiting.',
              file=sys.stderr)
        return 1
    model = cfg.get('TRIAGE_MODEL', 'claude-haiku-4-5')

    if not os.path.exists(TASKS_FILE):
        print('tasks.json not found. Exiting.', file=sys.stderr)
        return 1
    with open(TASKS_FILE, encoding='utf-8') as f:
        tasks = json.load(f)

    pending = [t for t in tasks if t.get('stage') == 'triage' and not t.get('assessment')]
    if not pending:
        print('No untriaged tasks. Nothing to do.')
        return 0
    print(f'Assessing {len(pending)} task(s) with {model}…')

    changed = []
    for t in pending:
        a = assess(t, api_key, model)
        if not a:
            continue
        t['assessment'] = a['assessment']
        t['priority'] = a['priority']
        # merge suggested tags into existing (dedupe, keep order)
        merged = list(dict.fromkeys((t.get('tags') or []) + a['tags']))
        t['tags'] = merged
        t['triaged_at'] = datetime.now(timezone.utc).isoformat(timespec='seconds')
        if AUTO_MOVE:
            t['stage'] = 'progress'
        changed.append(t)
        arrow = '→ In Progress' if AUTO_MOVE else '(stays in Triage)'
        print(f"  ✓ {t['id']} [{a['priority']}] {a['assessment'][:70]} {arrow}")

    if not changed:
        print('Nothing assessed (all calls failed). tasks.json untouched.')
        return 0

    if dry:
        print('\n--dry-run: no changes written.')
        return 0

    write_atomic(TASKS_FILE, tasks)

    stamp = datetime.now().strftime('%Y-%m-%d')
    titles = ', '.join(t['title'] for t in changed)
    line = (f"\n{stamp} — **Triage bot** assessed {len(changed)} task(s)"
            f"{' and moved to In Progress' if AUTO_MOVE else ''}: {titles}. (board)\n")
    try:
        with open(LOG_FILE, 'a', encoding='utf-8') as f:
            f.write(line)
    except OSError as e:
        print(f"  ! log append failed: {e}", file=sys.stderr)

    notify(cfg, f"Triage bot: assessed {len(changed)} task(s)"
                f"{' → In Progress' if AUTO_MOVE else ''}.\n{titles}")
    print(f'\nDone. {len(changed)} task(s) updated.')
    return 0


if __name__ == '__main__':
    sys.exit(main())
