// --- WALL-E Listening Skill: Calendar Integration ---
// Syncs upcoming meetings from macOS Calendar (EventKit), ICS feeds,
// or Google Calendar. Detects current/upcoming meetings for auto-recording.

const { execFileSync } = require('child_process');
const path = require('path');
const { randomUUID: uuidv4 } = require('node:crypto');
const brain = require('../brain');
const { createSafeFetch } = require('../security/ssrf');

const CAL_READER = path.join(__dirname, '..', 'skills', '_bundled', 'google-calendar', 'cal-reader');

// ── Configuration ────────────────────────────────────────────────────

/**
 * Get the active calendar source.
 * @returns {'eventkit'|'google'|'ics'|null}
 */
function getCalendarSource() {
  return brain.getKv('listening_calendar_source') || null;
}

/**
 * Configure a calendar source.
 * @param {'eventkit'|'google'|'ics'} source
 * @param {object} [config] - Source-specific config (e.g. {icsUrl} for ICS)
 */
function configureSource(source, config) {
  if (!['eventkit', 'google', 'ics'].includes(source)) {
    throw new Error('Invalid calendar source: ' + source);
  }
  brain.setKv('listening_calendar_source', source);
  if (config) {
    brain.setKv('listening_calendar_config', JSON.stringify(config));
  }
}

/**
 * Get calendar configuration.
 */
function getConfig() {
  const source = getCalendarSource();
  let config = {};
  try { config = JSON.parse(brain.getKv('listening_calendar_config') || '{}'); } catch {}
  return { source, config };
}

// ── Meeting Sync ─────────────────────────────────────────────────────

/**
 * Sync upcoming meetings from the configured calendar source.
 * @param {number} [windowHours=2] - How far ahead to look
 * @returns {Promise<{synced: number, source: string}>}
 */
async function syncUpcomingMeetings(windowHours) {
  if (windowHours == null) windowHours = 2;
  const source = getCalendarSource();
  if (!source) return { synced: 0, source: 'none' };

  let events;
  if (source === 'eventkit' || source === 'google') {
    events = _readEventKit(windowHours);
  } else if (source === 'ics') {
    const config = getConfig().config;
    if (!config.icsUrl) throw new Error('ICS URL not configured');
    events = await _readICS(config.icsUrl, windowHours);
  } else {
    return { synced: 0, source };
  }

  const db = brain.getDb();
  const upsert = db.prepare(`
    INSERT INTO calendar_meetings (id, source, calendar_uid, title, start_time, end_time, attendees, location, synced_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
    ON CONFLICT(calendar_uid) DO UPDATE SET
      title = excluded.title, start_time = excluded.start_time, end_time = excluded.end_time,
      attendees = excluded.attendees, location = excluded.location, synced_at = excluded.synced_at
  `);

  const syncAll = db.transaction((events) => {
    let count = 0;
    for (const evt of events) {
      // Use calendar_uid for dedup, generate id if needed
      const existing = db.prepare('SELECT id FROM calendar_meetings WHERE calendar_uid = ?').get(evt.uid);
      const id = existing ? existing.id : uuidv4();
      const attendees = evt.attendees ? JSON.stringify(evt.attendees) : null;

      upsert.run(id, source, evt.uid, evt.title || 'Untitled', evt.start, evt.end, attendees, evt.location || null);
      count++;
    }
    return count;
  });

  const synced = syncAll(events);
  return { synced, source };
}

/**
 * Detect the current or next upcoming meeting.
 * @returns {{meeting: object|null, status: 'active'|'upcoming'|'none', startsInMs: number|null}}
 */
function detectCurrentMeeting() {
  const db = brain.getDb();

  // Check for currently active meeting (started and not ended)
  // Normalize ISO dates (2026-04-17T23:22:00.125Z) to SQLite format (2026-04-17 23:22:00)
  const active = db.prepare(`
    SELECT * FROM calendar_meetings
    WHERE replace(substr(start_time, 1, 19), 'T', ' ') <= datetime('now', '+2 minutes')
      AND replace(substr(end_time, 1, 19), 'T', ' ') >= datetime('now', '-2 minutes')
    ORDER BY start_time ASC LIMIT 1
  `).get();

  if (active) {
    const startMs = new Date(active.start_time).getTime();
    const now = Date.now();
    if (startMs <= now + 2 * 60000) {
      return { meeting: active, status: 'active', startsInMs: Math.max(0, startMs - now) };
    }
  }

  // Check for upcoming meeting within 15 minutes
  const upcoming = db.prepare(`
    SELECT * FROM calendar_meetings
    WHERE replace(substr(start_time, 1, 19), 'T', ' ') > datetime('now')
      AND replace(substr(start_time, 1, 19), 'T', ' ') <= datetime('now', '+15 minutes')
    ORDER BY start_time ASC LIMIT 1
  `).get();

  if (upcoming) {
    const startsInMs = new Date(upcoming.start_time).getTime() - Date.now();
    return { meeting: upcoming, status: 'upcoming', startsInMs };
  }

  return { meeting: null, status: 'none', startsInMs: null };
}

/**
 * Get upcoming meetings from the database.
 * @param {number} [limit=10]
 * @returns {Array<calendar_meeting>}
 */
function getUpcomingMeetings(limit) {
  return brain.getDb().prepare(`
    SELECT * FROM calendar_meetings
    WHERE replace(substr(end_time, 1, 19), 'T', ' ') >= datetime('now')
    ORDER BY start_time ASC
    LIMIT ?
  `).all(limit || 10);
}

// ── Calendar Source Readers ──────────────────────────────────────────

/**
 * Read events from macOS Calendar via EventKit (uses compiled cal-reader binary).
 * @param {number} hoursAhead
 * @returns {Array<{uid, title, start, end, attendees, location}>}
 */
function _readEventKit(hoursAhead) {
  const fs = require('fs');
  if (!fs.existsSync(CAL_READER)) {
    throw new Error('cal-reader binary not found. Run the google-calendar skill first to compile it.');
  }

  try {
    const output = execFileSync(CAL_READER, [], {
      timeout: 10000,
      encoding: 'utf8'
    });

    const events = JSON.parse(output);
    const now = Date.now();
    const cutoff = now + hoursAhead * 60 * 60 * 1000;

    return events
      .filter(evt => {
        if (evt.allDay) return false;
        const start = new Date(evt.start).getTime();
        return start >= now - 60000 && start <= cutoff;
      })
      .map(evt => ({
        uid: evt.uid,
        title: evt.title,
        start: evt.start,
        end: evt.end,
        attendees: evt.attendees || [],
        location: evt.location || ''
      }));
  } catch (err) {
    if (err.message && err.message.includes('not found')) throw err;
    console.error('[listening] cal-reader error:', err.message);
    return [];
  }
}

/**
 * Read events from an ICS feed URL.
 * Lightweight parser for VEVENT blocks.
 * @param {string} url
 * @param {number} hoursAhead
 * @returns {Promise<Array<{uid, title, start, end, attendees, location}>>}
 */
async function _readICS(url, hoursAhead) {
  const safeFetch = createSafeFetch({ fetchImpl: globalThis.fetch });
  const resp = await safeFetch(url, { signal: AbortSignal.timeout(15000) });
  if (!resp.ok) throw new Error('ICS fetch failed: ' + resp.status);
  const text = await resp.text();

  const events = [];
  const now = Date.now();
  const cutoff = now + hoursAhead * 60 * 60 * 1000;

  // Split into VEVENT blocks
  const veventBlocks = text.split('BEGIN:VEVENT');
  for (let i = 1; i < veventBlocks.length; i++) {
    const block = veventBlocks[i].split('END:VEVENT')[0];

    const uid = _icsField(block, 'UID');
    const title = _icsField(block, 'SUMMARY');
    const location = _icsField(block, 'LOCATION');
    const dtstart = _icsField(block, 'DTSTART');
    const dtend = _icsField(block, 'DTEND');

    if (!dtstart) continue;

    const start = _parseIcsDate(dtstart);
    const end = dtend ? _parseIcsDate(dtend) : new Date(start.getTime() + 3600000);

    if (start.getTime() >= now - 60000 && start.getTime() <= cutoff) {
      // Extract attendees
      const attendees = [];
      const attendeeMatches = block.matchAll(/ATTENDEE([^:]*):mailto:([^\r\n]+)/gi);
      for (const m of attendeeMatches) {
        const params = m[1] || '';
        const cnMatch = params.match(/CN=([^;:\r\n]+)/i);
        const name = cnMatch ? cnMatch[1].replace(/^"|"$/g, '') : '';
        attendees.push({ name, email: m[2] || '' });
      }

      events.push({
        uid: uid || uuidv4(),
        title: title || 'Untitled',
        start: start.toISOString(),
        end: end.toISOString(),
        attendees,
        location: location || ''
      });
    }
  }

  return events;
}

function _icsField(block, name) {
  const regex = new RegExp('^' + name + '(?:;[^:\\r\\n]*)*:(.+)', 'mi');
  const match = block.match(regex);
  return match ? match[1].trim().replace(/\\,/g, ',').replace(/\\n/g, '\n') : '';
}

function _parseIcsDate(str) {
  // Handle YYYYMMDDTHHMMSSZ format
  const clean = str.replace(/[^0-9TZ]/g, '');
  if (/^\d{8}T\d{6}Z?$/.test(clean)) {
    const y = clean.slice(0, 4);
    const m = clean.slice(4, 6);
    const d = clean.slice(6, 8);
    const H = clean.slice(9, 11);
    const M = clean.slice(11, 13);
    const S = clean.slice(13, 15);
    const suffix = str.trim().endsWith('Z') ? 'Z' : '';
    return new Date(y + '-' + m + '-' + d + 'T' + H + ':' + M + ':' + S + suffix);
  }
  // Fallback to Date parser
  return new Date(str);
}

module.exports = {
  getCalendarSource,
  configureSource,
  getConfig,
  syncUpcomingMeetings,
  detectCurrentMeeting,
  getUpcomingMeetings
};
