import { clientStore, config, datastore, editor, index } from "@silverbulletmd/silverbullet/syscalls";
import ICAL from "ical.js";
import { RRule, RRuleSet } from "rrule";
import { getUtcOffsetMs, resolveIanaName } from "./timezones.ts";

const VERSION = "0.4.8";
const CACHE_KEY = "icalendar:lastSync";

console.log(`[iCalendar] Plug script executing at top level (Version ${VERSION})`);

/**
 * Mapping of verbose RRULE object keys to standard iCalendar shortened keys.
 */
const RRULE_KEY_MAP: Record<string, string> = {
  "frequency": "FREQ",
  "until": "UNTIL",
  "count": "COUNT",
  "interval": "INTERVAL",
  "bysecond": "BYSECOND",
  "byminute": "BYMINUTE",
  "byhour": "BYHOUR",
  "byday": "BYDAY",
  "bymonthday": "BYMONTHDAY",
  "byyearday": "BYYEARDAY",
  "byweekno": "BYWEEKNO",
  "bymonth": "BYMONTH",
  "bysetpos": "BYSETPOS",
  "wkst": "WKST",
  "workweekstart": "WKST",
  "freq": "FREQ",
};

/**
 * Robustly formats an RRULE value for its string representation.
 */
function formatRRuleValue(v: any): string {
  if (Array.isArray(v)) {
    return v.map((item) => formatRRuleValue(item)).join(",");
  }
  if (v instanceof Date) {
    return v.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
  }
  if (typeof v === "object" && v !== null) {
    const val = v.date || v.day || v.value;
    if (val !== undefined) {
      return formatRRuleValue(val);
    }
  }
  return String(v);
}

// ============================================================================
// Utility Functions
// ============================================================================

/**
 * Creates a SHA-256 hash of a string (hex encoded)
 */
async function sha256Hash(str: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(str);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
}

/**
 * Converts UTC Date to a specific timezone string
 * Uses toLocaleString for better compatibility
 */
export function dateToTimezoneString(date: Date, timezone: string = "America/Los_Angeles"): string {
  try {
    // Use toLocaleString which has better worker support
    const localeString = date.toLocaleString('en-US', {
      timeZone: timezone,
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false
    });
    
    console.log(`[iCalendar] Converting ${date.toISOString()} to ${timezone}: ${localeString}`);
    
    // Parse the result: "MM/DD/YYYY, HH:MM:SS"
    const match = localeString.match(/(\d{2})\/(\d{2})\/(\d{4}),\s*(\d{2}):(\d{2}):(\d{2})/);
    if (match) {
      const [_, month, day, year, hour, minute, second] = match;
      return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
    }
    
    throw new Error("Failed to parse toLocaleString result");
  } catch (err) {
    console.error(`[iCalendar] Error converting to timezone ${timezone}:`, err);
    // Fallback to UTC
    const pad = (n: number) => String(n).padStart(2, "0");
    return date.getUTCFullYear() + "-" + 
           pad(date.getUTCMonth() + 1) + "-" + 
           pad(date.getUTCDate()) + "T" + 
           pad(date.getUTCHours()) + ":" + 
           pad(date.getUTCMinutes()) + ":" + 
           pad(date.getUTCSeconds());
  }
}

/**
 * Recursively converts all Date objects and ISO date strings to strings
 */
function convertDatesToStrings<T>(obj: T): any {
  if (obj === null || obj === undefined) {
    return obj;
  }

  if (obj instanceof Date) {
    return obj.toISOString();
  }
  
  if (typeof obj === 'object' && 'date' in obj && (obj as any).date instanceof Date) {
    return (obj as any).date.toISOString();
  }

  if (typeof obj === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(obj)) {
    try {
      return new Date(obj).toISOString();
    } catch {
      return obj;
    }
  }

  if (Array.isArray(obj)) {
    return obj.map(item => convertDatesToStrings(item));
  }

  if (typeof obj === 'object') {
    const result: any = {};
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        result[key] = convertDatesToStrings((obj as any)[key]);
      }
    }
    return result;
  }

  return obj;
}

// ============================================================================
// Configuration Functions
// ============================================================================

async function getSources(): Promise<{ sources: any[], syncWindowDays: number, displayTimezone: string }> {
  try {
    const rawConfig = await config.get("icalendar", { sources: [] }) as any;
    console.log("[iCalendar] Raw config retrieved:", JSON.stringify(rawConfig));
    
    let sources = rawConfig.sources || [];
    const syncWindowDays = rawConfig.syncWindowDays || 365;
    // Get user's display timezone, default to America/Los_Angeles (PST)
    const displayTimezone = rawConfig.displayTimezone || "America/Los_Angeles";

    if (sources && typeof sources === "object" && !Array.isArray(sources)) {
      const sourceArray = [];
      for (const key in sources) {
        if (sources[key] && typeof sources[key].url === "string") {
          sourceArray.push(sources[key]);
        }
      }
      sources = sourceArray;
    }

    return { sources, syncWindowDays, displayTimezone };
  } catch (e) {
    console.error("[iCalendar] Error in getSources:", e);
    return { sources: [], syncWindowDays: 365, displayTimezone: "America/Los_Angeles" };
  }
}

// ============================================================================
// Calendar Fetching & Parsing
// ============================================================================

/**
 * Resolves the event start as a UTC Date object using DST-aware resolution.
 */
export async function resolveEventStart(icsEvent: any): Promise<Date | null> {
  const obj = icsEvent.start;
  if (!obj) return null;

  // 1. Extract the wall-clock local datetime string
  let wallClock: string | null = null;
  if (obj.local?.date) {
    const d = obj.local.date;
    wallClock = d instanceof Date ? d.toISOString() : String(d);
  } else if (obj.date) {
    const d = obj.date;
    wallClock = d instanceof Date ? d.toISOString() : String(d);
  }

  if (!wallClock) return null;

  // Strip any trailing Z — this is treated as wall-clock local time
  wallClock = wallClock.replace(/Z$/, "");

  // 2. Resolve IANA timezone
  const rawTz = obj.local?.timezone || (obj as any).timezone || "UTC";
  const ianaName = resolveIanaName(rawTz);

  if (!ianaName) {
    console.warn(`[iCalendar] Unknown timezone: "${rawTz}" - falling back to UTC for event "${icsEvent.summary}"`);
    const utcDate = new Date(wallClock + (wallClock.includes("T") ? "" : "T00:00:00") + "Z");
    if (isNaN(utcDate.getTime())) return null;
    return utcDate;
  }

  // 3. Parse the wall-clock time as a UTC instant (no offset yet)
  const wallClockAsUtc = new Date(wallClock + (wallClock.includes("T") ? "" : "T00:00:00") + "Z");
  if (isNaN(wallClockAsUtc.getTime())) return null;

  // 4. Get the DST-aware offset for this IANA zone at this instant
  const offsetMs = getUtcOffsetMs(ianaName, wallClockAsUtc);
  
  // 5. Convert: UTC = wall-clock - offset
  return new Date(wallClockAsUtc.getTime() - offsetMs);
}

/**
 * Resolves event end time
 */
async function resolveEventEnd(icsEvent: any): Promise<Date | null> {
  if (!icsEvent.end) return null;
  
  // Create a temporary event object with end as start
  const tempEvent = {
    ...icsEvent,
    start: icsEvent.end
  };
  
  return await resolveEventStart(tempEvent);
}

/**
 * Expands recurring events into individual occurrences.
 */
export function expandRecurrences(icsEvent: any, windowDays = 365, displayTimezone = "America/Los_Angeles", now = new Date()): any[] {
  const rruleStr = icsEvent.rrule || (icsEvent as any).recurrenceRule;
  if (!rruleStr) return [icsEvent];

  try {
    const set = new RRuleSet();
    
    let cleanRule = "";
    if (typeof rruleStr === "string") {
      cleanRule = rruleStr.replace(/^RRULE:/i, "");
    } else if (typeof rruleStr === "object" && rruleStr !== null) {
      cleanRule = Object.entries(rruleStr)
        .map(([k, v]) => {
          const standardKey = RRULE_KEY_MAP[k.toLowerCase()] || k.toUpperCase();
          return `${standardKey}=${formatRRuleValue(v)}`;
        })
        .join(";");
    } else {
      console.warn(`[iCalendar] Invalid rrule type (${typeof rruleStr}) for event "${icsEvent.summary || "Untitled"}". Treating as non-recurring.`);
      return [icsEvent];
    }
    
    // Parse the stored UTC time (don't add Z, it's already there)
    const dtstart = new Date(icsEvent.start);
    if (isNaN(dtstart.getTime())) {
      console.error(`[iCalendar] Invalid start date for recurrence: ${icsEvent.start}`);
      return [icsEvent];
    }

    const ruleOptions = RRule.parseString(cleanRule);
    ruleOptions.dtstart = dtstart;
    
    set.rrule(new RRule(ruleOptions));

    // Handle EXDATE
    for (const exdate of (icsEvent.exdate || [])) {
      set.exdate(new Date(exdate));
    }

    const windowEnd = new Date(now.getTime() + windowDays * 86400000);
    
    // Expand from the event's actual start date up to the window end
    const occurrences = set.between(dtstart, windowEnd, true);
    
    // Calculate duration for recurring events
    const duration = icsEvent.end ? 
      new Date(icsEvent.end).getTime() - dtstart.getTime() : 
      0;
    
    const mapped = occurrences.map(occurrenceDate => {
      const endDate = duration > 0 ? new Date(occurrenceDate.getTime() + duration) : null;
      
      return {
        ...icsEvent,
        start: occurrenceDate.toISOString(),
        startLocal: dateToTimezoneString(occurrenceDate, displayTimezone),
        end: endDate ? endDate.toISOString() : undefined,
        endLocal: endDate ? dateToTimezoneString(endDate, displayTimezone) : undefined,
        recurrent: true,
        rrule: undefined,
      };
    });
    
    return mapped;
  } catch (err) {
    console.error(`[iCalendar] Error expanding recurrence for ${icsEvent.summary}:`, err);
    return [icsEvent];
  }
}

async function fetchAndParseCalendar(source: any, windowDays = 365, displayTimezone = "America/Los_Angeles"): Promise<any[]> {
  try {
    const response = await fetch(source.url);
    if (!response.ok) {
      console.error(`[iCalendar] Fetch failed for ${source.name}: ${response.status} ${response.statusText}`);
      return [];
    }
    const text = await response.text();
    const jcalData = ICAL.parse(text);
    const vcalendar = new ICAL.Component(jcalData);
    const vevents = vcalendar.getAllSubcomponents("vevent");

    // First pass: map of UID -> Set of ISO strings for RECURRENCE-ID exceptions
    const overrides = new Map<string, Set<string>>();
    for (const vevent of vevents) {
      const recId = vevent.getFirstPropertyValue("recurrence-id") as any | null;
      const uid = vevent.getFirstPropertyValue("uid") as string | null;
      if (recId && uid) {
        if (!overrides.has(uid)) overrides.set(uid, new Set());
        overrides.get(uid)!.add(recId.toJSDate().toISOString());
      }
    }

    const events: any[] = [];
    for (const vevent of vevents) {
      const icsEvent = new ICAL.Event(vevent);
      const status = vevent.getFirstPropertyValue("status") as string | null;
      const summary = icsEvent.summary || "";
      
      // 1. Skip explicitly cancelled events
      if (status?.toUpperCase() === "CANCELLED") continue;
      if (summary.toLowerCase().startsWith("canceled:") || summary.toLowerCase().startsWith("cancelled:")) continue;

      // 2. Skip declined events (look for PARTSTAT=DECLINED in attendees)
      const attendees = vevent.getAllProperties("attendee");
      let declined = false;
      for (const attendee of attendees) {
        const partstat = attendee.getParameter("partstat");
        if (partstat?.toUpperCase() === "DECLINED") {
          // Note: In a multi-user environment, we'd need to check if this is *the* user's status.
          // For a personal plug, we assume any DECLINED attendee means the user declined or the event is out.
          declined = true;
          break;
        }
      }
      if (declined) continue;

      // Extract raw properties for recurrence expansion
      const uid = icsEvent.uid;
      const description = icsEvent.description;
      const location = icsEvent.location;
      const rrule = vevent.getFirstPropertyValue("rrule");
      const exdates = vevent.getAllProperties("exdate").map((p: any) => p.getFirstValue().toJSDate().toISOString());

      // Add recurrence-id overrides to exdates so the Master doesn't expand them
      if (uid && overrides.has(uid) && !vevent.getFirstPropertyValue("recurrence-id")) {
        for (const overDate of overrides.get(uid)!) {
          exdates.push(overDate);
        }
      }

      // Resolve start/end times
      const startDateUTC = icsEvent.startDate.toJSDate();
      const endDateUTC = icsEvent.endDate ? icsEvent.endDate.toJSDate() : null;
      
      const rawTz = icsEvent.startDate.timezone || "UTC";
      
      const baseEvent = {
        uid,
        summary,
        name: summary || "Untitled Event",
        description,
        location,
        // Store both UTC (for sorting/comparison) and local (for display)
        start: startDateUTC.toISOString(),
        startLocal: dateToTimezoneString(startDateUTC, displayTimezone),
        end: endDateUTC ? endDateUTC.toISOString() : undefined,
        endLocal: endDateUTC ? dateToTimezoneString(endDateUTC, displayTimezone) : undefined,
        tag: "ical-event",
        sourceName: source.name,
        timezone: rawTz,
        rrule: rrule ? rrule.toString() : undefined,
        exdate: exdates.length > 0 ? exdates : undefined
      };

      if (rawTz !== "UTC" && rawTz !== "None" && !resolveIanaName(rawTz)) {
        baseEvent.description = `(Warning: Unknown timezone "${rawTz}") ${baseEvent.description || ""}`;
      }

      const expanded = expandRecurrences(baseEvent, windowDays, displayTimezone);
      for (const occurrence of expanded) {
        // Use summary in key to avoid collisions
        const uniqueKey = `${occurrence.start}${occurrence.uid || ''}${occurrence.summary || ''}`;
        occurrence.ref = await sha256Hash(uniqueKey);
        
        // Save our correctly formatted time strings
        const savedTimes = {
          start: occurrence.start,
          startLocal: occurrence.startLocal,
          end: occurrence.end,
          endLocal: occurrence.endLocal
        };
        
        // Convert any remaining Date objects in other fields
        const converted = convertDatesToStrings(occurrence);
        
        // Restore our time strings (don't let them get reconverted)
        converted.start = savedTimes.start;
        converted.startLocal = savedTimes.startLocal;
        converted.end = savedTimes.end;
        converted.endLocal = savedTimes.endLocal;
        
        events.push(converted);
      }
    }
    return events;
  } catch (err: any) {
    console.error(`[iCalendar] Error fetching/parsing ${source.name}:`, err.message || err, err.stack || "");
    return [];
  }
}

export async function syncCalendars() {
  try {
    const { sources, syncWindowDays, displayTimezone } = await getSources();
    if (sources.length === 0) return;
    
    console.log(`[iCalendar] Using display timezone: ${displayTimezone}`);
    
    // Test timezone conversion
    const testDate = new Date("2026-02-21T14:00:00.000Z"); // 14:00 UTC
    const converted = dateToTimezoneString(testDate, displayTimezone);
    console.log(`[iCalendar] Timezone test: ${testDate.toISOString()} → ${converted} (should be 06:00 PST)`);
    
    await editor.flashNotification("Syncing calendars...", "info");
    const allEvents: any[] = [];
    for (const source of sources) {
      const events = await fetchAndParseCalendar(source, syncWindowDays, displayTimezone);
      allEvents.push(...events);
    }
    await index.indexObjects("$icalendar", allEvents);
    await editor.flashNotification(`Synced ${allEvents.length} events`, "info");
  } catch (err) {
    console.error("[iCalendar] syncCalendars failed:", err);
  }
}

export async function forceSync() {
  await clientStore.del(CACHE_KEY);
  await syncCalendars();
}

export async function clearCache() {
  if (!await editor.confirm("Clear all calendar events?")) return;
  const pageKeys = await datastore.query({ prefix: ["ridx", "$icalendar"] });
  const allKeys: any[] = [];
  for (const { key } of pageKeys) {
    allKeys.push(key);
    allKeys.push(["idx", ...key.slice(2), "$icalendar"]);
  }
  if (allKeys.length > 0) await datastore.batchDel(allKeys);
  await clientStore.del(CACHE_KEY);
  await editor.flashNotification("Calendar index cleared", "info");
}

export async function showVersion() {
  await editor.flashNotification(`iCalendar Plug ${VERSION}`, "info");
}
