#!/usr/bin/env node
'use strict';
/**
 * google-calendar sync skill — reads events from macOS Calendar (which syncs
 * with Google Calendar) via a compiled Swift binary using EventKit, then
 * stores them as memories in WALL-E's brain.
 *
 * Usage:  node run.js [--days-back N] [--days-ahead N]
 *
 * Requires: Calendar access granted to the terminal in
 *   System Settings > Privacy & Security > Calendars
 */
const { execFileSync } = require('child_process');
const path = require('path');
const fs = require('fs');

const SKILL_DIR = __dirname;
const SWIFT_SRC = path.join(SKILL_DIR, 'cal-reader.swift');
const BINARY = path.join(SKILL_DIR, 'cal-reader');

// Parse CLI args
const args = process.argv.slice(2);
let daysBack = 1;
let daysAhead = 14;
for (let i = 0; i < args.length; i++) {
  if (args[i] === '--days-back' && args[i + 1]) daysBack = parseInt(args[i + 1], 10);
  if (args[i] === '--days-ahead' && args[i + 1]) daysAhead = parseInt(args[i + 1], 10);
}

// ── Step 1: Compile Swift binary if needed ───────────────────────────

function ensureBinary() {
  const srcStat = fs.statSync(SWIFT_SRC);
  let needCompile = true;
  if (fs.existsSync(BINARY)) {
    const binStat = fs.statSync(BINARY);
    needCompile = srcStat.mtimeMs > binStat.mtimeMs;
  }
  if (needCompile) {
    console.error('[calendar-sync] Compiling cal-reader.swift...');
    execFileSync('swiftc', ['-O', SWIFT_SRC, '-o', BINARY, '-framework', 'EventKit'], {
      stdio: ['ignore', 'pipe', 'pipe'],
      timeout: 120_000,
    });
    console.error('[calendar-sync] Compiled successfully');
  }
}

// ── Step 2: Read events from macOS Calendar ──────────────────────────

function readEvents() {
  const result = execFileSync(BINARY, [
    '--days-back', String(daysBack),
    '--days-ahead', String(daysAhead),
  ], { timeout: 30_000, maxBuffer: 10 * 1024 * 1024 });

  const events = JSON.parse(result.toString('utf8'));
  console.error(`[calendar-sync] Read ${events.length} events (${daysBack}d back, ${daysAhead}d ahead)`);
  return events;
}

// ── Step 3: Store in brain ───────────────────────────────────────────

function storeToBrain(events) {
  const brainPath = path.resolve(SKILL_DIR, '..', '..', '..', 'brain.js');
  if (!fs.existsSync(brainPath)) {
    throw new Error(`[calendar-sync] brain.js not found at ${brainPath}`);
  }
  const brain = require(brainPath);
  brain.initDb();

  let inserted = 0;
  let skipped = 0;
  let updated = 0;

  // Track all source_ids seen in this sync to clean up stale entries afterwards
  const seenSourceIds = new Set();

  for (const evt of events) {
    // Build a readable content string
    const parts = [evt.title];
    if (evt.location) parts.push(`Location: ${evt.location}`);
    if (evt.attendees && evt.attendees.length > 0) {
      const names = evt.attendees.map(a => a.name || a.email).filter(Boolean);
      if (names.length > 0) parts.push(`Attendees: ${names.join(', ')}`);
    }
    if (evt.organizer) parts.push(`Organizer: ${evt.organizer}`);
    if (evt.notes) parts.push(`Notes: ${evt.notes}`);
    const content = parts.join('\n');

    // Use uid + start as source_id for dedup
    const sourceId = `cal:${evt.uid}:${evt.start}`;
    seenSourceIds.add(sourceId);

    // Check if this exact event already exists
    const existing = brain.getDb().prepare(
      'SELECT id, content FROM memories WHERE source = ? AND source_id = ?'
    ).get('calendar', sourceId);

    if (existing) {
      // Update if content changed (attendees added, notes updated, title renamed, etc.)
      if (existing.content !== content) {
        brain.getDb().prepare(
          'UPDATE memories SET content = ?, metadata = ?, timestamp = ?, subject = ? WHERE id = ?'
        ).run(content, JSON.stringify(evt), evt.start, evt.title, existing.id);
        updated++;
      } else {
        skipped++;
      }
      continue;
    }

    // Insert new event
    const mem = {
      source: 'calendar',
      source_id: sourceId,
      source_channel: evt.calendar,
      memory_type: evt.allDay ? 'calendar_allday' : 'calendar_event',
      subject: evt.title,
      content,
      metadata: JSON.stringify(evt),
      importance: evt.allDay ? 0.3 : 0.5,
      timestamp: evt.start,
    };

    const result = brain.insertMemory(mem);
    if (result) inserted++;
    else skipped++;
  }

  // Clean up: remove calendar events in the sync window that EventKit no longer
  // returns (deleted or cancelled events). Only clean within the actual time
  // range we synced — don't touch events outside the window.
  let cleaned = 0;
  if (events.length > 0) {
    const timestamps = events.map(e => e.start).sort();
    const windowStart = timestamps[0];
    const windowEnd = timestamps[timestamps.length - 1];
    const dbEvents = brain.getDb().prepare(
      "SELECT id, source_id FROM memories WHERE source = 'calendar' AND timestamp >= ? AND timestamp <= ?"
    ).all(windowStart, windowEnd);
    for (const row of dbEvents) {
      if (!seenSourceIds.has(row.source_id)) {
        brain.deleteMemory(row.id); // cascades to memory_index + embeddings (no orphan leak)
        cleaned++;
      }
    }
    if (cleaned > 0) {
      console.error(`[calendar-sync] Cleaned ${cleaned} stale/deleted events`);
    }
  }

  brain.closeDb();
  return { inserted, skipped, updated, cleaned, total: events.length };
}

// ── Main ─────────────────────────────────────────────────────────────

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function main() {
  ensureBinary();

  const MAX_RETRIES = 2;
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
    try {
      const events = readEvents();
      const stats = storeToBrain(events);
      console.log(JSON.stringify(stats));
      console.error(`[calendar-sync] Done: ${stats.inserted} new, ${stats.updated} updated, ${stats.skipped} unchanged${stats.cleaned ? `, ${stats.cleaned} cleaned` : ''}`);
      return stats;
    } catch (err) {
      const isAccessDenied = err.stderr && err.stderr.toString().includes('access_denied');
      if (isAccessDenied && attempt < MAX_RETRIES) {
        console.error(`[calendar-sync] Calendar access denied (attempt ${attempt + 1}/${MAX_RETRIES + 1}).`);
        console.error('[calendar-sync] Grant access in System Settings > Privacy & Security > Calendars');
        await sleep(5_000); // brief wait before retry
        continue;
      }
      if (isAccessDenied) {
        throw new Error('[calendar-sync] Calendar access still denied — grant access in System Settings > Privacy & Security > Calendars');
      }
      throw new Error(`[calendar-sync] ${err.message}`);
    }
  }
}

// Export for skill-planner; also run directly when invoked as CLI script
module.exports = { run: main };

if (require.main === module) {
  main().catch(err => {
    console.error('[calendar-sync] Error:', err.message);
    process.exit(1);
  });
}
