/**
 * Google Calendar / Tasks / People (Contacts) two-way sync helpers.
 *
 * Used by MailxService to push local edits to Google and pull server
 * changes into the local cache. All functions take a `getToken` function
 * and a fetch implementation so they stay platform-agnostic (Node uses
 * global `fetch` on Node 18+; browsers use window.fetch).
 *
 * Error handling: throws on network / HTTP errors. Caller catches and
 * either retries via the store_sync drainer or surfaces to the UI.
 */

type TokenProvider = () => Promise<string>;

export class GoogleHttpError extends Error {
    status: number;
    statusText: string;
    body: string;
    constructor(status: number, statusText: string, body: string) {
        super(`Google API ${status} ${statusText}: ${body.slice(0, 300)}`);
        this.status = status;
        this.statusText = statusText;
        this.body = body;
    }
}

async function googleFetch(
    tokenProvider: TokenProvider, url: string, init: RequestInit = {},
): Promise<Response> {
    const token = await tokenProvider();
    const headers = new Headers(init.headers || {});
    headers.set("Authorization", `Bearer ${token}`);
    if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
    const res = await fetch(url, { ...init, headers });
    if (!res.ok) {
        const body = await res.text().catch(() => "");
        throw new GoogleHttpError(res.status, res.statusText, body);
    }
    return res;
}

// ── Calendar ──

export interface GCalEvent {
    id: string;
    summary: string;
    start: { dateTime?: string; date?: string };
    end: { dateTime?: string; date?: string };
    location?: string;
    description?: string;
    etag?: string;
    /** Set on instances expanded from a recurring series (singleEvents=true).
     *  Absent for singletons. Lets the UI filter out recurring expansions. */
    recurringEventId?: string;
    /** Link to open the event in Google Calendar web. */
    htmlLink?: string;
    /** Per-event reminder configuration. `useDefault: true` means apply
     *  the calendar's `defaultReminders` (carried separately on the
     *  calendarList entry); when false, `overrides` lists each popup /
     *  email reminder offset for this specific event. */
    reminders?: {
        useDefault?: boolean;
        overrides?: Array<{ method: string; minutes: number }>;
    };
}

export async function listCalendarEvents(
    tokenProvider: TokenProvider, fromMs: number, toMs: number, calendarId = "primary",
): Promise<GCalEvent[]> {
    const from = new Date(fromMs).toISOString();
    const to = new Date(toMs).toISOString();
    const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events`
        + `?timeMin=${encodeURIComponent(from)}&timeMax=${encodeURIComponent(to)}`
        + `&singleEvents=true&orderBy=startTime&maxResults=250`;
    const all: GCalEvent[] = [];
    let pageToken: string | undefined;
    do {
        const pagedUrl = pageToken ? `${url}&pageToken=${encodeURIComponent(pageToken)}` : url;
        const res = await googleFetch(tokenProvider, pagedUrl);
        const data = await res.json() as any;
        for (const ev of (data.items || [])) all.push(ev);
        pageToken = data.nextPageToken;
    } while (pageToken);
    return all;
}

/** Enumerate the user's Google Calendars (everything in their
 *  `calendarList` — owned, shared, subscribed). Used so refreshCalendarEvents
 *  doesn't hard-code `primary` and miss events on secondary calendars
 *  (Bob 2026-05-12: recurring Statin event showed in Thunderbird but not
 *  rmfmail because it's on a secondary calendar, not primary).
 *
 *  Returns each entry's id (use as `calendarId` in subsequent fetches),
 *  summary (display name), and `selected` flag (matches the "show in
 *  Google web UI" toggle — a sensible default filter for "which calendars
 *  should mailx show"). Hidden / explicitly-unselected calendars are
 *  filtered out by the caller; we still return them so a future UI can
 *  expose the full list. */
export interface GCalendarListEntry {
    id: string;
    summary: string;
    selected: boolean;
    primary: boolean;
    /** Hex color Google assigns the calendar (`backgroundColor`). Used to
     *  tint the per-calendar icon/monogram badge in the sidebar. */
    backgroundColor: string;
    /** Calendar-wide default reminder offsets, applied to events whose
     *  `reminders.useDefault === true`. Each entry is `{ method, minutes }`;
     *  client carries only the minutes for popup reminders. */
    defaultReminderMinutes: number[];
}
export async function listCalendars(tokenProvider: TokenProvider): Promise<GCalendarListEntry[]> {
    const url = "https://www.googleapis.com/calendar/v3/users/me/calendarList?minAccessRole=reader&maxResults=250";
    const out: GCalendarListEntry[] = [];
    let pageToken: string | undefined;
    do {
        const pagedUrl = pageToken ? `${url}&pageToken=${encodeURIComponent(pageToken)}` : url;
        const res = await googleFetch(tokenProvider, pagedUrl);
        const data = await res.json() as any;
        for (const item of (data.items || [])) {
            const dr: number[] = Array.isArray(item.defaultReminders)
                ? item.defaultReminders
                    .filter((r: any) => (r.method === "popup" || !r.method) && Number.isFinite(r.minutes))
                    .map((r: any) => r.minutes)
                : [];
            out.push({
                id: item.id,
                summary: item.summary || item.id,
                selected: !!item.selected,
                primary: !!item.primary,
                backgroundColor: item.backgroundColor || "",
                defaultReminderMinutes: dr,
            });
        }
        pageToken = data.nextPageToken;
    } while (pageToken);
    return out;
}

export async function createCalendarEvent(
    tokenProvider: TokenProvider, event: any, calendarId = "primary",
): Promise<GCalEvent> {
    const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events`;
    const res = await googleFetch(tokenProvider, url, { method: "POST", body: JSON.stringify(event) });
    return res.json();
}

export async function updateCalendarEvent(
    tokenProvider: TokenProvider, eventId: string, event: any, calendarId = "primary",
): Promise<GCalEvent> {
    const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`;
    const res = await googleFetch(tokenProvider, url, { method: "PATCH", body: JSON.stringify(event) });
    return res.json();
}

export async function deleteCalendarEvent(
    tokenProvider: TokenProvider, eventId: string, calendarId = "primary",
): Promise<void> {
    const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`;
    await googleFetch(tokenProvider, url, { method: "DELETE" });
}

// ── Tasks ──

export interface GTask {
    id: string;
    title: string;
    notes?: string;
    due?: string;
    completed?: string;
    status?: "needsAction" | "completed";
    etag?: string;
}

/** Enumerate all of the user's Google task lists. Mirrors `listCalendars`
 *  — Google "Reminders" are stored as TASKS as of late 2023, and a user
 *  can have many task lists, each potentially holding reminders. Without
 *  this enumeration mailx only ever sees the `@default` list and misses
 *  every reminder filed elsewhere. Bob 2026-05-12. */
export interface GTaskListEntry {
    id: string;
    title: string;
}
export async function listTaskLists(tokenProvider: TokenProvider): Promise<GTaskListEntry[]> {
    const url = "https://tasks.googleapis.com/tasks/v1/users/@me/lists?maxResults=100";
    const res = await googleFetch(tokenProvider, url);
    const data = await res.json() as any;
    return (data.items || []).map((it: any) => ({ id: it.id, title: it.title || it.id }));
}

export async function listTasks(
    tokenProvider: TokenProvider, listId = "@default", showCompleted = false,
): Promise<GTask[]> {
    const url = `https://tasks.googleapis.com/tasks/v1/lists/${encodeURIComponent(listId)}/tasks`
        + `?showCompleted=${showCompleted}&maxResults=100`;
    const res = await googleFetch(tokenProvider, url);
    const data = await res.json() as any;
    return data.items || [];
}

export async function createTask(
    tokenProvider: TokenProvider, task: any, listId = "@default",
): Promise<GTask> {
    const url = `https://tasks.googleapis.com/tasks/v1/lists/${encodeURIComponent(listId)}/tasks`;
    const res = await googleFetch(tokenProvider, url, { method: "POST", body: JSON.stringify(task) });
    return res.json();
}

export async function updateTask(
    tokenProvider: TokenProvider, taskId: string, task: any, listId = "@default",
): Promise<GTask> {
    const url = `https://tasks.googleapis.com/tasks/v1/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}`;
    const res = await googleFetch(tokenProvider, url, { method: "PATCH", body: JSON.stringify(task) });
    return res.json();
}

export async function deleteTask(
    tokenProvider: TokenProvider, taskId: string, listId = "@default",
): Promise<void> {
    const url = `https://tasks.googleapis.com/tasks/v1/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}`;
    await googleFetch(tokenProvider, url, { method: "DELETE" });
}

// ── Contacts (People API) ──

export async function createContact(
    tokenProvider: TokenProvider, person: any,
): Promise<any> {
    const url = `https://people.googleapis.com/v1/people:createContact`;
    const res = await googleFetch(tokenProvider, url, { method: "POST", body: JSON.stringify(person) });
    return res.json();
}

export async function updateContact(
    tokenProvider: TokenProvider, resourceName: string, updatePersonFields: string, person: any,
): Promise<any> {
    const url = `https://people.googleapis.com/v1/${resourceName}:updateContact`
        + `?updatePersonFields=${encodeURIComponent(updatePersonFields)}`;
    const res = await googleFetch(tokenProvider, url, { method: "PATCH", body: JSON.stringify(person) });
    return res.json();
}

export async function deleteContact(
    tokenProvider: TokenProvider, resourceName: string,
): Promise<void> {
    const url = `https://people.googleapis.com/v1/${resourceName}:deleteContact`;
    await googleFetch(tokenProvider, url, { method: "DELETE" });
}

// ── Conversion helpers ──

/** Parse a Google all-day `date` ("YYYY-MM-DD") as LOCAL midnight.
 *  `Date.parse` treats a date-only string as UTC, so an all-day event
 *  renders a day early for any user west of UTC — a June 14 birthday
 *  showed as June 13 in US Eastern (Bob 2026-05-16). Build the Date from
 *  explicit Y/M/D components so midnight is local. */
function dateOnlyToLocalMs(d: string): number {
    const m = d.match(/^(\d{4})-(\d{2})-(\d{2})/);
    if (!m) return Date.parse(d);
    return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])).getTime();
}

export function calendarEventToLocal(ev: GCalEvent, accountId: string, calendarDefaultReminderMinutes: number[] = []): {
    providerId: string; accountId: string; title: string;
    startMs: number; endMs: number; allDay: boolean;
    location: string; notes: string; etag: string;
    recurringEventId: string | undefined;
    htmlLink: string | undefined;
    reminderMinutes: number[];
} {
    const allDay = !!ev.start.date && !ev.start.dateTime;
    const startMs = allDay
        ? dateOnlyToLocalMs(ev.start.date!)
        : Date.parse(ev.start.dateTime!);
    const endMs = allDay
        ? dateOnlyToLocalMs(ev.end.date!)
        : Date.parse(ev.end.dateTime!);
    // Per-event reminder offsets. Google sends `useDefault: true` to mean
    // "apply the calendar's defaultReminders" (which we passed in from the
    // calendarList entry). When `useDefault: false`, the event's own
    // `overrides` array wins. We only fire `popup` reminders client-side
    // — email reminders are Google's job, sending a duplicate from mailx
    // would be redundant. Bob 2026-05-12: "use the reminder times in the
    // event (there can be multiple)".
    let reminderMinutes: number[] = [];
    if (ev.reminders?.useDefault) {
        reminderMinutes = calendarDefaultReminderMinutes.slice();
    } else if (Array.isArray(ev.reminders?.overrides)) {
        reminderMinutes = ev.reminders!.overrides!
            .filter(r => (r.method === "popup" || !r.method) && Number.isFinite(r.minutes))
            .map(r => r.minutes);
    }
    return {
        providerId: ev.id, accountId,
        title: ev.summary || "(no title)",
        startMs, endMs, allDay,
        location: ev.location || "", notes: ev.description || "",
        etag: ev.etag || "",
        recurringEventId: ev.recurringEventId,
        htmlLink: ev.htmlLink,
        reminderMinutes,
    };
}

export function localToCalendarEvent(local: {
    title: string; startMs: number; endMs: number; allDay?: boolean;
    location?: string; notes?: string;
}): any {
    const toIso = (ms: number) => new Date(ms).toISOString();
    const toDate = (ms: number) => new Date(ms).toISOString().slice(0, 10);
    return {
        summary: local.title,
        start: local.allDay ? { date: toDate(local.startMs) } : { dateTime: toIso(local.startMs) },
        end: local.allDay ? { date: toDate(local.endMs) } : { dateTime: toIso(local.endMs) },
        location: local.location || undefined,
        description: local.notes || undefined,
    };
}

export function taskToLocal(t: GTask, accountId: string): {
    providerId: string; accountId: string; title: string; notes: string;
    dueMs: number | undefined; completedMs: number | undefined; etag: string;
} {
    return {
        providerId: t.id, accountId, title: t.title || "",
        notes: t.notes || "",
        dueMs: t.due ? Date.parse(t.due) : undefined,
        completedMs: t.completed ? Date.parse(t.completed) : undefined,
        etag: t.etag || "",
    };
}

export function localToTask(local: {
    title: string; notes?: string; dueMs?: number; completedMs?: number;
}): any {
    return {
        title: local.title,
        notes: local.notes || undefined,
        due: local.dueMs ? new Date(local.dueMs).toISOString() : undefined,
        completed: local.completedMs ? new Date(local.completedMs).toISOString() : undefined,
        status: local.completedMs ? "completed" : "needsAction",
    };
}
