#!/usr/bin/env python3
"""
Daily calendar → community-interaction sync.

Reads yesterday's Google Calendar events from a private ICS URL, looks for
events that either:
  - have a `#friend:<Name>` (or `#family:<Name>`) tag in the title, OR
  - have an ATTENDEE whose email matches a member's email in friends01 or family01

For each match, POSTs to /api/community/auto-log on the dashboard server.
The endpoint dedupes by source + external_id (the ICS UID), so this script
is safe to re-run.

Config: .claude/context/calendar-config.json
{
  "ics_url": "https://calendar.google.com/calendar/ical/.../basic.ics",
  "api_base": "http://localhost:8765"
}

Env override: CALENDAR_ICS_URL takes precedence over the config file.

Run: ~/bin/run-calendar-friend-sync.sh   (launchd: com.chl.calendar-friend-sync)
"""
import json
import os
import re
import sys
import urllib.request
from datetime import datetime, timedelta, timezone
from pathlib import Path

BRISBANE = timezone(timedelta(hours=10))  # Queensland — no DST
PROJECT_ROOT = Path(__file__).resolve().parent.parent
CONFIG_PATH = PROJECT_ROOT / '.claude' / 'context' / 'calendar-config.json'


def log(msg: str) -> None:
    ts = datetime.now(BRISBANE).strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{ts}] {msg}", flush=True)


def load_config() -> dict:
    cfg = {}
    if CONFIG_PATH.exists():
        with open(CONFIG_PATH) as f:
            cfg = json.load(f)
    if os.environ.get('CALENDAR_ICS_URL'):
        cfg['ics_url'] = os.environ['CALENDAR_ICS_URL']
    if not cfg.get('ics_url'):
        log("ERROR: no ics_url configured. Create .claude/context/calendar-config.json "
            "with {\"ics_url\": \"...\"} or set CALENDAR_ICS_URL env var.")
        sys.exit(1)
    cfg.setdefault('api_base', 'http://localhost:8765')
    return cfg


def fetch_ics(url: str) -> str:
    req = urllib.request.Request(url, headers={'User-Agent': 'chl-calendar-friend-sync/1.0'})
    with urllib.request.urlopen(req, timeout=30) as resp:
        return resp.read().decode('utf-8', errors='replace')


def parse_ics(text: str) -> list[dict]:
    """Minimal ICS VEVENT parser. Handles line folding, returns list of dicts
    with: uid, summary, dtstart (date or datetime), attendee_emails."""
    # Unfold lines: any line starting with space or tab continues the previous line
    unfolded = []
    for line in text.splitlines():
        if line.startswith((' ', '\t')) and unfolded:
            unfolded[-1] += line[1:]
        else:
            unfolded.append(line)

    events = []
    current = None
    for line in unfolded:
        if line == 'BEGIN:VEVENT':
            current = {'attendee_emails': []}
        elif line == 'END:VEVENT':
            if current is not None:
                events.append(current)
            current = None
        elif current is not None:
            if ':' not in line:
                continue
            head, _, value = line.partition(':')
            name, _, _params = head.partition(';')
            name = name.upper()
            if name == 'UID':
                current['uid'] = value.strip()
            elif name == 'SUMMARY':
                current['summary'] = _unescape(value)
            elif name == 'DTSTART':
                current['dtstart'] = _parse_dt(value, head)
            elif name == 'ATTENDEE':
                # Value usually like "mailto:foo@bar.com"; sometimes just an email
                m = re.search(r'mailto:([^\s;,]+)', value, re.IGNORECASE)
                email = (m.group(1) if m else value).strip().lower()
                if '@' in email:
                    current['attendee_emails'].append(email)
    return events


def _unescape(value: str) -> str:
    return (value.replace('\\,', ',')
                 .replace('\\;', ';')
                 .replace('\\n', '\n')
                 .replace('\\N', '\n')
                 .replace('\\\\', '\\'))


def _parse_dt(value: str, head: str):
    """Returns a datetime in Brisbane tz, or a date (string YYYY-MM-DD) for all-day events."""
    value = value.strip()
    is_date_only = 'VALUE=DATE' in head.upper() or (len(value) == 8 and value.isdigit())
    if is_date_only:
        # All-day event: YYYYMMDD
        return datetime.strptime(value[:8], '%Y%m%d').replace(tzinfo=BRISBANE)
    # Datetime: YYYYMMDDTHHMMSS optionally with Z (UTC) or TZID param
    fmt = '%Y%m%dT%H%M%SZ' if value.endswith('Z') else '%Y%m%dT%H%M%S'
    dt = datetime.strptime(value, fmt)
    if value.endswith('Z'):
        dt = dt.replace(tzinfo=timezone.utc).astimezone(BRISBANE)
    else:
        # Naive — assume Brisbane (Google ICS exports include TZID in params we don't fully parse)
        dt = dt.replace(tzinfo=BRISBANE)
    return dt


def fetch_members(api_base: str, community_id: str) -> list[dict]:
    url = f"{api_base}/api/community-members?community_id={community_id}"
    with urllib.request.urlopen(url, timeout=15) as resp:
        data = json.loads(resp.read())
    return data.get('members', [])


TAG_PATTERN = re.compile(r'#(?:friend|family):\s*([A-Za-zÆØÅæøåÄÖÜäöü\-\' ]+?)(?=\s+#|\s*$|\s*[,;])', re.IGNORECASE)


def find_matches(event: dict, members_by_community: dict) -> list[tuple[str, str]]:
    """Returns list of (community_id, contact_name) tuples for one event."""
    matches = []
    summary = event.get('summary', '') or ''

    # 1. Tag-based: #friend:Nanne or #family:Christian
    for m in TAG_PATTERN.finditer(summary):
        name = m.group(1).strip()
        # Search for member with this name across communities
        for cid, members in members_by_community.items():
            for member in members:
                mname = (member.get('name') or '').lower()
                if not mname:
                    continue
                if mname == name.lower() or mname.split()[0] == name.lower():
                    matches.append((cid, member['name']))
                    break

    # 2. Email-based: attendee email matches a member's email
    if event.get('attendee_emails'):
        for cid, members in members_by_community.items():
            for member in members:
                email = (member.get('email') or '').strip().lower()
                if email and email in event['attendee_emails']:
                    matches.append((cid, member['name']))

    # Dedupe
    seen = set()
    result = []
    for cid, name in matches:
        key = (cid, name)
        if key not in seen:
            seen.add(key)
            result.append(key)
    return result


def post_auto_log(api_base: str, source: str, contact_name: str, community_id: str,
                  external_id: str, date: str, description: str) -> tuple[int, dict]:
    payload = {
        "source": source,
        "contact_name": contact_name,
        "community_id": community_id,
        "external_id": external_id,
        "date": date,
        "description": description,
    }
    req = urllib.request.Request(
        f"{api_base}/api/community/auto-log",
        data=json.dumps(payload).encode(),
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            return resp.status, json.loads(resp.read())
    except urllib.error.HTTPError as e:
        return e.code, json.loads(e.read() or b'{}')


def main():
    cfg = load_config()
    log(f"Fetching ICS feed…")
    try:
        ics_text = fetch_ics(cfg['ics_url'])
    except Exception as e:
        log(f"ERROR fetching ICS: {e}")
        sys.exit(2)

    events = parse_ics(ics_text)
    log(f"Parsed {len(events)} events from feed")

    # Filter to yesterday (Brisbane)
    today_bne = datetime.now(BRISBANE).date()
    yesterday_bne = today_bne - timedelta(days=1)
    target_events = []
    for ev in events:
        dt = ev.get('dtstart')
        if not dt:
            continue
        ev_date = dt.astimezone(BRISBANE).date() if isinstance(dt, datetime) else dt
        if ev_date == yesterday_bne:
            target_events.append(ev)
    log(f"{len(target_events)} events on {yesterday_bne.isoformat()}")

    if not target_events:
        return

    # Load community members for auto-loggable communities
    members_by_community = {}
    for cid in ('family01', 'friends01'):
        try:
            members_by_community[cid] = fetch_members(cfg['api_base'], cid)
            log(f"Loaded {len(members_by_community[cid])} members from {cid}")
        except Exception as e:
            log(f"WARN: couldn't load members for {cid}: {e}")
            members_by_community[cid] = []

    created, dup, missed = 0, 0, 0
    for ev in target_events:
        matches = find_matches(ev, members_by_community)
        if not matches:
            continue
        for community_id, contact_name in matches:
            uid = ev.get('uid', '')
            status, body = post_auto_log(
                api_base=cfg['api_base'],
                source='calendar',
                contact_name=contact_name,
                community_id=community_id,
                external_id=uid,
                date=yesterday_bne.isoformat(),
                description=ev.get('summary', '') or 'Calendar event'
            )
            if status == 201:
                created += 1
                log(f"  + {community_id}/{contact_name}: {body.get('interaction_id')} ({ev.get('summary','')})")
            elif status == 200 and body.get('status') == 'duplicate':
                dup += 1
                log(f"  · {community_id}/{contact_name}: duplicate (already logged)")
            else:
                missed += 1
                log(f"  ! {community_id}/{contact_name}: HTTP {status} — {body}")

    log(f"Done. created={created} duplicate={dup} errors={missed}")


if __name__ == '__main__':
    main()
