// Pure RFC 5545 iCalendar serialization. Deterministic (no randomness, no
// wall-clock): UID and DTSTAMP are derived from the event itself, so the same
// input always yields byte-identical output. CRLF line endings, 75-octet
// folding.

interface IcsEvent {
  title: string;
  dateMs: number;
  allDay: boolean;
}

/** Serialize events to a VCALENDAR string. Valid (and non-empty) even for []. */
export function toIcs(events: IcsEvent[]): string {
  const lines: string[] = [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    'PRODID:-//Wisp//Enrich//EN',
    'CALSCALE:GREGORIAN',
  ];

  for (const ev of events) {
    const stamp = formatUtc(ev.dateMs);
    lines.push('BEGIN:VEVENT');
    lines.push(`UID:${uidFor(ev)}`);
    lines.push(`DTSTAMP:${stamp}`);
    if (ev.allDay) {
      lines.push(`DTSTART;VALUE=DATE:${formatDate(ev.dateMs)}`);
    } else {
      lines.push(`DTSTART:${stamp}`);
    }
    lines.push(`SUMMARY:${escapeText(ev.title)}`);
    lines.push('END:VEVENT');
  }

  lines.push('END:VCALENDAR');

  return lines.map(foldLine).join('\r\n') + '\r\n';
}

// ---------------------------------------------------------------------------
// Deterministic UID derivation (FNV-1a hash of title + dateMs)
// ---------------------------------------------------------------------------

function uidFor(ev: IcsEvent): string {
  const h = fnv1a(`${ev.title}|${ev.dateMs}`);
  return `${h}@wisp`;
}

function fnv1a(s: string): string {
  let hash = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) {
    hash ^= s.charCodeAt(i);
    // 32-bit FNV prime multiply via shifts, kept unsigned.
    hash = Math.imul(hash, 0x01000193) >>> 0;
  }
  return hash.toString(16).padStart(8, '0');
}

// ---------------------------------------------------------------------------
// Date formatting from dateMs only (UTC)
// ---------------------------------------------------------------------------

function formatDate(ms: number): string {
  const d = new Date(ms);
  return (
    pad(d.getUTCFullYear(), 4) + pad(d.getUTCMonth() + 1) + pad(d.getUTCDate())
  );
}

function formatUtc(ms: number): string {
  const d = new Date(ms);
  return (
    pad(d.getUTCFullYear(), 4) +
    pad(d.getUTCMonth() + 1) +
    pad(d.getUTCDate()) +
    'T' +
    pad(d.getUTCHours()) +
    pad(d.getUTCMinutes()) +
    pad(d.getUTCSeconds()) +
    'Z'
  );
}

function pad(n: number, width = 2): string {
  return String(n).padStart(width, '0');
}

// ---------------------------------------------------------------------------
// RFC 5545 text escaping + line folding
// ---------------------------------------------------------------------------

/** Escape backslash, semicolon, comma, and newlines per RFC 5545 §3.3.11. */
function escapeText(s: string): string {
  return s
    .replace(/\\/g, '\\\\')
    .replace(/;/g, '\\;')
    .replace(/,/g, '\\,')
    .replace(/\r\n|\n|\r/g, '\\n');
}

/**
 * Fold a content line to <=75 octets, continuation lines starting with a
 * single space. UTF-8 aware: never splits a multi-byte character across the
 * fold boundary, so every emitted line is valid UTF-8.
 */
function foldLine(line: string): string {
  if (utf8Len(line) <= 75) return line;

  const chunks: string[] = [];
  // First line: 75 octets. Continuation lines: 74 octets (1 leading space).
  let limit = 75;
  let current = '';
  let currentBytes = 0;

  for (const ch of line) {
    const w = utf8Len(ch);
    if (currentBytes + w > limit) {
      chunks.push(current);
      current = '';
      currentBytes = 0;
      limit = 74;
    }
    current += ch;
    currentBytes += w;
  }
  if (current) chunks.push(current);

  return chunks.map((c, i) => (i === 0 ? c : ' ' + c)).join('\r\n');
}

/** Number of UTF-8 octets a string encodes to. */
function utf8Len(s: string): number {
  let n = 0;
  for (const ch of s) {
    const cp = ch.codePointAt(0)!;
    if (cp < 0x80) n += 1;
    else if (cp < 0x800) n += 2;
    else if (cp < 0x10000) n += 3;
    else n += 4;
  }
  return n;
}
