// PRE Web GUI — Calendar integration (macOS Calendar.app + Windows Outlook COM)
// macOS: Zero-config calendar via EventKit/AppleScript — works with iCloud, Google, Exchange, or any configured account
// Windows: Outlook COM automation via PowerShell — works with any account configured in Outlook

const { execSync } = require('child_process');
const { IS_WIN, IS_MAC, IS_LINUX } = require('../platform');

function runAS(script, timeout = 30000) {
  try {
    return execSync('osascript -', {
      input: script,
      encoding: 'utf-8',
      timeout,
      maxBuffer: 256 * 1024,
    }).trim();
  } catch (err) {
    const msg = (err.stderr || err.message || '').trim();
    throw new Error(msg || 'AppleScript execution failed');
  }
}

/**
 * Run a PowerShell script and return the result.
 * stdio is fully piped so PowerShell errors don't leak to the Node console.
 */
function runPS(script, timeout = 30000) {
  try {
    return execSync('powershell.exe -NoProfile -Command -', {
      input: `$ErrorActionPreference = 'Stop'\n${script}`,
      encoding: 'utf-8',
      timeout,
      maxBuffer: 256 * 1024,
      stdio: ['pipe', 'pipe', 'pipe'],
    }).trim();
  } catch (err) {
    const msg = (err.stderr || err.message || '').trim();
    throw new Error(msg || 'PowerShell execution failed');
  }
}

/**
 * Check if Outlook COM automation is available on Windows.
 * Result is cached for the lifetime of the process.
 */
let _outlookAvailable = null;
function isOutlookAvailable() {
  if (_outlookAvailable !== null) return _outlookAvailable;
  try {
    runPS("$o = New-Object -ComObject Outlook.Application; [System.Runtime.InteropServices.Marshal]::ReleaseComObject($o) | Out-Null", 10000);
    _outlookAvailable = true;
  } catch {
    _outlookAvailable = false;
  }
  return _outlookAvailable;
}

/**
 * Escape a string for safe embedding in PowerShell single-quoted strings.
 */
function escPS(str) {
  return (str || '').replace(/'/g, "''");
}

/**
 * Calendar tool dispatcher.
 * @param {Object} args - { action, title, start, end, location, notes, calendar, query, days, id }
 */
async function calendar(args) {
  const action = args?.action;
  if (!action) return 'Error: action required. Actions: list_events, create_event, search, list_calendars, delete_event, today, week';

  if (IS_WIN) {
    if (!isOutlookAvailable()) {
      return 'Calendar not available: Microsoft Outlook is not installed. Install Outlook or configure a cloud calendar integration in Settings.';
    }
    switch (action) {
      case 'today': return winListEvents({ ...args, days: 1 });
      case 'week': return winListEvents({ ...args, days: 7 });
      case 'list_events': return winListEvents(args);
      case 'create_event': return winCreateEvent(args);
      case 'search': return winSearchEvents(args);
      case 'list_calendars': return winListCalendars();
      case 'delete_event': return winDeleteEvent(args);
      default:
        return `Error: unknown action '${action}'. Available: list_events, create_event, search, list_calendars, delete_event, today, week`;
    }
  }

  if (IS_MAC) {
    switch (action) {
      case 'today': return listEvents({ ...args, days: 1 });
      case 'week': return listEvents({ ...args, days: 7 });
      case 'list_events': return listEvents(args);
      case 'create_event': return createEvent(args);
      case 'search': return searchEvents(args);
      case 'list_calendars': return listCalendars();
      case 'delete_event': return deleteEvent(args);
      default:
        return `Error: unknown action '${action}'. Available: list_events, create_event, search, list_calendars, delete_event, today, week`;
    }
  }

  if (IS_LINUX) {
    const eds = require('./eds-linux');
    switch (action) {
      case 'today': return eds.edsCalendarList(1, args.calendar);
      case 'week': return eds.edsCalendarList(7, args.calendar);
      case 'list_events': return eds.edsCalendarList(args.days || 7, args.calendar);
      case 'create_event': return eds.edsCalendarCreate(args.title, args.start, args.end, args.location, args.notes, args.calendar);
      case 'search': return eds.edsCalendarSearch(args.query, args.days);
      case 'list_calendars': return eds.edsListCalendars();
      case 'delete_event': return eds.edsCalendarDelete(args.id);
      default:
        return `Error: unknown action '${action}'. Available: list_events, create_event, search, list_calendars, delete_event, today, week`;
    }
  }

  return 'Error: calendar tool is not supported on this platform';
}

function listEvents(args) {
  const days = Math.min(args.days || 7, 90);
  const calName = args.calendar || '';

  // Use EventKit via Swift — dramatically faster than AppleScript for date queries
  return listEventsSwift(days, calName);
}

/**
 * Fast event listing via EventKit (compiled Swift binary).
 * Compiles once to /tmp/pre-cal-events, reuses on subsequent calls.
 */
function listEventsSwift(days, calName) {
  const { execSync } = require('child_process');
  const fs = require('fs');
  const binPath = '/tmp/pre-cal-events';
  const srcPath = '/tmp/pre-cal-events.swift';

  // Compile the helper binary if missing
  if (!fs.existsSync(binPath)) {
    fs.writeFileSync(srcPath, CAL_EVENTS_SWIFT);
    try {
      execSync(`swiftc -O -o ${binPath} ${srcPath} -framework EventKit`, {
        timeout: 60000, encoding: 'utf-8',
      });
    } catch (err) {
      return `Error compiling calendar helper: ${(err.stderr || err.message).trim().split('\n').pop()}`;
    }
  }

  try {
    let cmd = `${binPath} --days ${days}`;
    if (calName) cmd += ` --calendar "${calName}"`;
    const output = execSync(cmd, {
      encoding: 'utf-8',
      timeout: 10000,
      maxBuffer: 256 * 1024,
    }).trim();
    return output || `No events found in the next ${days} day(s)`;
  } catch (err) {
    // If binary is stale/corrupt, remove and let it recompile next time
    try { fs.unlinkSync(binPath); } catch {}
    return `Error listing events: ${(err.stderr || err.message).trim().split('\n').pop()}`;
  }
}

// Swift source for the compiled calendar helper
const CAL_EVENTS_SWIFT = `
import EventKit
import Foundation

let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)

if #available(macOS 14.0, *) {
    store.requestFullAccessToEvents { granted, error in sem.signal() }
} else {
    store.requestAccess(to: .event) { granted, error in sem.signal() }
}
sem.wait()

var days = 7
var targetCal = ""
var mode = "list" // list or search
var query = ""

var args = CommandLine.arguments.dropFirst()
while let arg = args.first {
    args = args.dropFirst()
    switch arg {
    case "--days":
        if let next = args.first { days = Int(next) ?? 7; args = args.dropFirst() }
    case "--calendar":
        if let next = args.first { targetCal = next; args = args.dropFirst() }
    case "--search":
        mode = "search"
        if let next = args.first { query = next; args = args.dropFirst() }
    default: break
    }
}

let now = Calendar.current.startOfDay(for: Date())
let startDate: Date
let endDate: Date

if mode == "search" {
    startDate = Calendar.current.date(byAdding: .day, value: -days, to: now)!
    endDate = Calendar.current.date(byAdding: .day, value: days, to: now)!
} else {
    startDate = now
    endDate = Calendar.current.date(byAdding: .day, value: days, to: now)!
}

let predicate = store.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)
var events = store.events(matching: predicate).sorted { $0.startDate < $1.startDate }

if !targetCal.isEmpty {
    events = events.filter { $0.calendar.title == targetCal }
}

if mode == "search" && !query.isEmpty {
    let q = query.lowercased()
    events = events.filter {
        ($0.title ?? "").lowercased().contains(q) ||
        ($0.location ?? "").lowercased().contains(q)
    }
}

let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd HH:mm"
let tf = DateFormatter()
tf.dateFormat = "HH:mm"

for e in events {
    var line = "UID:\\(e.calendarItemIdentifier) | \\(df.string(from: e.startDate))-\\(tf.string(from: e.endDate)) | \\(e.title ?? "(no title)")"
    if let loc = e.location, !loc.isEmpty { line += " | @ \\(loc)" }
    line += " | [\\(e.calendar.title)]"
    print(line)
}

if events.isEmpty {
    print("No events found")
}
`;

function createEvent(args) {
  const title = args.title;
  const startTime = args.start;
  const endTime = args.end;
  const location = args.location || '';
  const notes = args.notes || '';
  const calName = args.calendar || '';

  if (!title) return 'Error: "title" is required';
  if (!startTime) return 'Error: "start" is required (e.g. "2026-04-15 10:00")';

  const escTitle = title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  const escLocation = location.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  const escNotes = notes.replace(/\\/g, '\\\\').replace(/"/g, '\\"');

  // Parse start/end times — accept ISO-ish formats
  // AppleScript needs: date "April 15, 2026 10:00:00 AM"
  // We'll use a flexible approach: pass to osascript as a parseable string
  const calTarget = calName
    ? `calendar "${calName.replace(/"/g, '\\"')}"`
    : 'first calendar';

  let endDateClause = '';
  if (endTime) {
    endDateClause = `set end date of newEvent to date "${endTime}"`;
  } else {
    // Default: 1 hour duration
    endDateClause = 'set end date of newEvent to (start date of newEvent) + 3600';
  }

  const script = `
tell application "Calendar"
  tell ${calTarget}
    set newEvent to make new event with properties {summary:"${escTitle}", start date:date "${startTime}"}
    ${endDateClause}
    ${location ? `set location of newEvent to "${escLocation}"` : ''}
    ${notes ? `set description of newEvent to "${escNotes}"` : ''}
    set eventUID to uid of newEvent
  end tell
  return "Event created: ${escTitle}\\nUID: " & eventUID
end tell
`;

  try {
    return runAS(script);
  } catch (err) {
    // Common error: date parsing failure. Provide helpful guidance.
    if (err.message.includes('date') || err.message.includes('Can')) {
      return `Error creating event: Could not parse date "${startTime}". Use a format like "April 15, 2026 10:00:00 AM" or "2026-04-15 10:00 AM". Original error: ${err.message}`;
    }
    return `Error creating event: ${err.message}`;
  }
}

function searchEvents(args) {
  const query = args.query;
  const days = Math.min(args.days || 30, 365);

  if (!query) return 'Error: "query" is required for search';

  // Reuse the compiled EventKit binary with --search flag
  const { execSync } = require('child_process');
  const fs = require('fs');
  const binPath = '/tmp/pre-cal-events';

  // Ensure binary exists (compile if needed)
  if (!fs.existsSync(binPath)) {
    // Trigger compilation via listEvents
    listEventsSwift(1, '');
  }

  try {
    const cmd = `${binPath} --days ${days} --search "${query.replace(/"/g, '\\"')}"`;
    const output = execSync(cmd, {
      encoding: 'utf-8',
      timeout: 10000,
      maxBuffer: 256 * 1024,
    }).trim();
    return output || `No events found matching: ${query}`;
  } catch (err) {
    return `Error searching calendar: ${(err.stderr || err.message).trim().split('\n').pop()}`;
  }
}

function listCalendars() {
  const script = `
tell application "Calendar"
  set resultList to {}
  repeat with cal in calendars
    set calInfo to (name of cal) & " | Color: " & (color of cal) & " | Writable: " & (writable of cal)
    try
      set calInfo to calInfo & " | Description: " & (description of cal)
    end try
    set end of resultList to calInfo
  end repeat
  set AppleScript's text item delimiters to "\\n"
  return resultList as text
end tell
`;

  try {
    return runAS(script);
  } catch (err) {
    return `Error listing calendars: ${err.message}`;
  }
}

function deleteEvent(args) {
  const uid = args.id || args.uid;
  if (!uid) return 'Error: "id" (event UID) is required';

  const script = `
tell application "Calendar"
  repeat with cal in calendars
    try
      set evts to (every event of cal whose uid is "${uid}")
      repeat with e in evts
        set evtName to summary of e
        delete e
        return "Deleted event: " & evtName
      end repeat
    end try
  end repeat
  return "Event not found with UID: ${uid}"
end tell
`;

  try {
    return runAS(script);
  } catch (err) {
    return `Error deleting event: ${err.message}`;
  }
}

// ── Windows Outlook COM implementations ──────────────────────────────────────

function winListEvents(args) {
  const days = Math.min(args.days || 7, 90);
  const calName = args.calendar || '';

  const filterClause = calName
    ? `| Where-Object { $_.Parent.Name -eq '${escPS(calName)}' }`
    : '';

  const script = `
$outlook = New-Object -ComObject Outlook.Application
$ns = $outlook.GetNamespace('MAPI')
$folder = $ns.GetDefaultFolder(9)
$now = Get-Date
$end = $now.AddDays(${days})
$restrict = "[Start] >= '" + $now.ToString('g') + "' AND [Start] <= '" + $end.ToString('g') + "'"
$folder.Items.IncludeRecurrences = $true
$folder.Items.Sort('[Start]')
$items = $folder.Items.Restrict($restrict) ${filterClause}
$results = @()
foreach ($item in $items) {
  $start = $item.Start.ToString('yyyy-MM-dd HH:mm')
  $finish = $item.End.ToString('HH:mm')
  $line = "ID:$($item.EntryID.Substring(0,20)) | $start-$finish | $($item.Subject)"
  if ($item.Location) { $line += " | @ $($item.Location)" }
  $line += " | [$($item.Parent.Name)]"
  $results += $line
}
if ($results.Count -eq 0) { Write-Output 'No events found in the next ${days} day(s)' }
else { $results | ForEach-Object { Write-Output $_ } }
`;

  try {
    return runPS(script, 15000);
  } catch (err) {
    return `Error listing events: ${err.message}`;
  }
}

function winCreateEvent(args) {
  const title = args.title;
  const startTime = args.start;
  const endTime = args.end;
  const location = args.location || '';
  const eventNotes = args.notes || '';

  if (!title) return 'Error: "title" is required';
  if (!startTime) return 'Error: "start" is required (e.g. "2026-04-15 10:00")';

  const endClause = endTime
    ? `$appt.End = [DateTime]::Parse('${escPS(endTime)}')`
    : '$appt.End = $appt.Start.AddHours(1)';

  const script = `
$outlook = New-Object -ComObject Outlook.Application
$appt = $outlook.CreateItem(1)
$appt.Subject = '${escPS(title)}'
$appt.Start = [DateTime]::Parse('${escPS(startTime)}')
${endClause}
${location ? `$appt.Location = '${escPS(location)}'` : ''}
${eventNotes ? `$appt.Body = '${escPS(eventNotes)}'` : ''}
$appt.Save()
Write-Output "Event created: ${escPS(title)}"
Write-Output "ID: $($appt.EntryID.Substring(0,20))"
Write-Output "Start: $($appt.Start.ToString('yyyy-MM-dd HH:mm'))"
Write-Output "End: $($appt.End.ToString('yyyy-MM-dd HH:mm'))"
`;

  try {
    return runPS(script);
  } catch (err) {
    return `Error creating event: ${err.message}`;
  }
}

function winSearchEvents(args) {
  const query = args.query;
  const days = Math.min(args.days || 30, 365);

  if (!query) return 'Error: "query" is required for search';

  const script = `
$outlook = New-Object -ComObject Outlook.Application
$ns = $outlook.GetNamespace('MAPI')
$folder = $ns.GetDefaultFolder(9)
$now = Get-Date
$start = $now.AddDays(-${days})
$end = $now.AddDays(${days})
$restrict = "[Start] >= '" + $start.ToString('g') + "' AND [Start] <= '" + $end.ToString('g') + "'"
$folder.Items.IncludeRecurrences = $true
$folder.Items.Sort('[Start]')
$items = $folder.Items.Restrict($restrict)
$q = '${escPS(query)}'.ToLower()
$results = @()
foreach ($item in $items) {
  $subj = if ($item.Subject) { $item.Subject.ToLower() } else { '' }
  $loc = if ($item.Location) { $item.Location.ToLower() } else { '' }
  if ($subj.Contains($q) -or $loc.Contains($q)) {
    $s = $item.Start.ToString('yyyy-MM-dd HH:mm')
    $f = $item.End.ToString('HH:mm')
    $line = "ID:$($item.EntryID.Substring(0,20)) | $s-$f | $($item.Subject)"
    if ($item.Location) { $line += " | @ $($item.Location)" }
    $line += " | [$($item.Parent.Name)]"
    $results += $line
  }
}
if ($results.Count -eq 0) { Write-Output 'No events found matching: ${escPS(query)}' }
else { $results | ForEach-Object { Write-Output $_ } }
`;

  try {
    return runPS(script, 15000);
  } catch (err) {
    return `Error searching calendar: ${err.message}`;
  }
}

function winListCalendars() {
  const script = `
$outlook = New-Object -ComObject Outlook.Application
$ns = $outlook.GetNamespace('MAPI')
$folder = $ns.GetDefaultFolder(9)
$results = @()
$results += "$($folder.Name) | Item count: $($folder.Items.Count) | Default"
foreach ($sub in $folder.Folders) {
  $results += "$($sub.Name) | Item count: $($sub.Items.Count)"
}
# Also check other stores for shared calendars
foreach ($store in $ns.Stores) {
  try {
    $cal = $store.GetDefaultFolder(9)
    if ($cal.FolderPath -ne $folder.FolderPath) {
      $results += "$($cal.Name) | Item count: $($cal.Items.Count) | Store: $($store.DisplayName)"
    }
  } catch {}
}
if ($results.Count -eq 0) { Write-Output 'No calendars found' }
else { $results | ForEach-Object { Write-Output $_ } }
`;

  try {
    return runPS(script);
  } catch (err) {
    return `Error listing calendars: ${err.message}`;
  }
}

function winDeleteEvent(args) {
  const entryId = args.id || args.uid;
  if (!entryId) return 'Error: "id" (event ID) is required';

  const script = `
$outlook = New-Object -ComObject Outlook.Application
$ns = $outlook.GetNamespace('MAPI')
try {
  $item = $ns.GetItemFromID('${escPS(entryId)}')
  $name = $item.Subject
  $item.Delete()
  Write-Output "Deleted event: $name"
} catch {
  # Try searching by partial ID prefix in default calendar
  $folder = $ns.GetDefaultFolder(9)
  $found = $false
  foreach ($item in $folder.Items) {
    if ($item.EntryID.StartsWith('${escPS(entryId)}')) {
      $name = $item.Subject
      $item.Delete()
      Write-Output "Deleted event: $name"
      $found = $true
      break
    }
  }
  if (-not $found) { Write-Output 'Event not found with ID: ${escPS(entryId)}' }
}
`;

  try {
    return runPS(script);
  } catch (err) {
    return `Error deleting event: ${err.message}`;
  }
}

module.exports = { calendar };
