#!/usr/bin/env node
'use strict';
/**
 * email-sync skill — reads sent emails from macOS Mail (which syncs with
 * Gmail/Google Workspace) via JXA, then stores them as memories in
 * WALL-E's brain so it can learn the owner's communication style.
 *
 * By default, only SENT emails are synced (with body content).
 * Inbox sync is opt-in via --sync-inbox, and when enabled only emails
 * where the owner is on the To/Cc line are included.
 *
 * Usage:  node run.js [--days-back N] [--sync-inbox]
 *
 * Requires: Mail.app running with accounts configured. Terminal needs
 *   Automation access for Mail in System Settings > Privacy & Security.
 */
const { execFileSync } = require('child_process');
const path = require('path');
const fs = require('fs');

const SKILL_DIR = __dirname;
const JXA_SCRIPT = path.join(SKILL_DIR, 'mail-reader.jxa');

// Hard ceilings — keep Mail.app responsive even with bad config.
//
// INCREMENTAL_FALLBACK_DAYS: when --incremental is on but the brain has no
// prior email rows, the previous behavior took the configured days_back
// (often 3650 from a backfill task), telling JXA to traverse a decade of
// inbox and pinning Mail at 80%+ CPU "Not Responding" for ~40 minutes. The
// recurring task should refresh recent mail; the explicit 10-year backfill
// is a separate one-time op (--full-backfill).
//
// MAX_DAYS_BACK_INCREMENTAL: caps the latest-timestamp delta in case the
// last email in the brain is years old, which would also balloon daysBack.
//
// MAX_TIMEOUT_MS: was 2 h, which let one bad run hold Mail unresponsive
// the entire morning. 10 min is enough for a real incremental sync.
const INCREMENTAL_FALLBACK_DAYS = 30;
const MAX_DAYS_BACK_INCREMENTAL = 30;
const MAX_TIMEOUT_MS = 10 * 60 * 1000; // 10 min

// Parse CLI args
const args = process.argv.slice(2);
let daysBack = 3;
let syncInbox = false;
let incremental = false;
let fullBackfill = false;
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] === '--sync-inbox') syncInbox = true;
  if (args[i] === '--incremental') incremental = true;
  if (args[i] === '--full-backfill') fullBackfill = true;
}

// Task-level config overrides (passed via WALL_E_SKILL_CONFIG env var)
try {
  const envConfig = process.env.WALL_E_SKILL_CONFIG
    ? JSON.parse(process.env.WALL_E_SKILL_CONFIG) : {};
  if (envConfig.days_back != null) daysBack = envConfig.days_back;
  if (envConfig.sync_inbox != null) syncInbox = !!envConfig.sync_inbox;
  if (envConfig.incremental != null) incremental = !!envConfig.incremental;
  if (envConfig.full_backfill != null) fullBackfill = !!envConfig.full_backfill;
} catch { /* ignore parse errors */ }

// Incremental mode: compute days_back from the latest email already in the brain
if (incremental) {
  try {
    const brainPath = path.resolve(SKILL_DIR, '..', '..', '..', 'brain.js');
    const brain = require(brainPath);
    brain.initDb();
    const row = brain.getDb().prepare(
      "SELECT max(timestamp) as ts FROM memories WHERE source = 'email'"
    ).get();
    brain.closeDb();
    if (row?.ts) {
      const latestDate = new Date(row.ts);
      const now = new Date();
      // Go back 1 extra day for overlap/safety margin
      const computedDays = Math.ceil((now - latestDate) / (24 * 60 * 60 * 1000)) + 1;
      const cappedDays = fullBackfill ? computedDays : Math.min(computedDays, MAX_DAYS_BACK_INCREMENTAL);
      if (cappedDays < computedDays) {
        console.error(`[email-sync] Incremental: latest email is ${row.ts} (${computedDays}d ago), capping at ${cappedDays}d. Pass --full-backfill (one-time) to fill the gap.`);
      } else {
        console.error(`[email-sync] Incremental: latest email is ${row.ts}, fetching ${cappedDays}d back`);
      }
      daysBack = cappedDays;
    } else if (fullBackfill) {
      console.error(`[email-sync] Incremental: no existing emails, --full-backfill set, fetching ${daysBack}d back`);
    } else {
      console.error(`[email-sync] Incremental: no existing emails, falling back to ${INCREMENTAL_FALLBACK_DAYS}d (pass --full-backfill for a one-time historical sync)`);
      daysBack = INCREMENTAL_FALLBACK_DAYS;
    }
  } catch (err) {
    console.error(`[email-sync] Incremental check failed: ${err.message}, capping days_back at ${INCREMENTAL_FALLBACK_DAYS}d`);
    if (!fullBackfill) daysBack = Math.min(daysBack, INCREMENTAL_FALLBACK_DAYS);
  }
}

// ── Concurrency lock ─────────────────────────────────────────────────
// One in-flight email-sync per host. Mail.app handles Apple Events
// serially anyway; running two in parallel just lets two scripts fight
// over the same Mail process. Without this, every walle-dev-* daemon on
// the box stampedes Mail simultaneously.
const LOCK_PATH = process.env.WALLE_MAIL_LOCK_PATH ||
  path.join(require('os').tmpdir(), 'wall-e-mail-appleevent.lock');
const COOLDOWN_PATH = process.env.WALLE_MAIL_COOLDOWN_PATH ||
  path.join(require('os').tmpdir(), 'wall-e-mail-appleevent-cooldown.json');
let lockSkipReason = 'busy';

function readJson(filePath) {
  try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
  catch { return null; }
}

function mailCooldownActive() {
  const data = readJson(COOLDOWN_PATH);
  if (!data || !data.until || Number(data.until) <= Date.now()) {
    try { fs.unlinkSync(COOLDOWN_PATH); } catch {}
    return null;
  }
  return data;
}

function acquireLock() {
  const cooldown = mailCooldownActive();
  if (cooldown) {
    lockSkipReason = 'cooldown';
    const retrySec = Math.ceil((Number(cooldown.until) - Date.now()) / 1000);
    console.error(`[email-sync] Mail automation cooling down after ${cooldown.operation || 'mail'}; retry in about ${retrySec}s.`);
    return false;
  }
  lockSkipReason = 'busy';
  const payload = JSON.stringify({ pid: process.pid, operation: 'email-sync', startedAt: Date.now() });
  try {
    const fd = fs.openSync(LOCK_PATH, 'wx', 0o644);
    fs.writeFileSync(fd, payload);
    fs.closeSync(fd);
    return true;
  } catch (err) {
    if (!err || err.code !== 'EEXIST') return false;
  }
  try {
    const raw = fs.readFileSync(LOCK_PATH, 'utf8');
    const parsed = readJson(LOCK_PATH);
    const existing = parsed && typeof parsed === 'object'
      ? parsed
      : { pid: parseInt(raw, 10), startedAt: 0, operation: 'email-sync' };
    const pid = Number(existing.pid || 0);
    if (pid) {
      try {
        process.kill(pid, 0);
        return false;  // alive — refuse
      } catch { /* stale — fall through */ }
    }
  } catch { /* no lock file */ }
  try { fs.unlinkSync(LOCK_PATH); } catch {}
  return acquireLock();
}
function releaseLock() {
  try {
    const raw = fs.readFileSync(LOCK_PATH, 'utf8');
    const parsed = readJson(LOCK_PATH);
    const existing = parsed && typeof parsed === 'object'
      ? parsed
      : { pid: parseInt(raw, 10) };
    if (Number(existing.pid) === process.pid) fs.unlinkSync(LOCK_PATH);
  } catch { /* nothing to do */ }
}

if (!acquireLock()) {
  if (lockSkipReason !== 'cooldown') {
    console.error('[email-sync] Another email-sync is already running on this host — skipping this run.');
  }
  process.exit(0);
}
process.on('exit', releaseLock);
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
  process.on(sig, () => { releaseLock(); process.exit(1); });
}

// ── Step 1: Read emails via JXA ──────────────────────────────────────

function readEmails() {
  const jxaArgs = ['-l', 'JavaScript', JXA_SCRIPT, '--days-back', String(daysBack)];
  if (syncInbox) jxaArgs.push('--inbox');

  // Scale timeout with daysBack but hard-cap at MAX_TIMEOUT_MS so a stuck
  // run never holds Mail unresponsive longer than 10 minutes.
  const scaled = daysBack <= 7 ? 300_000 : Math.min(daysBack * 20_000, MAX_TIMEOUT_MS);
  const timeoutMs = Math.min(scaled, MAX_TIMEOUT_MS);
  const result = execFileSync('osascript', jxaArgs, {
    timeout: timeoutMs,
    maxBuffer: 50 * 1024 * 1024,
  });

  const data = JSON.parse(result.toString('utf8'));
  console.error(`[email-sync] Read ${data.inbox.length} inbox + ${data.sent.length} sent (${daysBack}d back)`);
  return data;
}

// ── Step 2: Open Automation privacy settings on permission error ─────

function openAutomationSettings() {
  // Never auto-open System Settings — just log instructions
  console.error('[email-sync] Please open: System Settings > Privacy & Security > Automation');
  console.error('[email-sync] Enable Mail access for your terminal app.');
}

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

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

  let inserted = 0;
  let skipped = 0;
  let updated = 0;
  let filtered = 0;
  let errors = 0;

  // Collect owner's email addresses from all accounts
  const ownerEmails = new Set();
  for (const acct of (data.accounts || [])) {
    for (const email of (acct.emails || [])) {
      ownerEmails.add(email.toLowerCase());
    }
  }
  console.error(`[email-sync] Owner emails: ${[...ownerEmails].join(', ')}`);

  // Filter inbox: only emails where owner is on To or Cc line
  const inboxFiltered = data.inbox.filter(msg => {
    const allRecips = [...(msg.to || []), ...(msg.cc || [])];
    return allRecips.some(r => r.email && ownerEmails.has(r.email.toLowerCase()));
  });
  filtered = data.inbox.length - inboxFiltered.length;
  if (data.inbox.length > 0) {
    console.error(`[email-sync] Inbox: ${inboxFiltered.length} addressed to owner, ${filtered} filtered out (mailing lists, BCC, etc.)`);
  }

  const allMessages = [
    ...inboxFiltered.map(m => ({ ...m, type: 'email_received' })),
    ...data.sent.map(m => ({ ...m, type: 'email_sent' })),
  ];

  for (const msg of allMessages) {
    try {
      // Build readable content
      const parts = [msg.subject];
      parts.push(`From: ${msg.sender}`);
      if (msg.to && msg.to.length > 0) {
        const names = msg.to.map(r => r.name || r.email).filter(Boolean);
        if (names.length > 0) parts.push(`To: ${names.join(', ')}`);
      }
      if (msg.cc && msg.cc.length > 0) {
        const names = msg.cc.map(r => r.name || r.email).filter(Boolean);
        if (names.length > 0) parts.push(`Cc: ${names.join(', ')}`);
      }
      if (msg.content) {
        parts.push(`\n${msg.content}`);
      }
      const content = parts.join('\n');

      const sourceId = `mail:${msg.messageId}`;

      // Check if exists
      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(msg), msg.date, existing.id);
          updated++;
        } else {
          skipped++;
        }
        continue;
      }

      // Insert new
      const mem = {
        source: 'email',
        source_id: sourceId,
        source_channel: msg.account,
        memory_type: msg.type,
        subject: msg.subject,
        content,
        metadata: JSON.stringify(msg),
        importance: msg.type === 'email_sent' ? 0.6 : 0.4,
        timestamp: msg.date,
      };

      const result = brain.insertMemory(mem);
      if (result) inserted++;
      else skipped++;
    } catch (err) {
      errors++;
      console.error(`[email-sync] Error processing message: ${err.message}`);
    }
  }

  brain.closeDb();
  return { inserted, updated, skipped, filtered, errors, total: allMessages.length };
}

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

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

async function main() {
  const MAX_RETRIES = 2;
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
    try {
      const data = readEmails();
      const stats = storeToBrain(data);
      console.log(JSON.stringify(stats));
      console.error(`[email-sync] Done: ${stats.inserted} new, ${stats.updated} updated, ${stats.skipped} unchanged, ${stats.filtered} filtered, ${stats.errors} errors`);
      return stats;
    } catch (err) {
      const errMsg = (err.stderr || err.message || '').toString();
      const isTimeout = errMsg.includes('-1712') || errMsg.includes('timed out');
      const isPermDenied = !isTimeout && (errMsg.includes('not allowed') || errMsg.includes('assistive access')
        || errMsg.includes('-1743') || errMsg.includes('-1744'));

      if (isTimeout) {
        console.error(`[email-sync] Mail app timed out (attempt ${attempt + 1}/${MAX_RETRIES + 1}). Mail may be busy, not running, or the query is too large.`);
        if (attempt < MAX_RETRIES) {
          console.error('[email-sync] Retrying in 10s — make sure Mail.app is open and responsive...');
          await sleep(10_000);
          continue;
        }
        throw new Error('[email-sync] Still timing out after retries — try a smaller days_back or ensure Mail.app is responsive');
      }

      if (isPermDenied && attempt < MAX_RETRIES) {
        console.error(`[email-sync] Mail access denied (attempt ${attempt + 1}/${MAX_RETRIES + 1}).`);
        openAutomationSettings();
        await sleep(5_000);
        continue;
      }
      if (isPermDenied) {
        throw new Error('[email-sync] Mail access still denied — grant Automation > Mail access to your terminal in System Settings > Privacy & Security');
      }
      throw new Error(`[email-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('[email-sync] Error:', err.message);
    process.exit(1);
  });
}
