/**
 * VEVENT builder — constructs a valid VCALENDAR/VEVENT iCalendar string for PUT to Fastmail.
 *
 * D-13 contract (mirrors sync.ts parser in reverse):
 *   - All-day events: use ICAL.Time({ isDate: true }) → produces VALUE=DATE, no TZID, no time (D-13)
 *   - Timed events:   use ICAL.Time.fromJSDate(date, true) (useUTC=true) → DTSTART:...Z suffix, no TZID
 *   - NEVER coerce DATE to DATETIME (Pitfall #3)
 *
 * T-03-03 (Tampering): ical.js ICAL.Component handles line-folding + escaping (commas, semicolons,
 * newlines in summary/location/description). Never hand-roll ICS strings.
 *
 * Sources:
 *   - https://github.com/kewisch/ical.js/wiki/Convert-to-iCalendar-(rfc5545)
 *   - https://github.com/kewisch/ical.js/blob/main/lib/ical/component.js
 *   - https://github.com/kewisch/ical.js/blob/main/lib/ical/time.js
 */

import ICAL from 'ical.js';
import { randomUUID } from 'crypto';

export interface NewEventParams {
  uid?: string; // omit = generate new UUID (appended with @familysync)
  summary: string;
  allDay: boolean;
  // All-day: YYYY-MM-DD string (or Date — only the date portion is used)
  // Timed: JS Date representing a UTC instant
  dtstart: string | Date;
  dtend: string | Date;
  location?: string;
  description?: string;
  rruleString?: string; // e.g. 'FREQ=WEEKLY;BYDAY=MO' — omit for non-recurring (CAL-07)
  dtstamp?: Date; // omit = now()
}

/**
 * Simple RRULE preset strings for whole-series recurring events (D-11 / CAL-07).
 * "weekly on Monday" example: 'FREQ=WEEKLY;BYDAY=MO' (not a key in this map; compose manually).
 *
 * IN-01 (v1 limitation — lossy round-trip): these presets map only to a BARE
 * FREQ=DAILY|WEEKLY|MONTHLY|YEARLY. extractRruleString() preserves the FULL stored RECUR
 * (which may carry BYDAY/INTERVAL/COUNT/UNTIL), and the preserve-on-edit path keeps that
 * rich rule. But if a `recurrence` preset value is ever applied to a previously-rich rule,
 * it COLLAPSES the rule to the bare preset — silently dropping BYDAY/INTERVAL/UNTIL/COUNT.
 * This is acceptable in v1 only because the picker offers just these four bare presets and
 * is disabled on edit (EventForm WR-01). When recurrence EDITING ships, do NOT replace the
 * stored rule with a preset: parse the existing RECUR and modify it in place so qualifiers
 * survive. See CR-01/WR-01 preserve paths for the precedent.
 */
export const RRULE_PRESETS: Record<string, string> = {
  daily: 'FREQ=DAILY',
  weekly: 'FREQ=WEEKLY',
  monthly: 'FREQ=MONTHLY',
  yearly: 'FREQ=YEARLY',
};

/**
 * WR-01: Extract the existing RRULE string from a stored VCALENDAR/VEVENT, so the
 * outbox worker can preserve recurrence on an edit whose payload omits it (the PWA
 * occurrence contract does not expose recurrence, D-03). Returns the RRULE value as a
 * 'FREQ=…' string (e.g. 'FREQ=WEEKLY'), or undefined when the event has no RRULE or
 * the input cannot be parsed.
 */
export function extractRruleString(rawVevent: string): string | undefined {
  let parsed: ReturnType<typeof ICAL.parse>;
  try {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- ical.js parse() returns 'any'; result is only passed to ICAL.Component which accepts it
    parsed = ICAL.parse(rawVevent);
  } catch {
    return undefined;
  }
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- ical.js parse() output is 'any'; ICAL.Component is the correct consumer of this value
  const comp = new ICAL.Component(parsed);
  const vevent = comp.getFirstSubcomponent('vevent');
  if (!vevent) return undefined;
  const rrule = vevent.getFirstPropertyValue('rrule');
  if (!rrule) return undefined;
  // ICAL.Recur#toString() yields the RECUR value, e.g. 'FREQ=WEEKLY'.
  return typeof rrule === 'string' ? rrule : (rrule as ICAL.Recur).toString();
}

/**
 * Builds a VCALENDAR/VEVENT iCalendar string from form parameters.
 *
 * @returns { uid, icsString } — uid is the generated or provided UID;
 *   icsString is the full VCALENDAR string ready for PUT to Fastmail.
 */
export function buildVeventString(params: NewEventParams): { uid: string; icsString: string } {
  const uid = params.uid ?? `${randomUUID()}@familysync`;

  // --- VCALENDAR wrapper ---
  const cal = new ICAL.Component(['vcalendar', [], []]);
  cal.updatePropertyWithValue('version', '2.0');
  cal.updatePropertyWithValue('prodid', '-//FamilySync//FamilySync//EN');

  // --- VEVENT ---
  const vevent = new ICAL.Component('vevent');
  vevent.addPropertyWithValue('uid', uid);
  vevent.addPropertyWithValue('summary', params.summary);

  // DTSTAMP: always present (RFC 5545 §3.8.7.2 — required property)
  const dtstamp = ICAL.Time.fromJSDate(params.dtstamp ?? new Date(), true);
  vevent.addPropertyWithValue('dtstamp', dtstamp);

  if (params.allDay) {
    // DATE value (not DATETIME) — isDate:true produces VALUE=DATE, no time component (D-13 contract)
    const startStr =
      typeof params.dtstart === 'string'
        ? params.dtstart
        : params.dtstart.toISOString().slice(0, 10);
    const endStr =
      typeof params.dtend === 'string' ? params.dtend : params.dtend.toISOString().slice(0, 10);

    const [sy, sm, sd] = startStr.split('-').map(Number) as [number, number, number];
    const [ey, em, ed] = endStr.split('-').map(Number) as [number, number, number];

    // WR-04 (owning boundary): RFC-5545 §3.6.1 — DTEND for an all-day event is the
    // EXCLUSIVE end date. Advance the user-entered inclusive end by one calendar day.
    // Building a Date from UTC components ensures no DST ambiguity during the roll-over.
    const endDate = new Date(Date.UTC(ey, em - 1, ed));
    endDate.setUTCDate(endDate.getUTCDate() + 1);
    const ey2 = endDate.getUTCFullYear();
    const em2 = endDate.getUTCMonth() + 1;
    const ed2 = endDate.getUTCDate();

    // ICAL.Timezone.localTimezone is passed as the zone arg required by TS types.
    // isDate:true suppresses any time/TZID output regardless of zone. (D-13)
    const startTime = new ICAL.Time(
      { year: sy, month: sm, day: sd, isDate: true },
      ICAL.Timezone.localTimezone,
    );
    const endTime = new ICAL.Time(
      { year: ey2, month: em2, day: ed2, isDate: true },
      ICAL.Timezone.localTimezone,
    );
    vevent.addPropertyWithValue('dtstart', startTime);
    vevent.addPropertyWithValue('dtend', endTime);
  } else {
    // DATETIME in UTC (useUTC=true → Z suffix; TZID is NOT added by ical.js) (D-13 contract)
    const startTime = ICAL.Time.fromJSDate(params.dtstart as Date, true);
    const endTime = ICAL.Time.fromJSDate(params.dtend as Date, true);
    vevent.addPropertyWithValue('dtstart', startTime);
    vevent.addPropertyWithValue('dtend', endTime);
  }

  // Optional: RRULE (CAL-07 — whole-series recurring events only in v1, D-11)
  // Must use ICAL.Recur.fromString + ICAL.Property to get correct serialization.
  // addPropertyWithValue('rrule', string) serializes the string character-by-character
  // instead of as a RECUR value type.
  if (params.rruleString) {
    const recur = ICAL.Recur.fromString(params.rruleString);
    const rruleProp = new ICAL.Property('rrule');
    rruleProp.setValue(recur);
    vevent.addProperty(rruleProp);
  }

  // Optional fields — included only when provided (no empty properties in ICS)
  if (params.location) {
    vevent.addPropertyWithValue('location', params.location);
  }
  if (params.description) {
    vevent.addPropertyWithValue('description', params.description);
  }

  cal.addSubcomponent(vevent);

  return { uid, icsString: cal.toString() };
}
