#!/usr/bin/env python3
"""
Morning Digest — canonical cron script template.

Copy to /tmp/morning_digest_cron.py and adapt the delivery target at
the bottom. Combines the three proven pieces in one file:

  1. Token probe + REFRESH with VERIFIED WRITE (2026-07-17, 2026-08-08,
     2026-08-15 — three strikes, same bug class. Refresh + verify-write
     MUST be embedded in the same script block. Do not split into
     separate steps.)
  2. Single-pass Gmail fetch: list 50 unread, fetch format=full metadata
     for all of them, body-extract only deadline-bearing candidates.
     Holds full list in memory — no double-fetching downstream.
  3. Calendar next-7-days fetch in the same script run.

USAGE:
  python3 this_script.py    # exits 0 with /tmp/morning_fetch.json written

The shape comes from the 2026-08-14 + 2026-08-15 morning-digest crons.
"""
import json, time, urllib.request, urllib.parse, base64, re, html as htmllib
from datetime import datetime, timezone, timedelta
from pathlib import Path

# ===== CONFIG =====
try:
    import zoneinfo
    SYD = zoneinfo.ZoneInfo('Australia/Sydney')
except Exception:
    SYD = timezone(timedelta(hours=10), name='AEST')

TOKEN_PATH = '/root/.hermes/google_token.json'
CLIENT_PATH = '/root/.hermes/google_client_secret.json'   # installed JSON client
OUT_PATH = '/tmp/morning_fetch.json'
GMAIL_LIST_QUERY = 'is:unread'
GMAIL_MAX = 50
CAL_DAYS_AHEAD = 7
BODY_BUDGET = 8

NOW = datetime.now(SYD)
WEEK_FROM = NOW + timedelta(days=CAL_DAYS_AHEAD)
NOW_ISO = NOW.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
WEEK_ISO = WEEK_FROM.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')


# ===== TOKEN REFRESH + VERIFIED WRITE =====
#
# Three-strike rule (2026-07-17, 2026-08-08, 2026-08-15): the refresh +
# fetch split across separate scripts caused the save block to raise
# NameError because the datetime import was missed. Fix: embed the
# verify-write check INSIDE this function, abort non-zero if the file
# didn't actually update. If we silently returned here the cron would
# ship an in-memory digest against a stale token file and the next
# morning's run would hit the same refresh again.
#
def refresh_token():
    creds = json.load(open(CLIENT_PATH))['installed']
    tok = json.load(open(TOKEN_PATH))

    body = urllib.parse.urlencode({
        'client_id': creds['client_id'],
        'client_secret': creds['client_secret'],
        'refresh_token': tok['refresh_token'],
        'grant_type': 'refresh_token',
    }).encode()
    req = urllib.request.Request(
        creds['token_uri'], data=body, method='POST',
        headers={'Content-Type': 'application/x-www-form-urlencoded'},
    )
    with urllib.request.urlopen(req, timeout=30) as r:
        new = json.loads(r.read())

    # Persist
    tok['access_token'] = new['access_token']
    tok['expires_in'] = new.get('expires_in', 3600)
    tok['expiry_date'] = int(time.time() * 1000) + new.get('expires_in', 3600) * 1000
    Path(TOKEN_PATH).write_text(json.dumps(tok, indent=2))

    # VERIFIED WRITE — abort non-zero if save didn't land
    saved = json.load(open(TOKEN_PATH))
    if saved.get('access_token') != new['access_token']:
        raise SystemExit('WRITE_FAILED: token file not updated after refresh')
    if 'expiry_date' in saved:
        delta_ms = saved['expiry_date'] - int(time.time() * 1000)
        if delta_ms < 0:
            raise SystemExit(f'WRITE_FAILED: expiry_date still in past ({delta_ms}ms)')
    return new['access_token']


def get_token():
    """Return a working access token, refreshing on first call if needed."""
    tok = json.load(open(TOKEN_PATH))
    ed = tok.get('expiry_date')
    if ed and ed > int(time.time() * 1000) + 60_000:
        return tok['access_token']
    return refresh_token()


# ===== HTTP HELPER =====
def http_get(url, headers, timeout=30):
    req = urllib.request.Request(url, headers=headers)
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return r.status, r.read()


# ===== GMAIL =====
def fetch_message(mid, H):
    rs, rb = http_get(
        f'https://gmail.googleapis.com/gmail/v1/users/me/messages/{mid}?format=full',
        H,
    )
    return json.loads(rb) if rs == 200 else None


def hdr(headers, name):
    for h in headers or []:
        if h['name'].lower() == name.lower():
            return h['value']
    return ''


def extract_text(payload):
    """Extract text/plain body, walking parts. Returns '' if HTML-only."""
    def _data(p):
        d = (p.get('body') or {}).get('data')
        if d:
            raw = d + '=' * (-len(d) % 4)
            return base64.urlsafe_b64decode(raw).decode('utf-8', 'replace')
        return ''
    txt = _data(payload)
    if txt and 'must be viewed in HTML mode' not in txt.lower():
        return txt
    for p in payload.get('parts') or []:
        t = extract_text(p)
        if t and 'must be viewed in HTML mode' not in t.lower():
            return t
    return ''


def extract_html(payload):
    """Extract text/html body and strip tags. Returns plain text."""
    if (payload.get('mimeType') or '') == 'text/html':
        s = (payload.get('body') or {}).get('data')
        if s:
            raw = s + '=' * (-len(s) % 4)
            h = base64.urlsafe_b64decode(raw).decode('utf-8', 'replace')
            return html_to_text(h)
    for p in payload.get('parts') or []:
        t = extract_html(p)
        if t: return t
    return ''


def html_to_text(h):
    t = re.sub(r'<style[^>]*>.*?</style>', '', h, flags=re.S | re.I)
    t = re.sub(r'<[^>]+>', ' ', t)
    t = htmllib.unescape(t)
    return re.sub(r'\s+', ' ', t).strip()


def fetch_gmail(access_token):
    """One-pass: list, fetch metadata for all. Body decode on demand."""
    H = {'Authorization': f'Bearer {access_token}'}
    rs, rb = http_get(
        f'https://gmail.googleapis.com/gmail/v1/users/me/messages'
        f'?q={urllib.parse.quote(GMAIL_LIST_QUERY)}&maxResults={GMAIL_MAX}',
        H,
    )
    if rs != 200:
        return [], H
    msg_refs = json.loads(rb).get('messages', [])
    emails = []
    for ref in msg_refs:
        full = fetch_message(ref['id'], H)
        if not full: continue
        p = full.get('payload') or {}
        emails.append({
            'id': ref['id'],
            'from': hdr(p.get('headers', []), 'From'),
            'subject': hdr(p.get('headers', []), 'Subject'),
            'date_header': hdr(p.get('headers', []), 'Date'),
            'internal_ms': full.get('internalDate'),
            'snippet': full.get('snippet', ''),
            'payload': p,
        })
    return emails, H


# ===== CALENDAR =====
def fetch_calendar(access_token):
    H = {'Authorization': f'Bearer {access_token}'}
    url = (
        'https://www.googleapis.com/calendar/v3/calendars/primary/events'
        f'?timeMin={NOW_ISO}&timeMax={WEEK_ISO}&singleEvents=true&orderBy=startTime&maxResults=50'
    )
    rs, rb = http_get(url, H)
    if rs != 200:
        return [], rs
    events = []
    for ev in json.loads(rb).get('items', []):
        s = ev.get('start', {})
        e = ev.get('end', {})
        events.append({
            'summary': ev.get('summary', '(no title)'),
            'start': s.get('dateTime') or s.get('date'),
            'end': e.get('dateTime') or e.get('date'),
            'all_day': 'dateTime' not in s,
            'location': ev.get('location', ''),
            'attendees': len(ev.get('attendees', [])),
        })
    return events, rs


# ===== MAIN =====
def main():
    access = get_token()

    emails, H = fetch_gmail(access)
    events, cal_status = fetch_calendar(access)

    # Body-extract deadline-bearing candidates — Liam fwds, EstimateOne,
    # or any subject matching a tender keyword. Capped at BODY_BUDGET to
    # stay under rate limits.
    deadline_kw = ('tender', 'rft', 'rfp', 'addendum', 'rfq', 'quote', 'eot', 'extension of time')
    body_count = 0
    for e in emails:
        if body_count >= BODY_BUDGET:
            break
        subj = (e.get('subject') or '').lower()
        frm = (e.get('from') or '').lower()
        is_priority = (
            'liam.fitzgerald@lfcs' in frm
            or 'docs@estimateone' in frm
            or any(k in subj for k in deadline_kw)
        )
        if not is_priority:
            continue
        txt = extract_text(e['payload'])
        if not txt:
            txt = 'HTML: ' + extract_html(e['payload'])
        e['body'] = txt[:1500]
        body_count += 1

    # Persist (drop payload field — too large to dump)
    persist = [{
        **{k: v for k, v in e.items() if k != 'payload'},
        'has_body': 'body' in e,
    } for e in emails]

    json.dump({
        'fetched_at_syd': NOW.isoformat(),
        'now_syd': NOW.strftime('%A %d %b %Y %H:%M'),
        'cal_status': cal_status,
        'body_budget_used': body_count,
        'emails': persist,
        'events': events,
    }, open(OUT_PATH, 'w'), indent=2)

    print(f"OK emails={len(emails)} events={len(events)} bodies_extracted={body_count} → {OUT_PATH}")


if __name__ == '__main__':
    main()
