#!/usr/bin/env node
'use strict';
/**
 * gws-workspace sync skill — reads calendar events, gmail messages, and drive
 * files via the gws CLI, then stores them as memories in WALL-E's brain.
 *
 * Usage:  node run.js [--services calendar,gmail,drive] [--account email]
 *
 * Replaces: google-calendar (EventKit/Swift), email-sync (Mail.app/JXA)
 * Requires: gws CLI installed + authenticated (run setup.js first)
 */
const { execFileSync } = require('child_process');
const path = require('path');
const fs = require('fs');

const SKILL_DIR = __dirname;

// Parse CLI args
const args = process.argv.slice(2);
let services = 'calendar,gmail,drive';
let account = null;
let calDaysBack = 1;
let calDaysAhead = 14;
let gmailMaxResults = 20;
let gmailQuery = 'newer_than:3d';

for (let i = 0; i < args.length; i++) {
  if (args[i] === '--services' && args[i + 1]) services = args[i + 1];
  if (args[i] === '--account' && args[i + 1]) account = args[i + 1];
  if (args[i] === '--calendar-days-back' && args[i + 1]) calDaysBack = parseInt(args[i + 1], 10);
  if (args[i] === '--calendar-days-ahead' && args[i + 1]) calDaysAhead = parseInt(args[i + 1], 10);
  if (args[i] === '--gmail-max-results' && args[i + 1]) gmailMaxResults = parseInt(args[i + 1], 10);
  if (args[i] === '--gmail-query' && args[i + 1]) gmailQuery = args[i + 1];
}

// Task-level config overrides
try {
  const envConfig = process.env.WALL_E_SKILL_CONFIG
    ? JSON.parse(process.env.WALL_E_SKILL_CONFIG) : {};
  if (envConfig.services) services = envConfig.services;
  if (envConfig.calendar_days_back != null) calDaysBack = envConfig.calendar_days_back;
  if (envConfig.calendar_days_ahead != null) calDaysAhead = envConfig.calendar_days_ahead;
  if (envConfig.gmail_max_results != null) gmailMaxResults = envConfig.gmail_max_results;
  if (envConfig.gmail_query) gmailQuery = envConfig.gmail_query;
} catch { /* ignore */ }

const serviceList = services.split(',').map(s => s.trim());

// ── gws CLI wrapper ───────────────────────────────────────────────────

let setupModule = null;
function getSetup() {
  if (!setupModule) setupModule = require('./setup');
  return setupModule;
}

function getGwsCommandConfig(profileId) {
  const setup = getSetup();
  return {
    binary: typeof setup.getGwsBinary === 'function' ? setup.getGwsBinary(profileId) : 'gws',
    env: typeof setup.getGwsEnv === 'function' ? setup.getGwsEnv(profileId) : { ...process.env },
  };
}

/**
 * Execute a gws command and return parsed JSON.
 * Handles credential swapping for multi-account support.
 */
function gws(cmdArgs, opts = {}) {
  const cfg = getGwsCommandConfig(opts.profileId);
  const env = { ...cfg.env };

  // If account specified, use per-account credentials via token env var
  if (opts.token) {
    env.GOOGLE_WORKSPACE_CLI_TOKEN = opts.token;
  }

  try {
    const result = execFileSync(cfg.binary, cmdArgs, {
      timeout: opts.timeout || 30000,
      maxBuffer: 10 * 1024 * 1024,
      env,
      encoding: 'utf8',
    });
    // gws may print "Using keyring backend: keyring" to stdout before JSON
    const jsonStart = result.indexOf('{');
    const jsonArrayStart = result.indexOf('[');
    const start = jsonStart >= 0 && (jsonArrayStart < 0 || jsonStart < jsonArrayStart)
      ? jsonStart : jsonArrayStart;
    if (start < 0) return null;
    return JSON.parse(result.slice(start));
  } catch (err) {
    const stderr = (err.stderr || '').toString();
    const stdout = (err.stdout || '').toString();
    // Try to parse error JSON
    try {
      const errJson = JSON.parse(stdout.slice(stdout.indexOf('{')));
      return errJson;
    } catch {}
    console.error(`[gws-workspace] gws error: ${stderr || err.message}`);
    return null;
  }
}

// ── Calendar Sync ─────────────────────────────────────────────────────

function syncCalendar(brain, token, profileId) {
  console.error(`[gws-workspace] Syncing calendar (${calDaysBack}d back, ${calDaysAhead}d ahead)...`);

  const now = new Date();
  const timeMin = new Date(now.getTime() - calDaysBack * 86400000).toISOString();
  const timeMax = new Date(now.getTime() + calDaysAhead * 86400000).toISOString();

  // Get calendar list first
  const calList = gws(['calendar', 'calendarList', 'list', '--format', 'json'], { token, profileId });
  if (!calList || calList.error) {
    console.error(`[gws-workspace] Calendar list failed: ${calList?.error?.message || 'unknown'}`);
    return { inserted: 0, updated: 0, skipped: 0, cleaned: 0, total: 0, error: calList?.error?.message };
  }

  const calendars = calList.items || [];
  let inserted = 0, updated = 0, skipped = 0, cleaned = 0, total = 0;
  const seenSourceIds = new Set();

  for (const cal of calendars) {
    const params = JSON.stringify({
      calendarId: cal.id,
      timeMin,
      timeMax,
      singleEvents: true,
      orderBy: 'startTime',
      maxResults: 250,
    });

    const events = gws(['calendar', 'events', 'list', '--params', params, '--format', 'json'], { token, profileId, timeout: 60000 });
    if (!events || events.error) continue;

    for (const evt of (events.items || [])) {
      total++;
      const start = evt.start?.dateTime || evt.start?.date || '';
      const isAllDay = !evt.start?.dateTime;

      const parts = [evt.summary || '(no title)'];
      if (evt.location) parts.push(`Location: ${evt.location}`);
      if (evt.attendees?.length > 0) {
        const names = evt.attendees.map(a => a.displayName || a.email).filter(Boolean);
        if (names.length > 0) parts.push(`Attendees: ${names.join(', ')}`);
      }
      if (evt.organizer) parts.push(`Organizer: ${evt.organizer.displayName || evt.organizer.email}`);
      if (evt.description) parts.push(`Notes: ${evt.description.slice(0, 500)}`);
      const content = parts.join('\n');

      const sourceId = `cal:${evt.id}:${start}`;
      seenSourceIds.add(sourceId);

      const existing = brain.getDb().prepare(
        'SELECT id, content FROM memories WHERE source = ? AND source_id = ?'
      ).get('calendar', sourceId);

      if (existing) {
        if (existing.content !== content) {
          brain.getDb().prepare(
            'UPDATE memories SET content = ?, metadata = ?, timestamp = ?, subject = ? WHERE id = ?'
          ).run(content, JSON.stringify(evt), start, evt.summary, existing.id);
          updated++;
        } else {
          skipped++;
        }
        continue;
      }

      const mem = {
        source: 'calendar',
        source_id: sourceId,
        source_channel: cal.summary || cal.id,
        memory_type: isAllDay ? 'calendar_allday' : 'calendar_event',
        subject: evt.summary || '(no title)',
        content,
        metadata: JSON.stringify(evt),
        importance: isAllDay ? 0.3 : 0.5,
        timestamp: start,
      };

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

  // Clean stale events in sync window
  if (total > 0) {
    const dbEvents = brain.getDb().prepare(
      "SELECT id, source_id FROM memories WHERE source = 'calendar' AND timestamp >= ? AND timestamp <= ?"
    ).all(timeMin, timeMax);
    for (const row of dbEvents) {
      if (!seenSourceIds.has(row.source_id)) {
        brain.deleteMemory(row.id); // cascades to memory_index + embeddings (no orphan leak)
        cleaned++;
      }
    }
  }

  console.error(`[gws-workspace] Calendar: ${inserted} new, ${updated} updated, ${skipped} unchanged, ${cleaned} cleaned`);
  return { inserted, updated, skipped, cleaned, total };
}

// ── Gmail Sync ────────────────────────────────────────────────────────

function syncGmail(brain, token, accountEmail, profileId) {
  console.error(`[gws-workspace] Syncing gmail (query: ${gmailQuery}, max: ${gmailMaxResults})...`);

  const params = JSON.stringify({ userId: 'me', q: gmailQuery, maxResults: gmailMaxResults });
  const msgList = gws(['gmail', 'users', 'messages', 'list', '--params', params, '--format', 'json'], { token, profileId });

  if (!msgList || msgList.error) {
    console.error(`[gws-workspace] Gmail list failed: ${msgList?.error?.message || 'unknown'}`);
    return { inserted: 0, updated: 0, skipped: 0, total: 0, error: msgList?.error?.message };
  }

  const messages = msgList.messages || [];
  let inserted = 0, updated = 0, skipped = 0;

  for (const msgRef of messages) {
    const msgParams = JSON.stringify({ userId: 'me', id: msgRef.id, format: 'metadata', metadataHeaders: ['From', 'To', 'Cc', 'Subject', 'Date'] });
    const msg = gws(['gmail', 'users', 'messages', 'get', '--params', msgParams, '--format', 'json'], { token, profileId });
    if (!msg || msg.error) continue;

    const headers = {};
    for (const h of (msg.payload?.headers || [])) {
      headers[h.name.toLowerCase()] = h.value;
    }

    const sourceId = `mail:${accountEmail || account || 'gmail'}:${msg.id}`;
    const isSent = (msg.labelIds || []).includes('SENT');
    const subject = headers.subject || '(no subject)';
    const from = headers.from || '';
    const to = headers.to || '';
    const cc = headers.cc || '';
    const date = headers.date || '';
    const snippet = msg.snippet || '';

    const parts = [subject];
    parts.push(`From: ${from}`);
    if (to) parts.push(`To: ${to}`);
    if (cc) parts.push(`Cc: ${cc}`);
    if (snippet) parts.push(`\n${snippet}`);
    const content = parts.join('\n');

    const existing = brain.getDb().prepare(
      'SELECT id, content FROM memories WHERE source = ? AND source_id = ?'
    ).get('email', sourceId);

    if (existing) {
      if (existing.content !== content) {
        brain.getDb().prepare(
          'UPDATE memories SET content = ?, metadata = ?, timestamp = ? WHERE id = ?'
        ).run(content, JSON.stringify({ id: msg.id, from, to, cc, subject, date, snippet, labels: msg.labelIds }), date, existing.id);
        updated++;
      } else {
        skipped++;
      }
      continue;
    }

    const mem = {
      source: 'email',
      source_id: sourceId,
      source_channel: accountEmail || account || 'gmail',
      memory_type: isSent ? 'email_sent' : 'email_received',
      subject,
      content,
      metadata: JSON.stringify({ id: msg.id, from, to, cc, subject, date, snippet, labels: msg.labelIds }),
      importance: isSent ? 0.6 : 0.4,
      timestamp: date ? new Date(date).toISOString() : new Date().toISOString(),
    };

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

  console.error(`[gws-workspace] Gmail: ${inserted} new, ${updated} updated, ${skipped} unchanged`);
  return { inserted, updated, skipped, total: messages.length };
}

// ── Drive Sync ────────────────────────────────────────────────────────

function syncDrive(brain, token, accountEmail, profileId) {
  console.error('[gws-workspace] Syncing drive (recent files)...');

  const params = JSON.stringify({ pageSize: 20, orderBy: 'modifiedTime desc', fields: 'files(id,name,mimeType,modifiedTime,owners,webViewLink,size)' });
  const files = gws(['drive', 'files', 'list', '--params', params, '--format', 'json'], { token, profileId });

  if (!files || files.error) {
    console.error(`[gws-workspace] Drive list failed: ${files?.error?.message || 'unknown'}`);
    return { inserted: 0, skipped: 0, total: 0, error: files?.error?.message };
  }

  let inserted = 0, skipped = 0;
  const fileList = files.files || [];

  for (const file of fileList) {
    const sourceId = `drive:${file.id}`;

    const existing = brain.getDb().prepare(
      'SELECT id FROM memories WHERE source = ? AND source_id = ?'
    ).get('drive', sourceId);

    if (existing) { skipped++; continue; }

    const parts = [file.name];
    if (file.mimeType) parts.push(`Type: ${file.mimeType}`);
    if (file.modifiedTime) parts.push(`Modified: ${file.modifiedTime}`);
    if (file.owners?.length > 0) parts.push(`Owner: ${file.owners.map(o => o.displayName || o.emailAddress).join(', ')}`);

    const mem = {
      source: 'drive',
      source_id: sourceId,
      source_channel: accountEmail || account || 'drive',
      memory_type: 'drive_file',
      subject: file.name,
      content: parts.join('\n'),
      metadata: JSON.stringify(file),
      importance: 0.3,
      timestamp: file.modifiedTime || new Date().toISOString(),
    };

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

  console.error(`[gws-workspace] Drive: ${inserted} new, ${skipped} unchanged`);
  return { inserted, skipped, total: fileList.length };
}

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

async function syncAccount(brain, setup, acctEmail, profileId) {
  const creds = await setup.getCredentials(acctEmail, { profileId });
  if (!creds || !creds.access_token) {
    console.error(`[gws-workspace] No valid credentials for ${acctEmail || 'default'}. Run: node setup.js`);
    return null;
  }
  const token = creds.access_token;
  const email = acctEmail || account;

  const results = {};
  if (serviceList.includes('calendar')) results.calendar = syncCalendar(brain, token, profileId);
  if (serviceList.includes('gmail')) results.gmail = syncGmail(brain, token, email, profileId);
  if (serviceList.includes('drive')) results.drive = syncDrive(brain, token, email, profileId);
  return results;
}

function resolveAccountProfileId(setup, email) {
  if (!email || !setup || typeof setup.listAccounts !== 'function') return null;
  try {
    const acct = setup.listAccounts().find(a => a && a.email === email);
    return acct && acct.profileId ? acct.profileId : null;
  } catch {
    return null;
  }
}

async function main() {
  const setup = getSetup();

  // Check gws is installed
  try {
    execFileSync(setup.getGwsBinary(), ['--version'], {
      stdio: 'pipe',
      timeout: 10000,
      env: setup.getGwsEnv(),
    });
  } catch {
    throw new Error('[gws-workspace] gws CLI not found. Run: node setup.js');
  }

  // Load brain
  const brainPath = path.resolve(SKILL_DIR, '..', '..', '..', 'brain.js');
  if (!fs.existsSync(brainPath)) {
    throw new Error(`[gws-workspace] brain.js not found at ${brainPath}`);
  }
  const brain = require(brainPath);
  brain.initDb();

  let allResults = {};

  if (account) {
    // Single account mode (explicit --account flag)
    allResults = await syncAccount(brain, setup, account, resolveAccountProfileId(setup, account));
  } else {
    // Multi-account mode: sync all configured accounts
    const accounts = setup.listAccounts();
    if (accounts.length === 0) {
      brain.closeDb();
      // Soft no-op when invoked as a module (e.g. by skill-planner) — daemon
      // must NOT exit just because the user hasn't configured Google accounts.
      // CLI invocation surfaces this as a process exit via the wrapper below.
      return { skipped: 'no accounts configured', message: '[gws-workspace] No accounts configured. Run: node setup.js' };
    }
    for (const acct of accounts) {
      console.error(`[gws-workspace] Syncing account: ${acct.email}`);
      const result = await syncAccount(brain, setup, acct.email, acct.profileId);
      if (result) allResults[acct.email] = result;
    }
  }

  brain.closeDb();
  return allResults;
}

module.exports = { run: main, _internal: { gws, syncAccount, getGwsCommandConfig, resolveAccountProfileId } };

if (require.main === module) {
  main()
    .then((result) => {
      // Surface the soft "no accounts" branch as stdout + non-error exit so
      // the CLI behaviour stays informative without daemon-killing.
      if (result && result.skipped) {
        console.error(result.message);
        return;
      }
      console.log(JSON.stringify(result));
    })
    .catch(err => {
      console.error(`[gws-workspace] Error: ${err.message}`);
      process.exit(1);
    });
}
