import { computed, ref, shallowRef, watch } from "vue";

import type * as FabricNamespace from "fabric";
type FabricModule = typeof import("fabric");

export interface SlipmatTemplate {
  id: string;
  name: string;
  diameterInches: number;
  dpi: number;
  bleedInches?: number;
  safeZoneInches?: number;
  backgroundColor: string;
}

const DISPLAY_SIZE = 720;
const PREVIEW_SIZE = 1024;
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 2;
const ZOOM_STEP = 0.1;

const TEMPLATE_PRESETS: SlipmatTemplate[] = [
  {
    id: "lp-12",
    name: "12\" LP",
    diameterInches: 12,
    dpi: 300,
    bleedInches: 0.125,
    safeZoneInches: 0.25,
    backgroundColor: "#ffffff",
  },
  {
    id: "ep-10",
    name: "10\" EP",
    diameterInches: 10,
    dpi: 300,
    bleedInches: 0.125,
    safeZoneInches: 0.25,
    backgroundColor: "#f7f7f7",
  },
  {
    id: "single-7",
    name: "7\" Single",
    diameterInches: 7,
    dpi: 300,
    bleedInches: 0.1,
    safeZoneInches: 0.2,
    backgroundColor: "#ffffff",
  },
];

type FabricCanvas = FabricNamespace.Canvas;
type FabricSerializedCanvas = ReturnType<FabricCanvas["toJSON"]>;
type FabricCircle = FabricNamespace.Circle;
type FabricRect = FabricNamespace.Rect;
type FabricTextbox = FabricNamespace.Textbox;
type FabricObject = FabricNamespace.Object;

type CanvasReadyPayload = {
  canvas: FabricCanvas;
  fabric: FabricModule;
};

export interface ExportedDesign {
  previewUrl: string;
  previewBlob: Blob;
  productionUrl: string;
  productionBlob: Blob;
  templateId: string;
  createdAt: string;
  canvasJson: FabricSerializedCanvas;
}

const FALLBACK_TEMPLATE: SlipmatTemplate = TEMPLATE_PRESETS[0] ?? {
  id: "custom",
  name: "Custom",
  diameterInches: 12,
  dpi: 300,
  bleedInches: 0,
  safeZoneInches: 0,
  backgroundColor: "#ffffff",
};

export const useSlipmatDesigner = () => {
  const templates = ref<SlipmatTemplate[]>(TEMPLATE_PRESETS);
  const selectedTemplate = ref<SlipmatTemplate>(FALLBACK_TEMPLATE);

  const fabricApi = shallowRef<FabricModule | null>(null);
  const canvases = shallowRef<Record<string, FabricCanvas>>({});
  const activeCanvasId = ref<string>('front');
  const canvas = computed(() => canvases.value[activeCanvasId.value] || null);
  const backgroundCircle = shallowRef<Record<string, FabricCircle | null>>({});
  const safeZoneCircle = shallowRef<Record<string, FabricCircle | null>>({});

  const previewUrl = ref<string | null>(null);
  const previewBlob = shallowRef<Blob | null>(null);
  const productionBlob = shallowRef<Blob | null>(null);
  const productionObjectUrl = ref<string | null>(null);
  const isExporting = ref(false);
  const activeFillColor = ref<string | null>(null);
  const activeStrokeColor = ref<string | null>(null);
  const hasStyleableSelection = ref(false);
  const zoomLevel = ref(1);

  let previewRefreshScheduled = false;

  const displaySize = computed(() => DISPLAY_SIZE);

  const productionPixelSize = computed(() =>
    Math.round(selectedTemplate.value.diameterInches * selectedTemplate.value.dpi)
  );

  const templateLabel = computed(
    () =>
      `${selectedTemplate.value.name} (${selectedTemplate.value.diameterInches}\" @ ${selectedTemplate.value.dpi} DPI)`
  );

  const zoomPercent = computed(() => Math.round(zoomLevel.value * 100));

  const clampZoom = (value: number) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, value));

  const schedulePreviewRefresh = () => {
    if (previewRefreshScheduled) {
      return;
    }
    previewRefreshScheduled = true;
    if (typeof window !== "undefined") {
      requestAnimationFrame(async () => {
        previewRefreshScheduled = false;
        await refreshPreview();
      });
    }
  };

  const maintainStaticLayerOrder = () => {
    const currentCanvas = canvas.value;
    if (!currentCanvas) {
      return;
    }
    const bgCircle = backgroundCircle.value[activeCanvasId.value];
    const safeCircle = safeZoneCircle.value[activeCanvasId.value];
    
    if (bgCircle) {
      const bgRect = bgCircle as any;
      if (bgRect.set) {
        bgRect.set({ 
          width: currentCanvas.getWidth(), 
          height: currentCanvas.getHeight() 
        });
      }
    }
    if (bgCircle) {
      currentCanvas.sendObjectToBack(bgCircle);
    }
    if (safeCircle) {
      currentCanvas.bringObjectToFront(safeCircle);
    }
    currentCanvas.requestRenderAll();
  };

  const isStaticObject = (object: FabricObject | null) => {
    const bgCircle = backgroundCircle.value[activeCanvasId.value];
    const safeCircle = safeZoneCircle.value[activeCanvasId.value];
    return object === bgCircle || object === safeCircle;
  };

  const getSelectedObjects = (): FabricObject[] => {
    const currentCanvas = canvas.value;
    if (!currentCanvas) {
      return [];
    }
    const activeObjects = currentCanvas.getActiveObjects() as FabricObject[];
    if (activeObjects && activeObjects.length) {
      return activeObjects;
    }
    const activeObject = currentCanvas.getActiveObject() as FabricObject | null;
    return activeObject ? [activeObject] : [];
  };

  const getStyleableSelection = (): FabricObject[] =>
    getSelectedObjects().filter((object) => {
      if (!object || isStaticObject(object)) {
        return false;
      }
      const candidate = object as FabricObject & {
        set: (key: string, value: unknown) => void;
        fill?: unknown;
        stroke?: unknown;
      };
      return typeof candidate.set === "function" && ("fill" in candidate || "stroke" in candidate);
    });

  const updateSelectedStyleState = () => {
    const styleable = getStyleableSelection();
    hasStyleableSelection.value = styleable.length > 0;
    if (!styleable.length) {
      activeFillColor.value = null;
      activeStrokeColor.value = null;
      return;
    }

    const primary = styleable[0] as FabricObject & {
      fill?: unknown;
      stroke?: unknown;
    };

    activeFillColor.value =
      primary && typeof primary.fill === "string" ? primary.fill : null;
    activeStrokeColor.value =
      primary && typeof primary.stroke === "string" ? primary.stroke : null;

    canvas.value?.requestRenderAll();
  };

  const canStyleSelection = computed(() => hasStyleableSelection.value);

  const refreshPreview = async () => {
    const currentCanvas = canvas.value;
    if (!currentCanvas) {
      return;
    }
    const multiplier = PREVIEW_SIZE / currentCanvas.getWidth();
    const url = currentCanvas.toDataURL({
      format: "png",
      multiplier,
      enableRetinaScaling: true,
    });
    previewUrl.value = url;
    previewBlob.value = await dataUrlToBlob(url);
  };

  const applyTemplateToCanvas = () => {
    const currentCanvas = canvas.value;
    const fabric = fabricApi.value;
    if (!currentCanvas || !fabric) {
      return;
    }

    const width = currentCanvas.getWidth();
    const height = currentCanvas.getHeight();
    const { backgroundColor, safeZoneInches = 0, diameterInches } =
      selectedTemplate.value;

    const canvasId = activeCanvasId.value;
    const bgRect =
      backgroundCircle.value[canvasId] as any ??
      new fabric.Rect({
        left: 0,
        top: 0,
        width: width,
        height: height,
        originX: "left",
        originY: "top",
        selectable: false,
        evented: false,
        rx: 8,
        ry: 8,
      });

    bgRect.set({ fill: backgroundColor });
    bgRect.set({ width: width, height: height });

    if (!backgroundCircle.value[canvasId]) {
      backgroundCircle.value[canvasId] = bgRect as any;
      currentCanvas.add(bgRect);
    }
    currentCanvas.sendObjectToBack(bgRect);
    currentCanvas.requestRenderAll();

    const safeRatio = Math.max(
      0,
      (diameterInches - safeZoneInches * 2) / diameterInches
    );

    const safeMargin = ((1 - safeRatio) * Math.min(width, height)) / 2;

    const safeRect =
      safeZoneCircle.value[canvasId] as any ??
      new fabric.Rect({
        left: safeMargin,
        top: safeMargin,
        width: width - safeMargin * 2,
        height: height - safeMargin * 2,
        originX: "left",
        originY: "top",
        fill: "rgba(0,0,0,0)",
        stroke: "#4b5563",
        strokeDashArray: [8, 8],
        strokeWidth: 1,
        selectable: false,
        evented: false,
        hoverCursor: "default",
        rx: 8,
        ry: 8,
      });

    safeRect.set({ 
      left: safeMargin,
      top: safeMargin,
      width: width - safeMargin * 2, 
      height: height - safeMargin * 2 
    });

    if (!safeZoneCircle.value[canvasId]) {
      safeZoneCircle.value[canvasId] = safeRect as any;
      currentCanvas.add(safeRect);
    }

    maintainStaticLayerOrder();
    currentCanvas.requestRenderAll();

    currentCanvas.clipPath = new fabric.Rect({
      left: 0,
      top: 0,
      width: width,
      height: height,
      originX: "left",
      originY: "top",
      absolutePositioned: true,
      rx: 8,
      ry: 8,
    });

    currentCanvas.renderAll();
    schedulePreviewRefresh();
  };

  const registerCanvas = ({ canvas: instance, fabric, canvasId = 'front' }: CanvasReadyPayload & { canvasId?: string }) => {
    fabricApi.value = fabric;
    canvases.value[canvasId] = instance;
    
    const previousActiveId = activeCanvasId.value;
    // Temporarily set this canvas as active to initialize it
    activeCanvasId.value = canvasId;

    const canvasWidth = DISPLAY_SIZE;
    const canvasHeight = Math.round(DISPLAY_SIZE * 0.67); // 3:2 aspect ratio for table
    instance.setDimensions({ width: canvasWidth, height: canvasHeight });
    instance.preserveObjectStacking = true;

    const refreshEvents = [
      "object:added",
      "object:modified",
      "object:removed",
      "object:skewing",
      "object:scaling",
      "object:rotating",
      "object:moving",
    ] as const;

    const handleMutation = () => {
      maintainStaticLayerOrder();
      updateSelectedStyleState();
      schedulePreviewRefresh();
    };

    refreshEvents.forEach((eventName) => {
      instance.on(eventName, handleMutation);
    });

    const selectionEvents = [
      "selection:created",
      "selection:updated",
      "selection:cleared",
    ] as const;

    selectionEvents.forEach((eventName) => {
      instance.on(eventName, () => updateSelectedStyleState());
    });

    instance.on("mouse:wheel", (opt) => {
      const { e } = opt;
      const delta = e.deltaY;
      const direction = delta > 0 ? -ZOOM_STEP : ZOOM_STEP;
      const nextZoom = zoomLevel.value + direction;
      const pointer = instance.getPointer(e);
      const fabric = fabricApi.value;
      if (!fabric) {
        return;
      }
      const point = new fabric.Point(pointer.x, pointer.y);
      setZoom(nextZoom, { point });
      e.preventDefault();
      e.stopPropagation();
    });

    applyTemplateToCanvas();
    updateSelectedStyleState();
    setZoom(zoomLevel.value);
    schedulePreviewRefresh();
    
    // Restore the active canvas to 'front' if this wasn't the front canvas
    if (canvasId !== 'front' && previousActiveId) {
      activeCanvasId.value = previousActiveId;
    } else if (canvasId === 'front') {
      // Keep 'front' as active
      activeCanvasId.value = 'front';
    }
  };

  const unregisterCanvas = (canvasId?: string) => {
    if (canvasId && canvases.value[canvasId]) {
      canvases.value[canvasId]?.dispose();
      delete canvases.value[canvasId];
      delete backgroundCircle.value[canvasId];
      delete safeZoneCircle.value[canvasId];
    } else {
      // Unregister all
      Object.values(canvases.value).forEach(c => c?.dispose());
      canvases.value = {};
      backgroundCircle.value = {};
      safeZoneCircle.value = {};
      fabricApi.value = null;
      activeFillColor.value = null;
      activeStrokeColor.value = null;
      hasStyleableSelection.value = false;
      zoomLevel.value = 1;
    }
  };

  const setActiveCanvas = (canvasId: string) => {
    if (canvases.value[canvasId]) {
      activeCanvasId.value = canvasId;
      updateSelectedStyleState();
    }
  };

  const centerPoint = () => {
    const currentCanvas = canvas.value;
    if (!currentCanvas) {
      return { x: DISPLAY_SIZE / 2, y: DISPLAY_SIZE / 2 };
    }
    return {
      x: currentCanvas.getWidth() / 2,
      y: currentCanvas.getHeight() / 2,
    };
  };

  const addTextbox = () => {
    const fabric = fabricApi.value;
    const currentCanvas = canvas.value;
    if (!fabric || !currentCanvas) {
      return;
    }
    const { x, y } = centerPoint();
    const textbox: FabricTextbox = new fabric.Textbox("Your Text", {
      left: x,
      top: y,
      originX: "center",
      originY: "center",
      fill: "#111827",
      fontSize: 36,
      fontFamily: "Helvetica Neue, Helvetica, Arial, sans-serif",
      editable: true,
      width: DISPLAY_SIZE * 0.5,
      textAlign: "center",
    });
    currentCanvas.add(textbox);
    currentCanvas.setActiveObject(textbox);
    maintainStaticLayerOrder();
    currentCanvas.requestRenderAll();
    updateSelectedStyleState();
    schedulePreviewRefresh();
  };

  const addShape = (shape: "circle" | "rect") => {
    const fabric = fabricApi.value;
    const currentCanvas = canvas.value;
    if (!fabric || !currentCanvas) {
      return;
    }
    const { x, y } = centerPoint();
    let object: FabricCircle | FabricRect;

    if (shape === "circle") {
      object = new fabric.Circle({
        left: x,
        top: y,
        radius: DISPLAY_SIZE * 0.18,
        originX: "center",
        originY: "center",
        fill: "rgba(59, 130, 246, 0.4)",
        stroke: "#3b82f6",
        strokeWidth: 2,
      });
    } else {
      object = new fabric.Rect({
        left: x,
        top: y,
        width: DISPLAY_SIZE * 0.3,
        height: DISPLAY_SIZE * 0.18,
        originX: "center",
        originY: "center",
        rx: 12,
        ry: 12,
        fill: "rgba(16, 185, 129, 0.35)",
        stroke: "#10b981",
        strokeWidth: 2,
      });
    }

    currentCanvas.add(object);
    currentCanvas.setActiveObject(object);
    maintainStaticLayerOrder();
    currentCanvas.requestRenderAll();
    updateSelectedStyleState();
    schedulePreviewRefresh();
  };

  const addImageFromFile = async (file: File) => {
    const fabric = fabricApi.value;
    const currentCanvas = canvas.value;
    if (!fabric || !currentCanvas) {
      return;
    }

    const dataUrl = await readFileAsDataUrl(file);

    const img = await fabric.Image.fromURL(dataUrl, {
      crossOrigin: "anonymous",
    });

    const { x, y } = centerPoint();
    img.set({
      left: x,
      top: y,
      originX: "center",
      originY: "center",
      selectable: true,
      clipPath: undefined,
    });

    const maxSize = DISPLAY_SIZE * 0.8;
    const scale = Math.min(
      1,
      maxSize / Math.max(img.width ?? maxSize, img.height ?? maxSize)
    );
    img.scale(scale);
    currentCanvas.add(img);
    currentCanvas.setActiveObject(img);
    maintainStaticLayerOrder();
    currentCanvas.requestRenderAll();
    updateSelectedStyleState();
    schedulePreviewRefresh();
  };

  const setActiveFillColor = (color: string) => {
    const currentCanvas = canvas.value;
    if (!currentCanvas) {
      return;
    }
    const targets = getStyleableSelection();
    if (!targets.length) {
      return;
    }
    targets.forEach((object) => {
      (object as FabricObject & { set: (key: string, value: unknown) => void }).set(
        "fill",
        color
      );
    });
    activeFillColor.value = color;
    currentCanvas.requestRenderAll();
    updateSelectedStyleState();
    schedulePreviewRefresh();
  };

  const setActiveStrokeColor = (color: string) => {
    const currentCanvas = canvas.value;
    if (!currentCanvas) {
      return;
    }
    const targets = getStyleableSelection();
    if (!targets.length) {
      return;
    }
    targets.forEach((object) => {
      const target = object as FabricObject & {
        set: (key: string, value: unknown) => void;
        strokeWidth?: number;
      };
      target.set("stroke", color);
      if (target.strokeWidth === undefined || target.strokeWidth === 0) {
        target.set("strokeWidth", 2);
      }
    });
    activeStrokeColor.value = color;
    currentCanvas.requestRenderAll();
    updateSelectedStyleState();
    schedulePreviewRefresh();
  };

  const setZoom = (value: number, options?: { point?: FabricNamespace.Point }) => {
    const currentCanvas = canvas.value;
    const fabric = fabricApi.value;
    if (!currentCanvas || !fabric) {
      return;
    }
    const clamped = clampZoom(value);

    const targetPoint =
      options?.point ??
      new fabric.Point(
        currentCanvas.getWidth() / 2,
        currentCanvas.getHeight() / 2
      );

    currentCanvas.zoomToPoint(targetPoint, clamped);

    if (!options?.point) {
      const viewport = currentCanvas.viewportTransform;
      if (viewport) {
        const width = currentCanvas.getWidth();
        const height = currentCanvas.getHeight();
        viewport[4] = (width - width * clamped) / 2;
        viewport[5] = (height - height * clamped) / 2;
        currentCanvas.setViewportTransform(viewport);
      }
    }

    zoomLevel.value = clamped;
    maintainStaticLayerOrder();
    currentCanvas.requestRenderAll();
    schedulePreviewRefresh();
  };

  const zoomIn = () => setZoom(zoomLevel.value + ZOOM_STEP);

  const zoomOut = () => setZoom(zoomLevel.value - ZOOM_STEP);

  const resetZoom = () => {
    const currentCanvas = canvas.value;
    if (currentCanvas) {
      currentCanvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
    }
    setZoom(1);
  };

  const clearDesign = () => {
    const currentCanvas = canvas.value;
    if (!currentCanvas) {
      return;
    }
    const staticObjects = new Set<FabricObject | null>([
      backgroundCircle.value,
      safeZoneCircle.value,
    ]);
    currentCanvas
      .getObjects()
      .filter(
        (object: FabricObject | null): object is FabricObject =>
          !staticObjects.has(object)
      )
      .forEach((object: FabricObject) => currentCanvas.remove(object));
    currentCanvas.discardActiveObject();
    updateSelectedStyleState();
    schedulePreviewRefresh();
  };

  const selectTemplate = (templateId: string) => {
    const template = templates.value.find((entry) => entry.id === templateId);
    if (template) {
      selectedTemplate.value = template;
    }
  };

  const waitForCanvasReady = async (): Promise<FabricCanvas> => {
    if (canvas.value) {
      return canvas.value;
    }

    await new Promise<void>((resolve) => {
      const stop = watch(
        canvas,
        (value) => {
          if (value) {
            stop();
            resolve();
          }
        },
        { immediate: false }
      );
    });

    if (!canvas.value) {
      throw new Error("Canvas not ready");
    }

    return canvas.value;
  };

  const loadDesign = async (payload: {
    templateId?: string | null;
    canvasJson: string | FabricSerializedCanvas;
    previewUrl?: string | null;
  }) => {
    if (payload.templateId) {
      selectTemplate(payload.templateId);
    }

    const currentCanvas = await waitForCanvasReady();
    if (!fabricApi.value) {
      throw new Error("Fabric API not ready");
    }

    const parsedJson =
      typeof payload.canvasJson === "string"
        ? (JSON.parse(payload.canvasJson) as FabricSerializedCanvas)
        : payload.canvasJson;

    await new Promise<void>((resolve, reject) => {
      currentCanvas.loadFromJSON(parsedJson, () => {
        maintainStaticLayerOrder();
        updateSelectedStyleState();
        currentCanvas.renderAll();
        schedulePreviewRefresh();
        resolve();
      });
    }).catch((error) => {
      throw error;
    });

    // Reset cached assets; caller can provide preview if available.
    previewBlob.value = null;
    productionBlob.value = null;
    if (productionObjectUrl.value) {
      URL.revokeObjectURL(productionObjectUrl.value);
      productionObjectUrl.value = null;
    }

    if (payload.previewUrl) {
      previewUrl.value = payload.previewUrl;
    } else {
      await refreshPreview();
    }
  };

  const exportDesign = async (): Promise<ExportedDesign | null> => {
    const currentCanvas = canvas.value;
    if (!currentCanvas) {
      return null;
    }
    isExporting.value = true;
    try {
      const previewMultiplier = PREVIEW_SIZE / currentCanvas.getWidth();
      const previewDataUrl = currentCanvas.toDataURL({
        format: "png",
        multiplier: previewMultiplier,
        enableRetinaScaling: true,
      });
      const previewDataBlob = await dataUrlToBlob(previewDataUrl);

      const productionSize = productionPixelSize.value;
      const productionMultiplier = productionSize / currentCanvas.getWidth();
      const productionDataUrl = currentCanvas.toDataURL({
        format: "png",
        multiplier: productionMultiplier,
        enableRetinaScaling: true,
      });
      const productionDataBlob = await dataUrlToBlob(productionDataUrl);

      const canvasJson = currentCanvas.toJSON();

      previewUrl.value = previewDataUrl;
      previewBlob.value = previewDataBlob;
      productionBlob.value = productionDataBlob;

      if (productionObjectUrl.value) {
        URL.revokeObjectURL(productionObjectUrl.value);
      }
      productionObjectUrl.value = URL.createObjectURL(productionDataBlob);

      return {
        previewUrl: previewDataUrl,
        previewBlob: previewDataBlob,
        productionUrl: productionDataUrl,
        productionBlob: productionDataBlob,
        templateId: selectedTemplate.value.id,
        createdAt: new Date().toISOString(),
        canvasJson,
      };
    } finally {
      isExporting.value = false;
    }
  };

  const downloadPreview = async () => {
    if (!previewUrl.value) {
      await refreshPreview();
    }
    if (!previewUrl.value) {
      return;
    }
    triggerDownload(previewUrl.value, `${selectedTemplate.value.id}-preview.png`);
  };

  const downloadProduction = async () => {
    if (!productionObjectUrl.value) {
      const exportResult = await exportDesign();
      if (!exportResult) {
        return;
      }
    }
    if (!productionObjectUrl.value) {
      return;
    }
    triggerDownload(
      productionObjectUrl.value,
      `${selectedTemplate.value.id}-production.png`
    );
  };

  const setBackgroundColor = (color: string) => {
    const bgCircle = backgroundCircle.value;
    if (!bgCircle || !canvas.value) {
      return;
    }
    bgCircle.set({ fill: color });
    selectedTemplate.value = {
      ...selectedTemplate.value,
      backgroundColor: color,
    };
    canvas.value.requestRenderAll();
    schedulePreviewRefresh();
  };

  watch(selectedTemplate, () => {
    resetZoom();
    applyTemplateToCanvas();
    maintainStaticLayerOrder();
    updateSelectedStyleState();
    schedulePreviewRefresh();
  });

  return {
    templates,
    selectedTemplate,
    selectTemplate,
    loadDesign,
    displaySize,
    productionPixelSize,
    templateLabel,
    previewUrl,
    previewBlob,
    productionBlob,
    productionObjectUrl,
    isExporting,
    activeFillColor,
    activeStrokeColor,
    canStyleSelection,
    zoomLevel,
    zoomPercent,
    minZoom: MIN_ZOOM,
    maxZoom: MAX_ZOOM,
    registerCanvas,
    unregisterCanvas,
    setActiveCanvas,
    addTextbox,
    addShape,
    addImageFromFile,
    setActiveFillColor,
    setActiveStrokeColor,
    setBackgroundColor,
    setZoom,
    zoomIn,
    zoomOut,
    resetZoom,
    clearDesign,
    exportDesign,
    downloadPreview,
    downloadProduction,
    refreshPreview,
    schedulePreviewRefresh,
    applyTemplateToCanvas,
    getCanvasElement: () => canvas.value?.lowerCanvasEl || null,
  };
};

const readFileAsDataUrl = (file: File): Promise<string> => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result as string);
    reader.onerror = () => reject(reader.error ?? new Error("Failed to read file"));
    reader.readAsDataURL(file);
  });
};

const dataUrlToBlob = async (dataUrl: string): Promise<Blob> => {
  const response = await fetch(dataUrl);
  return response.blob();
};

const triggerDownload = (url: string, filename: string) => {
  if (typeof window === "undefined") {
    return;
  }
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = filename;
  anchor.rel = "noopener";
  anchor.click();
};
