// Client-side download helper.
//
// The naive approach — `await res.blob()` then createObjectURL — buffers the
// entire decrypted file in memory before anything is written. That is fine for
// small transfers and untenable for multi-gigabyte ones, which is exactly what
// this app exists to move.
//
// Where the File System Access API is available (Chromium), the response body
// is piped straight to a user-chosen file and peak memory stays at roughly one
// chunk. Everywhere else we fall back to the blob path.

interface SaveFilePickerOptions {
  suggestedName?: string;
  types?: { description: string; accept: Record<string, string[]> }[];
}

interface FileSystemWritable {
  write(data: BufferSource | Blob | string): Promise<void>;
  close(): Promise<void>;
  abort?(reason?: unknown): Promise<void>;
}

interface FileSystemFileHandleLike {
  createWritable(): Promise<FileSystemWritable>;
}

type PickerWindow = Window & {
  showSaveFilePicker?: (
    options?: SaveFilePickerOptions
  ) => Promise<FileSystemFileHandleLike>;
};

export function supportsStreamingDownload(): boolean {
  return (
    typeof window !== 'undefined' &&
    typeof (window as PickerWindow).showSaveFilePicker === 'function'
  );
}

/** Thrown when the user dismisses the save dialog. */
export class DownloadCancelled extends Error {
  constructor() {
    super('Download cancelled');
    this.name = 'DownloadCancelled';
  }
}

/**
 * Write `response` to disk, streaming when the browser allows it.
 * `onProgress` receives bytes written so far.
 */
export async function saveResponseToDisk(
  response: Response,
  filename: string,
  onProgress?: (bytesWritten: number) => void
): Promise<void> {
  const picker = (window as PickerWindow).showSaveFilePicker;

  if (picker && response.body) {
    let handle: FileSystemFileHandleLike;
    try {
      handle = await picker({ suggestedName: filename });
    } catch (err) {
      // AbortError means the user closed the dialog; anything else is a real
      // failure worth falling back for.
      if ((err as DOMException)?.name === 'AbortError') {
        throw new DownloadCancelled();
      }
      return saveViaBlob(response, filename);
    }

    const writable = await handle.createWritable();
    const reader = response.body.getReader();
    let written = 0;

    try {
      for (;;) {
        const { done, value } = await reader.read();
        if (done) break;
        await writable.write(value);
        written += value.byteLength;
        onProgress?.(written);
      }
      await writable.close();
    } catch (err) {
      await writable.abort?.(err).catch(() => {});
      throw err;
    }
    return;
  }

  return saveViaBlob(response, filename);
}

/**
 * Fallback for browsers without the File System Access API. Buffers the whole
 * body in memory, so very large transfers may fail here.
 */
async function saveViaBlob(response: Response, filename: string): Promise<void> {
  const blob = await response.blob();
  const url = URL.createObjectURL(blob);
  try {
    const anchor = document.createElement('a');
    anchor.href = url;
    anchor.download = filename;
    document.body.appendChild(anchor);
    anchor.click();
    anchor.remove();
  } finally {
    // Give the browser a tick to start the download before revoking.
    setTimeout(() => URL.revokeObjectURL(url), 60_000);
  }
}
