#!/usr/bin/env node
// Calendar CLI — CalDAV client for Nextcloud calendar
// Credentials from .credentials/services.env:
//   NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_PASS, CALDAV_CALENDAR

const fs = require('fs');
const path = require('path');
const https = require('https');

// Load credentials
const credFile = path.join(__dirname, '..', '.credentials', 'services.env');
const env = {};
if (fs.existsSync(credFile)) {
  fs.readFileSync(credFile, 'utf8').split('\n').forEach(l => {
    const m = l.match(/^([A-Z_]+)=(.+)$/);
    if (m) env[m[1]] = m[2].trim();
  });
}

const BASE_URL = env.NEXTCLOUD_URL;
const USER = env.NEXTCLOUD_USER;
const PASS = env.NEXTCLOUD_PASS;
const CALENDAR = env.CALDAV_CALENDAR;

if (!BASE_URL || !USER || !PASS || !CALENDAR) {
  console.error('Missing NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_PASS, or CALDAV_CALENDAR in .credentials/services.env');
  process.exit(1);
}

function caldavRequest(startDate, endDate) {
  const url = new URL(`/remote.php/dav/calendars/${USER}/${CALENDAR}/`, BASE_URL);
  const auth = Buffer.from(`${USER}:${PASS}`).toString('base64');

  // Use calendar-multiget with expand to get expanded recurrences
  const body = `<?xml version="1.0" encoding="UTF-8"?>
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
  <d:prop>
    <c:calendar-data>
      <c:expand start="${startDate}" end="${endDate}"/>
    </c:calendar-data>
  </d:prop>
  <c:filter>
    <c:comp-filter name="VCALENDAR">
      <c:comp-filter name="VEVENT">
        <c:time-range start="${startDate}" end="${endDate}"/>
      </c:comp-filter>
    </c:comp-filter>
  </c:filter>
</c:calendar-query>`;

  return new Promise((resolve, reject) => {
    const opts = {
      method: 'REPORT',
      hostname: url.hostname,
      port: url.port || 443,
      path: url.pathname,
      headers: {
        'Authorization': `Basic ${auth}`,
        'Content-Type': 'application/xml',
        'Depth': '1',
      },
    };
    const req = https.request(opts, res => {
      let data = '';
      res.on('data', c => data += c);
      res.on('end', () => resolve({ status: res.statusCode, data }));
    });
    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

function parseEvents(xmlData) {
  const events = [];
  // Strip carriage returns (raw and XML-encoded) from entire response
  xmlData = xmlData.replace(/\r/g, '').replace(/&#13;/g, '');
  const eventBlocks = xmlData.split('BEGIN:VEVENT');

  for (let i = 1; i < eventBlocks.length; i++) {
    const block = eventBlocks[i].split('END:VEVENT')[0];
    const get = (key) => {
      // Handle both folded and unfolded lines, strip \r
      const regex = new RegExp(`${key}[^:]*:(.+?)(?:\\r?\\n(?! ))`, 's');
      const m = block.match(regex);
      return m ? m[1].replace(/\r?\n\s/g, '').replace(/\r/g, '').trim() : null;
    };

    const summary = get('SUMMARY');
    const dtstart = get('DTSTART');
    const dtend = get('DTEND');
    const location = get('LOCATION');
    const description = get('DESCRIPTION');

    if (summary) {
      events.push({ summary, dtstart, dtend, location, description });
    }
  }

  // Sort by start time
  events.sort((a, b) => (a.dtstart || '').localeCompare(b.dtstart || ''));
  return events;
}

function formatDateTime(dt) {
  if (!dt) return '';
  // Handle YYYYMMDD (all-day)
  if (/^\d{8}$/.test(dt)) {
    return `${dt.slice(0,4)}-${dt.slice(4,6)}-${dt.slice(6,8)} (all day)`;
  }
  // Handle YYYYMMDDTHHMMSSZ or YYYYMMDDTHHMMSS
  const m = dt.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z?$/);
  if (m) {
    const d = new Date(Date.UTC(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +m[6]));
    // Convert to Vienna time
    return d.toLocaleString('de-AT', { timeZone: 'Europe/Vienna', dateStyle: 'short', timeStyle: 'short' });
  }
  return dt;
}

function formatDateRange(start, end) {
  if (!start) return 'unknown time';
  // All-day event
  if (/^\d{8}$/.test(start)) return 'all day';
  const startStr = formatDateTime(start);
  if (!end) return startStr;
  // Same day — only show end time
  const endStr = formatDateTime(end);
  return `${startStr} – ${endStr.split(', ').pop() || endStr}`;
}

function getDateRange(offset, days) {
  const start = new Date();
  start.setUTCHours(0, 0, 0, 0);
  start.setUTCDate(start.getUTCDate() + offset);
  const end = new Date(start);
  end.setUTCDate(end.getUTCDate() + days);

  const fmt = d => d.toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
  return { start: fmt(start), end: fmt(end) };
}

async function main() {
  const args = process.argv.slice(2);
  const cmd = args[0] || 'today';

  // Parse date string (YYYY-MM-DD or natural offsets)
  function parseDate(str) {
    const m = str.match(/^(\d{4})-(\d{2})-(\d{2})$/);
    if (m) {
      const d = new Date(Date.UTC(+m[1], +m[2]-1, +m[3]));
      d.setUTCHours(0, 0, 0, 0);
      return d;
    }
    // Try as number of days offset
    const n = parseInt(str);
    if (!isNaN(n)) {
      const d = new Date();
      d.setUTCHours(0, 0, 0, 0);
      d.setUTCDate(d.getUTCDate() + n);
      return d;
    }
    return null;
  }

  let range;
  let label = cmd;
  const searchQuery = args.slice(1).join(' ').toLowerCase();

  switch (cmd) {
    case 'today':
      range = getDateRange(0, 1);
      break;
    case 'tomorrow':
      range = getDateRange(1, 1);
      break;
    case 'week':
      range = getDateRange(0, 7);
      break;
    case 'next':
      range = getDateRange(0, 14);
      break;
    case 'month':
      range = getDateRange(0, 30);
      break;
    case 'range': {
      // calendar range 2026-02-18 2026-02-25
      const from = args[1] && parseDate(args[1]);
      const to = args[2] && parseDate(args[2]);
      if (!from || !to) {
        console.error('Usage: calendar range <start-date> <end-date>');
        console.error('  Dates: YYYY-MM-DD or offset in days (e.g., 0 = today, 7 = in 7 days)');
        process.exit(1);
      }
      const fmt = d => d.toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
      range = { start: fmt(from), end: fmt(to) };
      label = `${args[1]} to ${args[2]}`;
      break;
    }
    case 'search': {
      // calendar search <query> [days] — search events by name, default 30 days
      if (!searchQuery) {
        console.error('Usage: calendar search <query> [days]');
        console.error('  Searches event summaries/locations. Default: next 30 days.');
        process.exit(1);
      }
      const days = parseInt(args[args.length - 1]);
      const searchDays = (!isNaN(days) && args.length > 2) ? days : 90;
      range = getDateRange(0, searchDays);
      label = `search "${searchQuery}" (${searchDays} days)`;
      break;
    }
    case 'date': {
      // calendar date 2026-02-20
      const d = args[1] && parseDate(args[1]);
      if (!d) {
        console.error('Usage: calendar date <YYYY-MM-DD>');
        process.exit(1);
      }
      const next = new Date(d);
      next.setUTCDate(next.getUTCDate() + 1);
      const fmt = d => d.toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
      range = { start: fmt(d), end: fmt(next) };
      label = args[1];
      break;
    }
    default:
      // Try as date: calendar 2026-02-20
      const asDate = parseDate(cmd);
      if (asDate) {
        const next = new Date(asDate);
        next.setUTCDate(next.getUTCDate() + 1);
        const fmt = d => d.toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
        range = { start: fmt(asDate), end: fmt(next) };
        label = cmd;
        break;
      }
      console.log('Usage: calendar <command>');
      console.log('  today                     — Events for today (default)');
      console.log('  tomorrow                  — Events for tomorrow');
      console.log('  week                      — Next 7 days');
      console.log('  next                      — Next 14 days');
      console.log('  month                     — Next 30 days');
      console.log('  date <YYYY-MM-DD>         — Events on a specific date');
      console.log('  <YYYY-MM-DD>              — Shorthand for date');
      console.log('  range <from> <to>         — Events in date range (YYYY-MM-DD or day offset)');
      console.log('  search <query> [days]     — Search events by name/location (default: 90 days)');
      process.exit(0);
  }

  try {
    const res = await caldavRequest(range.start, range.end);
    if (res.status !== 207 && res.status !== 200) {
      console.error(`CalDAV error: HTTP ${res.status}`);
      process.exit(1);
    }

    let events = parseEvents(res.data);

    // Filter by search query if searching
    if (cmd === 'search' && searchQuery) {
      const q = searchQuery.replace(/\s+\d+$/, ''); // strip trailing days number
      events = events.filter(e =>
        (e.summary || '').toLowerCase().includes(q) ||
        (e.location || '').toLowerCase().includes(q) ||
        (e.description || '').toLowerCase().includes(q)
      );
    }

    if (events.length === 0) {
      console.log(`No events (${label}).`);
      return;
    }

    for (const e of events) {
      const time = formatDateRange(e.dtstart, e.dtend);
      const loc = e.location ? ` 📍 ${e.location}` : '';
      console.log(`${time}\t${e.summary}${loc}`);
    }
    console.log(`\n${events.length} event(s).`);
  } catch (err) {
    console.error('Error:', err.message);
    process.exit(1);
  }
}

main();
