type TimeRange = { start: Date; end: Date };

type CacheEntry = { expiresAt: number; ranges: TimeRange[] };

const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const MAX_RANGE_DAYS = 365; // guard against unbounded events

const globalCache = (globalThis as any).__icalCache || new Map<string, CacheEntry>();
(globalThis as any).__icalCache = globalCache;

function parseDateValue(raw: string): Date | null {
  if (!raw) return null;
  // Format examples: 20250101, 20250101T120000Z, 20250101T120000
  if (/^\d{8}$/.test(raw)) {
    const year = Number(raw.slice(0, 4));
    const month = Number(raw.slice(4, 6)) - 1;
    const day = Number(raw.slice(6, 8));
    return new Date(Date.UTC(year, month, day));
  }
  const parsed = new Date(raw);
  return Number.isNaN(parsed.getTime()) ? null : parsed;
}

function normalizeRange(startRaw: string | null, endRaw: string | null): TimeRange | null {
  const start = startRaw ? parseDateValue(startRaw) : null;
  let end = endRaw ? parseDateValue(endRaw) : null;

  if (!start) return null;
  if (!end) {
    end = new Date(start);
    end.setUTCDate(end.getUTCDate() + 1);
  }

  // Clamp absurdly long ranges
  const maxEnd = new Date(start);
  maxEnd.setUTCDate(maxEnd.getUTCDate() + MAX_RANGE_DAYS);
  if (end > maxEnd) end = maxEnd;

  if (end <= start) return null;
  return { start, end };
}

function parseIcs(text: string): TimeRange[] {
  const lines = text.split(/\r?\n/);
  const ranges: TimeRange[] = [];
  let current: { dtstart?: string; dtend?: string; status?: string } | null = null;

  for (const rawLine of lines) {
    const line = rawLine.trim();
    if (line === 'BEGIN:VEVENT') {
      current = {};
      continue;
    }
    if (line === 'END:VEVENT') {
      if (current && current.status !== 'CANCELLED') {
        const range = normalizeRange(current.dtstart || null, current.dtend || null);
        if (range) ranges.push(range);
      }
      current = null;
      continue;
    }
    if (!current) continue;

    if (line.startsWith('DTSTART')) {
      const [, value] = line.split(':');
      current.dtstart = value;
    } else if (line.startsWith('DTEND')) {
      const [, value] = line.split(':');
      current.dtend = value;
    } else if (line.startsWith('STATUS')) {
      const [, value] = line.split(':');
      current.status = value;
    }
  }

  return ranges;
}

async function fetchCalendarUrl(url: string, forceRefresh = false): Promise<TimeRange[]> {
  const cached = globalCache.get(url) as CacheEntry | undefined;
  const now = Date.now();
  if (!forceRefresh && cached && cached.expiresAt > now) {
    return cached.ranges;
  }

  try {
    const res = await fetch(url, { headers: { 'User-Agent': 'lomavuokraus-ical/1.0' }, cache: 'no-store' });
    if (!res.ok) throw new Error(`Fetch failed (${res.status})`);
    const text = await res.text();
    const ranges = parseIcs(text);
    globalCache.set(url, { expiresAt: now + CACHE_TTL_MS, ranges });
    return ranges;
  } catch (err) {
    console.error('Failed to fetch calendar', url, err);
    globalCache.set(url, { expiresAt: now + CACHE_TTL_MS, ranges: [] });
    return [];
  }
}

export async function getCalendarRanges(urls: string[], opts?: { forceRefresh?: boolean }): Promise<TimeRange[]> {
  const forceRefresh = Boolean(opts?.forceRefresh);
  const unique = Array.from(new Set(urls.filter(Boolean)));
  if (!unique.length) return [];
  const results = await Promise.all(unique.map((u) => fetchCalendarUrl(u, forceRefresh)));
  return results.flat();
}

export function isRangeAvailable(ranges: TimeRange[], start: Date, end: Date): boolean {
  for (const r of ranges) {
    if (r.start < end && r.end > start) {
      return false;
    }
  }
  return true;
}

export function expandBlockedDates(ranges: TimeRange[], from: Date, to: Date): string[] {
  const days: string[] = [];
  const cursor = new Date(from);
  while (cursor <= to) {
    const next = new Date(cursor);
    next.setUTCDate(next.getUTCDate() + 1);
    if (!isRangeAvailable(ranges, cursor, next)) {
      days.push(cursor.toISOString().slice(0, 10));
    }
    cursor.setUTCDate(cursor.getUTCDate() + 1);
  }
  return days;
}
