import { DAVClient } from 'tsdav';
import ICAL from 'ical.js';

/**
 * Convert an ICAL.Time to a proper UTC JS Date.
 * When ICAL.js doesn't have VTIMEZONE info, toJSDate() may treat
 * TZID'd times as UTC. This helper detects that case and corrects it
 * using Intl.DateTimeFormat to find the real UTC offset.
 */
function icalTimeToUtcDate(icalTime: any, tzid?: string): Date {
  const jsDate = icalTime.toJSDate();
  // If no TZID, or it's already UTC, or the zone is properly resolved, just return
  if (!tzid || tzid === 'UTC' || tzid === 'Z') return jsDate;
  // Check if ICAL.js actually resolved the timezone (zone !== utcTimezone when resolved)
  if (icalTime.zone && icalTime.zone !== ICAL.Timezone.utcTimezone && icalTime.zone !== ICAL.Timezone.localTimezone) {
    return jsDate; // Properly resolved
  }
  // ICAL.js didn't resolve the TZID — manually convert local time components to UTC
  // icalTime has year, month, day, hour, minute, second as local-in-TZID values
  // but toJSDate() treated them as UTC. We need to find the UTC offset for this TZID.
  try {
    // Create a date string that we can parse in the target timezone
    const localStr = `${icalTime.year}-${String(icalTime.month).padStart(2, '0')}-${String(icalTime.day).padStart(2, '0')}T${String(icalTime.hour).padStart(2, '0')}:${String(icalTime.minute).padStart(2, '0')}:${String(icalTime.second || 0).padStart(2, '0')}`;
    // Find what UTC time corresponds to this local time in the given timezone
    // Use a binary search approach: start with the naive UTC interpretation and adjust
    const naiveUtc = new Date(localStr + 'Z');
    // Get what the local time would be at naiveUtc in the target timezone
    const formatter = new Intl.DateTimeFormat('en-CA', {
      timeZone: tzid, year: 'numeric', month: '2-digit', day: '2-digit',
      hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
    });
    const parts = formatter.formatToParts(naiveUtc);
    const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
    const actualLocal = `${get('year')}-${get('month')}-${get('day')}T${get('hour')}:${get('minute')}:${get('second')}`;
    // The difference between what we wanted and what we got is the offset
    const wantedMs = naiveUtc.getTime();
    const actualMs = new Date(actualLocal + 'Z').getTime();
    const offsetMs = actualMs - wantedMs;
    return new Date(wantedMs - offsetMs);
  } catch {
    return jsDate; // Fallback
  }
}

export interface SynologyCalendarEvent {
  id: string;
  title: string;
  startDate: string;
  endDate: string;
  description?: string;
  location?: string;
  url?: string;
  recurringEventId?: string;
  isRecurring?: boolean;
  reminders?: Array<{ method: string; minutes: number }>;
  attendees?: Array<{ email: string; displayName?: string; responseStatus?: string }>;
  attachments?: Array<{ url: string; title?: string }>;
  busyStatus?: string;
  visibility?: string;
}

/**
 * Format Date to local YYYY-MM-DD string without timezone shift.
 */
function formatDateToLocalISO(date: Date): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

/**
 * Convert reminder minutes to iCalendar TRIGGER duration string
 */
function reminderMinutesToDuration(minutes: number): string {
  if (minutes === 0) return 'PT0S';
  const prefix = '-';
  if (minutes % 10080 === 0) return `${prefix}P${minutes / 10080}W`;
  if (minutes % 1440 === 0) return `${prefix}P${minutes / 1440}D`;
  if (minutes % 60 === 0) return `${prefix}PT${minutes / 60}H`;
  return `${prefix}PT${minutes}M`;
}

/**
 * Extract extended properties from a VEVENT component
 */
function extractExtendedProps(vevent: any): Pick<SynologyCalendarEvent, 'reminders' | 'attendees' | 'attachments' | 'busyStatus' | 'visibility'> {
  const result: Pick<SynologyCalendarEvent, 'reminders' | 'attendees' | 'attachments' | 'busyStatus' | 'visibility'> = {};

  const valarms = vevent.getAllSubcomponents('valarm');
  if (valarms && valarms.length > 0) {
    result.reminders = valarms.map((valarm: any) => {
      const action = valarm.getFirstPropertyValue('action') || 'DISPLAY';
      const trigger = valarm.getFirstProperty('trigger');
      let minutes = 15;
      if (trigger) {
        const triggerVal = trigger.getFirstValue();
        if (triggerVal && typeof triggerVal.toSeconds === 'function') {
          minutes = Math.abs(Math.round(triggerVal.toSeconds() / 60));
        } else if (typeof triggerVal === 'string') {
          const match = triggerVal.match(/^-?PT?(\d+)([MHDS])/i);
          if (match) {
            const val = parseInt(match[1]);
            switch (match[2].toUpperCase()) {
              case 'M': minutes = val; break;
              case 'H': minutes = val * 60; break;
              case 'D': minutes = val * 1440; break;
              case 'S': minutes = Math.round(val / 60); break;
            }
          }
          const weekMatch = triggerVal.match(/^-?P(\d+)W/i);
          if (weekMatch) minutes = parseInt(weekMatch[1]) * 10080;
          const dayMatch = triggerVal.match(/^-?P(\d+)D/i);
          if (dayMatch) minutes = parseInt(dayMatch[1]) * 1440;
        }
      }
      return { method: action.toString().toLowerCase() === 'email' ? 'email' : 'display', minutes };
    });
  }

  const attendeeProps = vevent.getAllProperties('attendee');
  if (attendeeProps && attendeeProps.length > 0) {
    result.attendees = attendeeProps.map((prop: any) => {
      const val = prop.getFirstValue() || '';
      const email = val.replace(/^mailto:/i, '');
      const cn = prop.getParameter('cn');
      const partstat = prop.getParameter('partstat');
      const statusMap: Record<string, string> = {
        'ACCEPTED': 'accepted', 'DECLINED': 'declined', 'TENTATIVE': 'tentative', 'NEEDS-ACTION': 'needsAction',
      };
      return { email, displayName: cn || undefined, responseStatus: statusMap[partstat?.toUpperCase()] || 'needsAction' };
    });
  }

  const attachProps = vevent.getAllProperties('attach');
  if (attachProps && attachProps.length > 0) {
    result.attachments = attachProps
      .map((prop: any) => {
        const val = prop.getFirstValue();
        if (typeof val === 'string' && (val.startsWith('http://') || val.startsWith('https://'))) {
          return { url: val, title: prop.getParameter('filename') || undefined };
        }
        return null;
      })
      .filter(Boolean) as Array<{ url: string; title?: string }>;
    if (result.attachments.length === 0) delete result.attachments;
  }

  const transp = vevent.getFirstPropertyValue('transp');
  if (transp) result.busyStatus = transp.toString().toUpperCase() === 'TRANSPARENT' ? 'free' : 'busy';

  const cls = vevent.getFirstPropertyValue('class');
  if (cls) {
    const clsMap: Record<string, string> = { 'PUBLIC': 'public', 'PRIVATE': 'private', 'CONFIDENTIAL': 'confidential' };
    result.visibility = (clsMap[cls.toString().toUpperCase()] || 'default') as any;
  }

  return result;
}

export interface SynologyCalendar {
  id: string;
  title: string;
  color?: string;
  isPrimary?: boolean;
}

/**
 * Create a configured DAV client for Synology Calendar
 */
const createClient = (serverUrl: string, username: string, password: string) => {
  let url = serverUrl.replace(/\/$/, '');
  if (!url.includes('/caldav')) {
    url = `${url}/caldav/${username}`;
  }
  return new DAVClient({
    serverUrl: url,
    credentials: {
      username,
      password,
    },
    authMethod: 'Basic',
    defaultAccountType: 'caldav',
  });
};

/**
 * Login with fallback: Synology's CalDAV doesn't support well-known discovery,
 * so if login() fails with "cannot find homeUrl", retry with the serverUrl as homeUrl.
 */
const loginClient = async (client: DAVClient, serverUrl: string, username: string) => {
  try {
    await client.login();
  } catch (err: any) {
    if (err?.message?.includes('homeUrl')) {
      // Synology doesn't support .well-known discovery — derive homeUrl from serverUrl
      let homeUrl = serverUrl.replace(/\/$/, '');
      if (!homeUrl.includes('/caldav')) {
        homeUrl = `${homeUrl}/caldav/${username}`;
      }
      // Strip any calendar-specific path segments (UUID), keep up to username
      const match = homeUrl.match(/^(https?:\/\/[^/]+\/caldav(?:\.php)?\/[^/]+)\/?/);
      if (match) homeUrl = match[1] + '/';

      console.log('[SYNOLOGY CALENDAR] Well-known discovery failed, retrying with homeUrl:', homeUrl);
      (client as any).account = {
        serverUrl: client.serverUrl,
        accountType: 'caldav',
        homeUrl,
        rootUrl: homeUrl, // tsdav requires rootUrl for fetchCalendars
      };
    } else {
      throw err;
    }
  }
};

/**
 * Validate credentials by attempting to fetch calendars
 * @returns List of found calendars if successful
 */
export const validateCredentials = async (serverUrl: string, username: string, password: string): Promise<SynologyCalendar[]> => {
  try {
    const client = createClient(serverUrl, username, password);
    await loginClient(client, serverUrl, username);

    const calendars = await client.fetchCalendars();

    // Filter to VEVENT-only calendars — exclude VTODO (Reminders) collections
    const eventCalendars = calendars.filter(cal => {
      const components: string[] = (cal as any).components || [];
      // Keep if components include VEVENT, or if components is empty/undefined
      if (components.length === 0) return true;
      return components.includes('VEVENT');
    });

    const mappedCalendars = eventCalendars.map(cal => ({
      id: cal.url, // Using URL as ID for CalDAV
      title: (cal.displayName as string) || 'Untitled Calendar',
      color: cal.calendarColor,
      isPrimary: false,
    }));

    return mappedCalendars;
  } catch (error) {
    console.error('Synology Calendar validation failed:', error);
    throw new Error('Invalid credentials or unable to connect to Synology Server.');
  }
};

/**
 * Get user calendars (wrapper around validateCredentials for now as they do the same)
 */
export const getUserCalendars = validateCredentials;

/**
 * Get upcoming events for a specified time period from a specific calendar
 */
export const getUpcomingEvents = async (
  serverUrl: string,
  username: string,
  password: string,
  calendarUrl: string,
  timeMin: string,
  timeMax: string
): Promise<SynologyCalendarEvent[]> => {
  try {
    const client = createClient(serverUrl, username, password);
    await loginClient(client, serverUrl, username);

    const calendars = await client.fetchCalendars();
    const getPath = (url: string) => {
      try { return new URL(url).pathname; } catch { return url; }
    };
    const targetPath = getPath(calendarUrl);
    const targetCalendar = calendars.find(c => getPath(c.url) === targetPath);

    if (!targetCalendar) {
      throw new Error(`Calendar not found: ${calendarUrl}`);
    }

    const events = await client.fetchCalendarObjects({
      calendar: targetCalendar,
      timeRange: {
        start: new Date(timeMin).toISOString(),
        end: new Date(timeMax).toISOString(),
      },
    });

    const parsedEvents: SynologyCalendarEvent[] = [];
    const minTime = new Date(timeMin).getTime();
    const maxTime = new Date(timeMax).getTime();

    events.forEach(eventObj => {
      const data = (eventObj as any).data;
      if (!data) return;

      try {
        const jcalData = ICAL.parse(data);
        const comp = new ICAL.Component(jcalData);
        const vevents = comp.getAllSubcomponents('vevent');

        // Separate base events (with RRULE) from recurrence exceptions (with RECURRENCE-ID)
        const baseEvents: any[] = [];
        const exceptions: Map<string, any[]> = new Map();

        vevents.forEach((vevent: any) => {
          const recurrenceId = vevent.getFirstPropertyValue('recurrence-id');
          if (recurrenceId) {
            const event = new ICAL.Event(vevent);
            const uid = event.uid;
            if (!exceptions.has(uid)) exceptions.set(uid, []);
            exceptions.get(uid)!.push(vevent);
          } else {
            baseEvents.push(vevent);
          }
        });

        baseEvents.forEach((vevent: any) => {
          const event = new ICAL.Event(vevent);
          const rrule = vevent.getFirstPropertyValue('rrule');

          if (rrule) {
            const uid = event.uid;
            const exceptionVevents = exceptions.get(uid) || [];
            const exceptionDates = new Set<string>();
            const isAllDayRecurring = event.startDate.isDate === true;

            exceptionVevents.forEach((exVevent: any) => {
              const exEvent = new ICAL.Event(exVevent);
              const exDtStartProp = exVevent.getFirstProperty('dtstart');
              const exTzid = exDtStartProp?.getParameter('tzid') as string | undefined;
              const recId = exVevent.getFirstPropertyValue('recurrence-id');
              if (recId) {
                exceptionDates.add(icalTimeToUtcDate(recId, exTzid).toISOString());
              }

              const exStart = icalTimeToUtcDate(exEvent.startDate, exTzid);
              const exEnd = icalTimeToUtcDate(exEvent.endDate, exTzid);
              const exIsAllDay = exEvent.startDate.isDate === true;

              if (exEnd.getTime() >= minTime && exStart.getTime() <= maxTime) {
                parsedEvents.push({
                  id: `caldav::${eventObj.url}::${exEvent.uid}::${exStart.toISOString()}`,
                  title: exEvent.summary || 'Untitled Event',
                  startDate: exIsAllDay ? formatDateToLocalISO(exStart) : exStart.toISOString(),
                  endDate: exIsAllDay ? formatDateToLocalISO(exEnd) : exEnd.toISOString(),
                  description: exEvent.description,
                  location: exEvent.location,
                  url: exVevent.getFirstPropertyValue('url')?.toString() || undefined,
                  recurringEventId: exEvent.uid,
                  isRecurring: true,
                  ...extractExtendedProps(exVevent),
                });
              }
            });

            try {
              const dtStartProp = vevent.getFirstProperty('dtstart');
              const dtStartTzid = dtStartProp?.getParameter('tzid') as string | undefined;
              const startUtc = icalTimeToUtcDate(event.startDate, dtStartTzid);
              const endUtc = icalTimeToUtcDate(event.endDate, dtStartTzid);
              const duration = endUtc.getTime() - startUtc.getTime();
              const iter = event.iterator();
              let next;
              let safetyCount = 0;

              while ((next = iter.next()) && safetyCount < 500) {
                safetyCount++;
                const occStart = icalTimeToUtcDate(next, dtStartTzid);
                const occEnd = new Date(occStart.getTime() + duration);

                if (occStart.getTime() > maxTime) break;
                if (occEnd.getTime() < minTime) continue;
                if (exceptionDates.has(occStart.toISOString())) continue;

                parsedEvents.push({
                  id: `caldav::${eventObj.url}::${event.uid}::${occStart.toISOString()}`,
                  title: event.summary || 'Untitled Event',
                  startDate: isAllDayRecurring ? formatDateToLocalISO(occStart) : occStart.toISOString(),
                  endDate: isAllDayRecurring ? formatDateToLocalISO(occStart) : occEnd.toISOString(),
                  description: event.description,
                  location: event.location,
                  url: vevent.getFirstPropertyValue('url')?.toString() || undefined,
                  recurringEventId: event.uid,
                  isRecurring: true,
                  ...extractExtendedProps(vevent),
                });
              }
            } catch (expandErr: any) {
              console.error(`[SYNOLOGY CALENDAR] Error expanding recurrence for "${event.summary}":`, expandErr);
            }
          } else {
            const nrDtStartProp = vevent.getFirstProperty('dtstart');
            const nrTzid = nrDtStartProp?.getParameter('tzid') as string | undefined;
            const start = icalTimeToUtcDate(event.startDate, nrTzid);
            const end = icalTimeToUtcDate(event.endDate, nrTzid);
            const isAllDay = event.startDate.isDate === true;

            if (end.getTime() < minTime || start.getTime() > maxTime) return;

            parsedEvents.push({
              id: `caldav::${eventObj.url}::${event.uid || 'unknown'}`,
              title: event.summary || 'Untitled Event',
              startDate: isAllDay ? formatDateToLocalISO(start) : start.toISOString(),
              endDate: isAllDay ? formatDateToLocalISO(end) : end.toISOString(),
              description: event.description,
              location: event.location,
              url: vevent.getFirstPropertyValue('url')?.toString() || undefined,
              ...extractExtendedProps(vevent),
            });
          }
        });
      } catch (parseErr: any) {
        console.error(`[SYNOLOGY CALENDAR] Error parsing event data for calendar ${calendarUrl}:`, parseErr);
      }
    });

    return parsedEvents;
  } catch (error: any) {
    const msg = error?.message || '';
    const status = error?.response?.status || error?.status;
    // Re-throw 404 so callers can prune deleted calendars
    if (status === 404 || msg.includes('not found') || msg.includes('Calendar not found')) {
      console.warn(`[SYNOLOGY CALENDAR] Calendar not found: ${calendarUrl}`);
      throw error;
    }
    // Silently handle other expected failures (permission issues, method not allowed)
    if (status === 405 || msg.includes('405') || msg.includes('Not Allowed')) {
      console.warn(`[SYNOLOGY CALENDAR] Calendar unavailable (${status || msg.slice(0, 60)}): ${calendarUrl}`);
    } else {
      console.error(`[SYNOLOGY CALENDAR] Error fetching events for ${calendarUrl}:`, error);
    }
    return [];
  }
};

/**
 * Batch fetch events from multiple Synology calendars using a single client/login.
 * Avoids creating N separate connections for N calendars.
 */
export const getUpcomingEventsBatch = async (
  serverUrl: string,
  username: string,
  password: string,
  calendarUrls: string[],
  timeMin: string,
  timeMax: string
): Promise<Map<string, SynologyCalendarEvent[]>> => {
  const results = new Map<string, SynologyCalendarEvent[]>();
  if (calendarUrls.length === 0) return results;

  try {
    const client = createClient(serverUrl, username, password);
    await loginClient(client, serverUrl, username);
    const calendars = await client.fetchCalendars();

    const getPath = (url: string) => {
      try { return new URL(url).pathname; } catch { return url; }
    };

    const minTime = new Date(timeMin).getTime();
    const maxTime = new Date(timeMax).getTime();

    for (const calendarUrl of calendarUrls) {
      try {
        const targetPath = getPath(calendarUrl);
        const targetCalendar = calendars.find(c => getPath(c.url) === targetPath);
        if (!targetCalendar) {
          console.log(`[SYNOLOGY CALENDAR] Calendar not found: ${calendarUrl}`);
          results.set(calendarUrl, []);
          continue;
        }

        const events = await client.fetchCalendarObjects({
          calendar: targetCalendar,
          timeRange: {
            start: new Date(timeMin).toISOString(),
            end: new Date(timeMax).toISOString(),
          },
        });

        const parsedEvents: SynologyCalendarEvent[] = [];

        events.forEach(eventObj => {
          const data = (eventObj as any).data;
          if (!data) return;
          try {
            const jcalData = ICAL.parse(data);
            const comp = new ICAL.Component(jcalData);
            const vevents = comp.getAllSubcomponents('vevent');
            const baseEvents: any[] = [];
            const exceptions: Map<string, any[]> = new Map();

            vevents.forEach((vevent: any) => {
              const recurrenceId = vevent.getFirstPropertyValue('recurrence-id');
              if (recurrenceId) {
                const event = new ICAL.Event(vevent);
                const uid = event.uid;
                if (!exceptions.has(uid)) exceptions.set(uid, []);
                exceptions.get(uid)!.push(vevent);
              } else {
                baseEvents.push(vevent);
              }
            });

            baseEvents.forEach((vevent: any) => {
              const event = new ICAL.Event(vevent);
              const rrule = vevent.getFirstPropertyValue('rrule');

              if (rrule) {
                const uid = event.uid;
                const exceptionVevents = exceptions.get(uid) || [];
                const exceptionDates = new Set<string>();
                const isAllDayRecurring = event.startDate.isDate === true;

                exceptionVevents.forEach((exVevent: any) => {
                  const exEvent = new ICAL.Event(exVevent);
                  const exDtStartProp = exVevent.getFirstProperty('dtstart');
                  const exTzid = exDtStartProp?.getParameter('tzid') as string | undefined;
                  const recId = exVevent.getFirstPropertyValue('recurrence-id');
                  if (recId) exceptionDates.add(icalTimeToUtcDate(recId, exTzid).toISOString());

                  const exStart = icalTimeToUtcDate(exEvent.startDate, exTzid);
                  const exEnd = icalTimeToUtcDate(exEvent.endDate, exTzid);
                  const exIsAllDay = exEvent.startDate.isDate === true;

                  if (exEnd.getTime() >= minTime && exStart.getTime() <= maxTime) {
                    parsedEvents.push({
                      id: `caldav::${eventObj.url}::${exEvent.uid}::${exStart.toISOString()}`,
                      title: exEvent.summary || 'Untitled Event',
                      startDate: exIsAllDay ? formatDateToLocalISO(exStart) : exStart.toISOString(),
                      endDate: exIsAllDay ? formatDateToLocalISO(exEnd) : exEnd.toISOString(),
                      description: exEvent.description,
                      location: exEvent.location,
                      url: exVevent.getFirstPropertyValue('url')?.toString() || undefined,
                      recurringEventId: exEvent.uid,
                      isRecurring: true,
                      ...extractExtendedProps(exVevent),
                    });
                  }
                });

                try {
                  const dtStartProp = vevent.getFirstProperty('dtstart');
                  const dtStartTzid = dtStartProp?.getParameter('tzid') as string | undefined;
                  const startUtc = icalTimeToUtcDate(event.startDate, dtStartTzid);
                  const endUtc = icalTimeToUtcDate(event.endDate, dtStartTzid);
                  const duration = endUtc.getTime() - startUtc.getTime();
                  const iter = event.iterator();
                  let next;
                  let safetyCount = 0;

                  while ((next = iter.next()) && safetyCount < 500) {
                    safetyCount++;
                    const occStart = icalTimeToUtcDate(next, dtStartTzid);
                    const occEnd = new Date(occStart.getTime() + duration);

                    if (occStart.getTime() > maxTime) break;
                    if (occEnd.getTime() < minTime) continue;
                    if (exceptionDates.has(occStart.toISOString())) continue;

                    parsedEvents.push({
                      id: `caldav::${eventObj.url}::${event.uid}::${occStart.toISOString()}`,
                      title: event.summary || 'Untitled Event',
                      startDate: isAllDayRecurring ? formatDateToLocalISO(occStart) : occStart.toISOString(),
                      endDate: isAllDayRecurring ? formatDateToLocalISO(occStart) : occEnd.toISOString(),
                      description: event.description,
                      location: event.location,
                      url: vevent.getFirstPropertyValue('url')?.toString() || undefined,
                      recurringEventId: event.uid,
                      isRecurring: true,
                      ...extractExtendedProps(vevent),
                    });
                  }
                } catch (expandErr: any) {
                  console.error(`[SYNOLOGY CALENDAR] Error expanding recurrence for "${event.summary}":`, expandErr);
                }
              } else {
                const nrDtStartProp = vevent.getFirstProperty('dtstart');
                const nrTzid = nrDtStartProp?.getParameter('tzid') as string | undefined;
                const start = icalTimeToUtcDate(event.startDate, nrTzid);
                const end = icalTimeToUtcDate(event.endDate, nrTzid);
                const isAllDay = event.startDate.isDate === true;

                if (end.getTime() < minTime || start.getTime() > maxTime) return;

                parsedEvents.push({
                  id: `caldav::${eventObj.url}::${event.uid || 'unknown'}`,
                  title: event.summary || 'Untitled Event',
                  startDate: isAllDay ? formatDateToLocalISO(start) : start.toISOString(),
                  endDate: isAllDay ? formatDateToLocalISO(end) : end.toISOString(),
                  description: event.description,
                  location: event.location,
                  url: vevent.getFirstPropertyValue('url')?.toString() || undefined,
                  ...extractExtendedProps(vevent),
                });
              }
            });
          } catch (parseErr: any) {
            console.error(`[SYNOLOGY CALENDAR] Error parsing event data for calendar ${calendarUrl}:`, parseErr);
          }
        });

        results.set(calendarUrl, parsedEvents);
      } catch (calErr: any) {
        const msg = calErr?.message || '';
        const status = calErr?.response?.status || calErr?.status;
        if (status === 404 || msg.includes('not found') || msg.includes('Calendar not found')) {
          console.warn(`[SYNOLOGY CALENDAR] Calendar not found in batch: ${calendarUrl}`);
          results.set(calendarUrl, []);
        } else {
          console.error(`[SYNOLOGY CALENDAR] Batch error for ${calendarUrl}:`, calErr);
          results.set(calendarUrl, []);
        }
      }
    }

    return results;
  } catch (error: any) {
    console.error('[SYNOLOGY CALENDAR] Batch fetch failed:', error);
    // Return empty results for all calendars
    calendarUrls.forEach(url => results.set(url, []));
    return results;
  }
};

/**
 * Create a new event in the specified calendar
 */
export const createEvent = async (
  serverUrl: string,
  username: string,
  password: string,
  calendarUrl: string,
  eventData: {
    title: string;
    description?: string;
    location?: string;
    url?: string;
    recurrence?: string;
    recurrenceEndDate?: string;
    recurrenceCount?: number;
    recurrenceInterval?: number;
    recurrenceDays?: number[];
    timezone?: string;
    start: { dateTime?: string; date?: string };
    end: { dateTime?: string; date?: string };
    reminders?: Array<{ method: string; minutes: number }>;
    attendees?: Array<{ email: string; displayName?: string }>;
    attachments?: Array<{ url: string; title?: string }>;
    busyStatus?: string;
    visibility?: string;
  }
): Promise<SynologyCalendarEvent> => {
  try {
    const client = createClient(serverUrl, username, password);
    await loginClient(client, serverUrl, username);

    const calendars = await client.fetchCalendars();
    const targetCalendar = calendars.find(c => c.url === calendarUrl);

    if (!targetCalendar) {
      throw new Error(`Calendar not found: ${calendarUrl}`);
    }

    const now = new Date();
    const uid = crypto.randomUUID();

    const dtStamp = now.toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z';

    let dtStart = '';
    let dtEnd = '';
    let dtStartParam = '';
    let dtEndParam = '';

    if (eventData.start.date) {
      dtStart = eventData.start.date.replace(/-/g, '');
      dtEnd = eventData.end.date ? eventData.end.date.replace(/-/g, '') : dtStart; // Fallback
      dtStartParam = ';VALUE=DATE';
      dtEndParam = ';VALUE=DATE';
    } else if (eventData.start.dateTime) {
      // For recurring events, use local time with TZID to avoid DST shifts
      if (eventData.recurrence && eventData.timezone) {
        const tz = eventData.timezone;
        const toLocalIcal = (isoStr: string) => {
          const d = new Date(isoStr);
          const parts = new Intl.DateTimeFormat('en-CA', {
            timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit',
            hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
          }).formatToParts(d);
          const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
          return `${get('year')}${get('month')}${get('day')}T${get('hour')}${get('minute')}${get('second')}`;
        };
        dtStart = toLocalIcal(eventData.start.dateTime);
        dtEnd = eventData.end.dateTime ? toLocalIcal(eventData.end.dateTime) : dtStart;
        dtStartParam = `;TZID=${tz}`;
        dtEndParam = `;TZID=${tz}`;
      } else {
        dtStart = new Date(eventData.start.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z';
        dtEnd = eventData.end.dateTime
          ? new Date(eventData.end.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z'
          : dtStart;
      }
    }

    const description = eventData.description ? `DESCRIPTION:${eventData.description.replace(/\n/g, '\\n')}\r\n` : '';
    const location = eventData.location ? `LOCATION:${eventData.location.replace(/,/g, '\\,')}\r\n` : '';
    const url = eventData.url ? `URL:${eventData.url}\r\n` : '';
    let rruleLine = '';
    if (eventData.recurrence) {
      const rruleMap: Record<string, string> = {
        daily: 'RRULE:FREQ=DAILY',
        weekly: 'RRULE:FREQ=WEEKLY',
        biweekly: 'RRULE:FREQ=WEEKLY;INTERVAL=2',
        monthly: 'RRULE:FREQ=MONTHLY',
        yearly: 'RRULE:FREQ=YEARLY',
      };
      if (rruleMap[eventData.recurrence]) {
        let rrule = rruleMap[eventData.recurrence];
        if (eventData.recurrenceInterval && eventData.recurrenceInterval > 1 && eventData.recurrence !== 'biweekly') {
          rrule += `;INTERVAL=${eventData.recurrenceInterval}`;
        }
        if (eventData.recurrenceDays && eventData.recurrenceDays.length > 0 && eventData.recurrence === 'weekly') {
          const dayMap = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
          rrule += `;BYDAY=${eventData.recurrenceDays.map(d => dayMap[d]).join(',')}`;
        }
        if (eventData.recurrenceCount && eventData.recurrenceCount > 0) {
          rrule += `;COUNT=${eventData.recurrenceCount}`;
        } else if (eventData.recurrenceEndDate) {
          const d = new Date(eventData.recurrenceEndDate);
          d.setHours(23, 59, 59);
          rrule += `;UNTIL=${d.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`;
        }
        rruleLine = `${rrule}\r\n`;
      }
    }

    // Generate VALARM blocks for reminders
    let valarmLines = '';
    if (eventData.reminders && eventData.reminders.length > 0) {
      for (const reminder of eventData.reminders) {
        const action = reminder.method === 'email' ? 'EMAIL' : 'DISPLAY';
        const dur = reminderMinutesToDuration(reminder.minutes);
        valarmLines += `BEGIN:VALARM\r\nACTION:${action}\r\nTRIGGER:${dur}\r\n`;
        if (action === 'DISPLAY') valarmLines += `DESCRIPTION:Reminder\r\n`;
        valarmLines += `END:VALARM\r\n`;
      }
    }

    let attendeeLines = '';
    if (eventData.attendees && eventData.attendees.length > 0) {
      for (const att of eventData.attendees) {
        const cn = att.displayName ? `;CN=${att.displayName}` : '';
        attendeeLines += `ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION${cn}:mailto:${att.email}\r\n`;
      }
    }

    let attachLines = '';
    if (eventData.attachments && eventData.attachments.length > 0) {
      for (const att of eventData.attachments) {
        attachLines += `ATTACH:${att.url}\r\n`;
      }
    }

    const transpLine = eventData.busyStatus === 'free' ? 'TRANSP:TRANSPARENT\r\n' : eventData.busyStatus ? 'TRANSP:OPAQUE\r\n' : '';
    let classLine = '';
    if (eventData.visibility && eventData.visibility !== 'default') {
      classLine = `CLASS:${eventData.visibility.toUpperCase()}\r\n`;
    }

    const iCalString = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//My Weekly ToDo List//EN
BEGIN:VEVENT
UID:${uid}
DTSTAMP:${dtStamp}
DTSTART${dtStartParam}:${dtStart}
DTEND${dtEndParam}:${dtEnd}
SUMMARY:${eventData.title}
${description}${location}${url}${rruleLine}${transpLine}${classLine}${attendeeLines}${attachLines}${valarmLines}END:VEVENT
END:VCALENDAR`;

    console.log('[SYNOLOGY CALENDAR] Creating event with iCal:', iCalString);

    const filename = `${uid}.ics`;

    await client.createCalendarObject({
      calendar: targetCalendar,
      filename,
      iCalString
    });

    return {
      id: `${uid}-${filename}`,
      title: eventData.title,
      startDate: eventData.start.dateTime || eventData.start.date || '',
      endDate: eventData.end.dateTime || eventData.end.date || '',
      description: eventData.description,
      location: eventData.location,
      url: eventData.url
    };
  } catch (error) {
    console.error('[SYNOLOGY CALENDAR] Error creating event:', error);
    throw error;
  }
};

/**
 * Parse the new caldav:: ID format to extract the object URL and UID.
 */
function parseCaldavId(eventId: string): { objectUrl: string; uid: string } | null {
  if (!eventId.startsWith('caldav::')) return null;
  const parts = eventId.split('::');
  if (parts.length >= 3) {
    return { objectUrl: parts[1], uid: parts[2] };
  }
  return null;
}

/**
 * Update an existing event
 */
export const updateEvent = async (
  serverUrl: string,
  username: string,
  password: string,
  calendarUrl: string,
  eventId: string,
  eventData: {
    title?: string;
    description?: string;
    location?: string;
    url?: string;
    start?: { dateTime?: string; date?: string };
    end?: { dateTime?: string; date?: string };
    reminders?: Array<{ method: string; minutes: number }>;
    attendees?: Array<{ email: string; displayName?: string }>;
    attachments?: Array<{ url: string; title?: string }>;
    busyStatus?: string;
    visibility?: string;
  }
): Promise<SynologyCalendarEvent> => {
  try {
    const client = createClient(serverUrl, username, password);
    await loginClient(client, serverUrl, username);

    let targetObject: any = null;

    const parsed = parseCaldavId(eventId);
    if (parsed) {
      const objects = await client.fetchCalendarObjects({
        calendar: { url: calendarUrl } as any,
        objectUrls: [parsed.objectUrl],
      });
      targetObject = objects?.[0] || null;
      if (!targetObject) {
        throw new Error('Event not found on server at URL: ' + parsed.objectUrl);
      }
    } else {
      const calendars = await client.fetchCalendars();
      const targetCalendar = calendars.find(c => c.url === calendarUrl);

      if (!targetCalendar) {
        throw new Error(`Calendar not found: ${calendarUrl}`);
      }

      const uid = eventId.split('-')[0];

      const allObjects = await client.fetchCalendarObjects({
        calendar: targetCalendar,
      });

      targetObject = allObjects.find(obj => {
        if (obj.data) {
          return obj.data.includes(`UID:${uid}`);
        }
        return obj.url.includes(uid);
      });

      if (!targetObject) {
        throw new Error('Event not found on server');
      }
    }

    const jcal = ICAL.parse(targetObject.data);
    const comp = new ICAL.Component(jcal);
    const vevent = comp.getFirstSubcomponent('vevent');

    if (!vevent) {
      throw new Error('No VEVENT found in calendar object');
    }
    const event = new ICAL.Event(vevent);

    if (eventData.title) event.summary = eventData.title;
    if (eventData.description) event.description = eventData.description;
    if (eventData.location) event.location = eventData.location;

    if (eventData.url !== undefined) {
      if (eventData.url) {
        vevent.updatePropertyWithValue('url', eventData.url);
      } else {
        vevent.removeProperty('url');
      }
    }

    // Check if existing event uses TZID (for recurring events with DST-safe times)
    const existingDtStart = vevent.getFirstProperty('dtstart');
    const existingTzid = existingDtStart?.getParameter('tzid') as string | undefined;

    const toLocalIcalTime = (isoStr: string, tzid: string) => {
      const d = new Date(isoStr);
      const parts = new Intl.DateTimeFormat('en-CA', {
        timeZone: tzid, year: 'numeric', month: '2-digit', day: '2-digit',
        hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
      }).formatToParts(d);
      const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
      const tz = ICAL.Timezone.fromData({ tzid, component: comp.getFirstSubcomponent('vtimezone') || undefined });
      return ICAL.Time.fromData({
        year: parseInt(get('year')), month: parseInt(get('month')), day: parseInt(get('day')),
        hour: parseInt(get('hour')), minute: parseInt(get('minute')), second: parseInt(get('second')),
        isDate: false,
      }, tz);
    };

    if (eventData.start) {
      if (eventData.start.date) {
        event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
        event.startDate.isDate = true;
      } else if (eventData.start.dateTime) {
        if (existingTzid) {
          event.startDate = toLocalIcalTime(eventData.start.dateTime, existingTzid);
          const dtStartProp = vevent.getFirstProperty('dtstart');
          if (dtStartProp) dtStartProp.setParameter('tzid', existingTzid);
        } else {
          event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
          event.startDate.isDate = false;
        }
      }
    }

    if (eventData.end) {
      if (eventData.end.date) {
        event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.date), true);
        event.endDate.isDate = true;
      } else if (eventData.end.dateTime) {
        if (existingTzid) {
          event.endDate = toLocalIcalTime(eventData.end.dateTime, existingTzid);
          const dtEndProp = vevent.getFirstProperty('dtend');
          if (dtEndProp) dtEndProp.setParameter('tzid', existingTzid);
        } else {
          event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
          event.endDate.isDate = false;
        }
      }
    }

    // Update reminders (VALARM)
    if (eventData.reminders !== undefined) {
      const existingAlarms = vevent.getAllSubcomponents('valarm');
      existingAlarms.forEach((a: any) => vevent.removeSubcomponent(a));
      for (const reminder of eventData.reminders) {
        const valarm = new ICAL.Component('valarm');
        valarm.addPropertyWithValue('action', reminder.method === 'email' ? 'EMAIL' : 'DISPLAY');
        const dur = ICAL.Duration.fromString(reminderMinutesToDuration(reminder.minutes));
        valarm.addPropertyWithValue('trigger', dur);
        if (reminder.method !== 'email') valarm.addPropertyWithValue('description', 'Reminder');
        vevent.addSubcomponent(valarm);
      }
    }

    if (eventData.attendees !== undefined) {
      vevent.removeAllProperties('attendee');
      for (const att of eventData.attendees) {
        const prop = new ICAL.Property('attendee');
        prop.setValue(`mailto:${att.email}`);
        prop.setParameter('cutype', 'INDIVIDUAL');
        prop.setParameter('role', 'REQ-PARTICIPANT');
        prop.setParameter('partstat', 'NEEDS-ACTION');
        if (att.displayName) prop.setParameter('cn', att.displayName);
        vevent.addProperty(prop);
      }
    }

    if (eventData.attachments !== undefined) {
      vevent.removeAllProperties('attach');
      for (const att of eventData.attachments) {
        vevent.addPropertyWithValue('attach', att.url);
      }
    }

    if (eventData.busyStatus !== undefined) {
      if (eventData.busyStatus === 'free') vevent.updatePropertyWithValue('transp', 'TRANSPARENT');
      else if (eventData.busyStatus) vevent.updatePropertyWithValue('transp', 'OPAQUE');
    }

    if (eventData.visibility !== undefined) {
      if (eventData.visibility && eventData.visibility !== 'default') {
        vevent.updatePropertyWithValue('class', eventData.visibility.toUpperCase());
      } else {
        vevent.removeProperty('class');
      }
    }

    event.sequence = (event.sequence || 0) + 1;
    if (vevent) {
      vevent.updatePropertyWithValue('dtstamp', ICAL.Time.now());
    }

    const updatedIcalString = comp.toString();
    console.log('[SYNOLOGY CALENDAR] Updating event with iCal:', updatedIcalString);

    await client.updateCalendarObject({
      calendarObject: {
        ...targetObject,
        data: updatedIcalString,
      },
    });

    return {
      id: eventId,
      title: event.summary,
      startDate: event.startDate.toString(),
      endDate: event.endDate.toString(),
      description: event.description,
      location: event.location,
      url: vevent.getFirstPropertyValue('url')?.toString() || undefined
    };

  } catch (error) {
    console.error('[SYNOLOGY CALENDAR] Error updating event:', error);
    throw error;
  }
};

/**
 * Delete an event
 */
/**
 * Delete a specific instance or future instances of a recurring CalDAV event.
 * 'this' mode: adds EXDATE to exclude the specific occurrence.
 * 'future' mode: modifies RRULE to add UNTIL just before the target occurrence.
 */
export const deleteRecurringInstance = async (
  serverUrl: string,
  username: string,
  password: string,
  calendarUrl: string,
  eventId: string,
  deleteMode: string
): Promise<void> => {
  try {
    const client = createClient(serverUrl, username, password);
    await loginClient(client, serverUrl, username);

    // Parse the caldav ID to get objectUrl and occurrence date
    const parts = eventId.split('::');
    if (parts.length < 4 || parts[0] !== 'caldav') {
      throw new Error('Cannot determine occurrence from event ID format');
    }
    const objectUrl = parts[1];
    const occurrenceISO = parts[3]; // e.g. "2026-03-30T12:40:00.000Z"

    // Fetch the calendar object
    const objects = await client.fetchCalendarObjects({
      calendar: { url: calendarUrl } as any,
      objectUrls: [objectUrl],
    });
    const targetObject = objects?.[0];
    if (!targetObject?.data) {
      throw new Error('Event not found on server');
    }

    // Parse iCal data
    const jcal = ICAL.parse(targetObject.data);
    const comp = new ICAL.Component(jcal);
    const vevent = comp.getFirstSubcomponent('vevent');
    if (!vevent) {
      throw new Error('No VEVENT found in calendar object');
    }

    const occDate = new Date(occurrenceISO);

    if (deleteMode === 'this') {
      // Add EXDATE to exclude this specific occurrence
      const dtstart = vevent.getFirstProperty('dtstart');
      const isAllDay = dtstart?.getParameter('value') === 'DATE';

      if (isAllDay) {
        const exDate = ICAL.Time.fromJSDate(occDate, true);
        exDate.isDate = true;
        const prop = new ICAL.Property('exdate');
        prop.setParameter('value', 'DATE');
        prop.setValue(exDate);
        vevent.addProperty(prop);
      } else {
        // Use the same timezone as DTSTART if available
        const tzid = dtstart?.getParameter('tzid');
        const exDate = ICAL.Time.fromJSDate(occDate, false);
        const prop = new ICAL.Property('exdate');
        if (tzid) {
          prop.setParameter('tzid', tzid);
        }
        prop.setValue(exDate);
        vevent.addProperty(prop);
      }

      console.log('[SYNOLOGY CALENDAR] Added EXDATE for occurrence:', occurrenceISO);

    } else if (deleteMode === 'past') {
      // Move DTSTART forward to the next occurrence after this one
      const event = new ICAL.Event(vevent);
      const rruleProp = vevent.getFirstProperty('rrule');
      if (!rruleProp) {
        throw new Error('No RRULE found on event');
      }

      // Find the next occurrence after occDate
      const iter = event.iterator();
      let next = iter.next();
      let nextAfter: any = null;
      while (next) {
        const nextDate = next.toJSDate();
        if (nextDate.getTime() > occDate.getTime()) {
          nextAfter = next;
          break;
        }
        next = iter.next();
      }

      if (!nextAfter) {
        await client.deleteObject({ url: objectUrl, etag: undefined } as any);
        console.log('[SYNOLOGY CALENDAR] No future occurrences, deleted entire event');
        return;
      }

      // Update DTSTART and DTEND to the next occurrence
      const dtstart = vevent.getFirstProperty('dtstart');
      const oldStart = event.startDate;
      const oldEnd = event.endDate;
      const durationMs = oldEnd.toJSDate().getTime() - oldStart.toJSDate().getTime();
      const newEndDate = new Date(nextAfter.toJSDate().getTime() + durationMs);

      vevent.updatePropertyWithValue('dtstart', nextAfter);
      vevent.updatePropertyWithValue('dtend', ICAL.Time.fromJSDate(newEndDate, dtstart?.getParameter('tzid') ? false : true));

      // Adjust COUNT if present
      const rrule = rruleProp.getFirstValue() as any;
      if (rrule.count) {
        const countIter = event.iterator();
        let removed = 0;
        let cn = countIter.next();
        while (cn) {
          if (cn.toJSDate().getTime() <= occDate.getTime()) removed++;
          else break;
          cn = countIter.next();
        }
        rrule.count = Math.max(1, rrule.count - removed);
        rruleProp.setValue(rrule);
      }

      console.log('[SYNOLOGY CALENDAR] Moved DTSTART forward for past delete, new start:', nextAfter.toString());

    } else if (deleteMode === 'future') {
      // Modify RRULE to end just before this occurrence
      const rruleProp = vevent.getFirstProperty('rrule');
      if (!rruleProp) {
        throw new Error('No RRULE found on event');
      }
      const rrule = rruleProp.getFirstValue() as any;

      // Set UNTIL to one second before the occurrence
      const untilDate = new Date(occDate.getTime() - 1000);
      const until = ICAL.Time.fromJSDate(untilDate, true);

      // Remove COUNT if present, set UNTIL
      delete rrule.count;
      rrule.until = until;

      // Write modified RRULE back
      rruleProp.setValue(rrule);

      console.log('[SYNOLOGY CALENDAR] Modified RRULE UNTIL for future delete:', untilDate.toISOString());
    }

    // PUT the modified object back
    const updatedData = comp.toString();
    await client.updateCalendarObject({
      calendarObject: {
        ...targetObject,
        url: objectUrl,
        data: updatedData,
      },
    });

    console.log('[SYNOLOGY CALENDAR] Recurring instance delete complete (mode:', deleteMode, ')');

  } catch (error) {
    console.error('[SYNOLOGY CALENDAR] Error deleting recurring instance:', error);
    throw error;
  }
};

export const deleteEvent = async (
  serverUrl: string,
  username: string,
  password: string,
  calendarUrl: string,
  eventId: string
): Promise<void> => {
  try {
    const client = createClient(serverUrl, username, password);
    await loginClient(client, serverUrl, username);

    const parsed = parseCaldavId(eventId);
    if (parsed) {
      await client.deleteObject({
        url: parsed.objectUrl,
        etag: undefined,
      } as any);
      console.log('[SYNOLOGY CALENDAR] Event deleted via direct URL (O(1))');
      return;
    }

    const calendars = await client.fetchCalendars();
    const targetCalendar = calendars.find(c => c.url === calendarUrl);

    if (!targetCalendar) {
      throw new Error(`Calendar not found: ${calendarUrl}`);
    }

    const uid = eventId.split('-')[0];

    const allObjects = await client.fetchCalendarObjects({
      calendar: targetCalendar,
    });

    const targetObject = allObjects.find(obj => {
      if (obj.data) {
        return obj.data.includes(`UID:${uid}`);
      }
      return obj.url.includes(uid);
    });

    if (!targetObject) {
      console.warn('[SYNOLOGY CALENDAR] Event to delete not found, maybe already deleted?');
      return;
    }

    await client.deleteObject({
      url: targetObject.url,
      etag: targetObject.etag
    } as any);

    console.log('[SYNOLOGY CALENDAR] Event deleted via legacy scan');

  } catch (error) {
    console.error('[SYNOLOGY CALENDAR] Error deleting event:', error);
    throw error;
  }
};
