// @ts-nocheck
import React from 'react';
import * as fabric from 'fabric';

import {
  CANVAS_HEIGHT,
  CANVAS_WIDTH,
  LayoutElement,
  clamp,
} from './schema';

type DesignerCanvasProps = {
  elements: LayoutElement[];
  selectedId: string | null;
  onSelect: (id: string | null) => void;
  onChange: (id: string, patch: Partial<LayoutElement>) => void;
  background: string;
  gradient: { angle?: number; stops?: string[] } | null;
  accent: string;
  text: string;
  secondary: string;
  badge: string;
  qrCodeDataUrl: string | null;
  logoDataUrl: string | null;
  backgroundImageUrl?: string | null;
  scale?: number;
  readOnly?: boolean;
  layoutKey?: string;
};

type FabricObjectWithId = fabric.Object & { elementId?: string };

export function DesignerCanvas({
  elements,
  selectedId,
  onSelect,
  onChange,
  background,
  gradient,
  backgroundImageUrl = null,
  accent,
  text,
  secondary,
  badge,
  qrCodeDataUrl,
  logoDataUrl,
  scale = 1,
  readOnly = false,
}: DesignerCanvasProps): React.JSX.Element {
  const canvasElementRef = React.useRef<HTMLCanvasElement | null>(null);
  const fabricCanvasRef = React.useRef<fabric.Canvas | null>(null);
  const containerRef = React.useRef<HTMLDivElement | null>(null);
  const disposeTokenRef = React.useRef(0);
  const pendingDisposeRef = React.useRef<number | null>(null);
  const pendingTimeoutRef = React.useRef<number | null>(null);
  const lastRenderSignatureRef = React.useRef<string | null>(null);
  const requestedSelectionRef = React.useRef<string | null>(selectedId);

  React.useEffect(() => {
    requestedSelectionRef.current = selectedId;
  }, [selectedId]);

  const destroyCanvas = React.useCallback((canvas: fabric.Canvas | null) => {
    if (!canvas) {
      return;
    }

    if (fabricCanvasRef.current === canvas) {
      fabricCanvasRef.current = null;
    }

    const upperEl = canvas.upperCanvasEl as unknown as (HTMLElement & Record<string, unknown>) | undefined;
    if (upperEl) {
      if (upperEl.__canvas === canvas) {
        delete upperEl.__canvas;
      }
      if (upperEl.__fabricCanvas === canvas) {
        delete upperEl.__fabricCanvas;
      }
    }

    const lowerEl = canvas.lowerCanvasEl as unknown as (HTMLElement & Record<string, unknown>) | undefined;
    if (lowerEl) {
      if (lowerEl.__canvas === canvas) {
        delete lowerEl.__canvas;
      }
      if (lowerEl.__fabricCanvas === canvas) {
        delete lowerEl.__fabricCanvas;
      }
    }

    const targetEl = canvas.getElement() as (HTMLCanvasElement & Record<string, unknown>) | undefined;
    if (targetEl) {
      if (targetEl.__canvas === canvas) {
        delete targetEl.__canvas;
      }
      if (targetEl.__fabricCanvas === canvas) {
        delete targetEl.__fabricCanvas;
      }
    }

    const wrapper = (canvas as unknown as { wrapperEl?: (HTMLElement & Record<string, unknown>) }).wrapperEl;
    if (wrapper) {
      if (wrapper.__fabricCanvas === canvas) {
        delete wrapper.__fabricCanvas;
      }
      if (Object.getOwnPropertyDescriptor(wrapper, '__canvas')) {
        try {
          delete wrapper.__canvas;
        } catch (error) {
          console.warn('[Invites][Fabric] failed to delete wrapper __canvas', error);
        }
      }
      delete wrapper.dataset?.fabric;
    }

    if ((window as unknown as Record<string, unknown>).__inviteCanvas === canvas) {
      delete (window as unknown as Record<string, unknown>).__inviteCanvas;
    }

    try {
      canvas.dispose();
    } catch (error) {
      console.warn('[Invites][Fabric] dispose failed', error);
    }
  }, []);

  React.useLayoutEffect(() => {
    const element = canvasElementRef.current;
    if (!element) {
      console.warn('[Invites][Fabric] canvas element missing');
      return undefined;
    }

    if (pendingTimeoutRef.current !== null) {
      window.clearTimeout(pendingTimeoutRef.current);
      pendingTimeoutRef.current = null;
      pendingDisposeRef.current = null;
    }

    destroyCanvas(fabricCanvasRef.current);

    console.warn('[Invites][Fabric] initializing canvas element');

    const canvas = new fabric.Canvas(element, {
      selection: !readOnly,
      preserveObjectStacking: true,
      perPixelTargetFind: true,
      transparentCorners: true,
      cornerSize: 8,
      padding: readOnly ? 0 : 10, // Default padding for text/objects, 0 for readonly
    });

    fabricCanvasRef.current = canvas;
    lastRenderSignatureRef.current = null;
    const disposeToken = ++disposeTokenRef.current;

    (window as unknown as Record<string, unknown>).__inviteCanvas = canvas;
    (element as unknown as { __fabricCanvas?: fabric.Canvas }).__fabricCanvas = canvas;
    if (containerRef.current) {
      const wrapper = containerRef.current as unknown as (HTMLElement & Record<string, unknown>);
      wrapper.__fabricCanvas = canvas;
      Object.defineProperty(wrapper, '__canvas', {
        configurable: true,
        enumerable: false,
        writable: true,
        value: canvas,
      });
      wrapper.dataset.fabric = 'ready';
    }

    return () => {
      const timeoutId = window.setTimeout(() => {
        destroyCanvas(canvas);
        pendingTimeoutRef.current = null;
        pendingDisposeRef.current = null;
      }, 0);
      pendingTimeoutRef.current = timeoutId;
      pendingDisposeRef.current = disposeToken;
    };
  }, [destroyCanvas, readOnly]);

  React.useEffect(() => {
    const canvas = fabricCanvasRef.current;
    if (!canvas) {
      return;
    }
    canvas.selection = !readOnly;
    canvas.forEachObject((object: fabric.Object) => {
      object.set({
        selectable: !readOnly,
        hoverCursor: readOnly ? 'default' : 'move',
      });
    });
  }, [readOnly]);

  React.useEffect(() => {
    const canvas = fabricCanvasRef.current;
    if (!canvas) {
      return;
    }

    const handleSelection = () => {
      if (readOnly) {
        return;
      }
      const active = canvas.getActiveObject() as FabricObjectWithId | null;
      if (!active || typeof active.elementId !== 'string') {
        onSelect(null);
        return;
      }
      requestedSelectionRef.current = active.elementId ?? null;
      onSelect(active.elementId);
    };

    const handleSelectionCleared = (event?: fabric.TEvent<MouseEvent>) => {
      const pointerEvent = event?.e;
      if (readOnly) {
        return;
      }
      const triggeredByPointer = Boolean(pointerEvent?.e);
      if (!triggeredByPointer && requestedSelectionRef.current) {
        return;
      }
      requestedSelectionRef.current = null;
      onSelect(null);
    };

    const handleObjectModified = (event: fabric.TEvent<MouseEvent>) => {
      if (readOnly) {
        return;
      }
      const target = event.target as FabricObjectWithId | undefined;
      if (!target || typeof target.elementId !== 'string') {
        return;
      }
      const elementId = target.elementId;
      const action = event.transform?.action ?? null;
      const isScalingAction = action?.startsWith('scale') || action === 'resize';

      const bounds = target.getBoundingRect();
      const nextPatch: Partial<LayoutElement> = {
        x: clamp(Math.round(bounds.left ?? 0), 20, CANVAS_WIDTH - 20),
        y: clamp(Math.round(bounds.top ?? 0), 20, CANVAS_HEIGHT - 20),
      };

      const isImage = target.type === 'image';
      if (isScalingAction) {
        if (isImage) {
          const currentScaleX = target.scaleX ?? 1;
          const currentScaleY = target.scaleY ?? 1;
          const naturalWidth = target.width ?? 0;
          const naturalHeight = target.height ?? 0;
          if (elementId === 'qr') {
            // For QR: Enforce uniform scale, cap size, padding=0
            const avgScale = (currentScaleX + currentScaleY) / 2;
            const cappedSize = Math.min(Math.round(naturalWidth * avgScale), 800); // Cap at 800px for massive QR
            nextPatch.width = cappedSize;
            nextPatch.height = cappedSize;
            nextPatch.scaleX = cappedSize / naturalWidth;
            nextPatch.scaleY = cappedSize / naturalHeight;
            target.set({
              left: nextPatch.x,
              top: nextPatch.y,
              scaleX: nextPatch.scaleX,
              scaleY: nextPatch.scaleY,
              padding: 12, // Increased padding for better frame visibility
              uniformScaling: true, // Lock aspect ratio
              lockScalingFlip: true,
            });
          } else {
            nextPatch.width = Math.round(naturalWidth * currentScaleX);
            nextPatch.height = Math.round(naturalHeight * currentScaleY);
            nextPatch.scaleX = currentScaleX;
            nextPatch.scaleY = currentScaleY;
            target.set({ left: nextPatch.x, top: nextPatch.y, padding: 10 });
          }
        } else {
          nextPatch.width = clamp(Math.round(bounds.width ?? target.width ?? 0), 40, CANVAS_WIDTH - 40);
          nextPatch.height = clamp(Math.round(bounds.height ?? target.height ?? 0), 40, CANVAS_HEIGHT - 40);
          target.set({
            scaleX: 1,
            scaleY: 1,
            left: nextPatch.x,
            top: nextPatch.y,
            width: nextPatch.width,
            height: nextPatch.height,
            padding: 10, // Default padding for text
          });
        }
      } else {
        // Dragging: keep size, only move
        target.set({
          left: nextPatch.x,
          top: nextPatch.y,
        });
      }

      onChange(elementId, nextPatch);
      canvas.requestRenderAll();
    };

    canvas.on('selection:created', handleSelection);
    canvas.on('selection:updated', handleSelection);
    canvas.on('selection:cleared', handleSelectionCleared);
    canvas.on('object:modified', handleObjectModified);

    const handleEditingExited = (event: fabric.TEvent<MouseEvent> & { target?: FabricObjectWithId & { text?: string } }) => {
      if (readOnly) {
        return;
      }
      const target = event?.target;
      if (!target || typeof target.elementId !== 'string') {
        return;
      }

      const updatedText = typeof (target as fabric.Textbox).text === 'string' ? (target as fabric.Textbox).text : target.text ?? '';
      handleObjectModified({ target });
      onChange(target.elementId, { content: updatedText });
      canvas.requestRenderAll();
    };

    canvas.on('editing:exited' as unknown as keyof fabric.CanvasEvents, handleEditingExited as fabric.CanvasEvents[keyof fabric.CanvasEvents]);

    return () => {
      canvas.off('selection:created', handleSelection);
      canvas.off('selection:updated', handleSelection);
      canvas.off('selection:cleared', handleSelectionCleared);
      canvas.off('object:modified', handleObjectModified);
      canvas.off('editing:exited' as unknown as keyof fabric.CanvasEvents, handleEditingExited as fabric.CanvasEvents[keyof fabric.CanvasEvents]);
    };
  }, [onChange, onSelect, readOnly]);

  React.useEffect(() => {
    const canvas = fabricCanvasRef.current;
    if (!canvas) {
      return;
    }

    const signature = JSON.stringify({
      elements,
      accent,
      text,
      secondary,
      badge,
      qrCodeDataUrl,
      logoDataUrl,
      background,
      gradient,
      backgroundImageUrl,
      readOnly,
    });

    if (lastRenderSignatureRef.current === signature) {
      return;
    }
    lastRenderSignatureRef.current = signature;

    renderFabricLayout(canvas, {
      elements,
      accentColor: accent,
      textColor: text,
      secondaryColor: secondary,
      badgeColor: badge,
      qrCodeDataUrl,
      logoDataUrl,
      backgroundColor: background,
      backgroundGradient: gradient,
      backgroundImageUrl,
      readOnly,
    }).catch((error) => {
      console.error('[Fabric] Failed to render layout', error);
    });
  }, [
    elements,
    accent,
    text,
    secondary,
    badge,
    qrCodeDataUrl,
    logoDataUrl,
    background,
    gradient,
    backgroundImageUrl,
    readOnly,
  ]);

  React.useEffect(() => {
    const canvas = fabricCanvasRef.current;
    if (!canvas) {
      return;
    }

    if (readOnly) {
      canvas.discardActiveObject();
      canvas.requestRenderAll();
      return;
    }

    if (!selectedId) {
      canvas.discardActiveObject();
      canvas.requestRenderAll();
      return;
    }

    const match = canvas
      .getObjects()
      .find((object): object is FabricObjectWithId => (object as FabricObjectWithId).elementId === selectedId);

    if (match) {
      canvas.setActiveObject(match);
    } else {
      canvas.discardActiveObject();
    }

    canvas.requestRenderAll();
  }, [selectedId, readOnly]);

  React.useEffect(() => {
    const canvas = fabricCanvasRef.current;
    if (!canvas) {
      return;
    }

    const normalizedScale = Number.isFinite(scale) && scale > 0 ? scale : 1;

    canvas.viewportTransform = [normalizedScale, 0, 0, normalizedScale, 0, 0];
    canvas.setDimensions({
      width: CANVAS_WIDTH * normalizedScale,
      height: CANVAS_HEIGHT * normalizedScale,
    });
    canvas.requestRenderAll();
    canvas.calcViewportBoundaries();

    console.log('Zoom applied:', normalizedScale, 'Transform:', canvas.viewportTransform);
  }, [scale]);

  return (
    <div ref={containerRef} className="relative inline-block max-w-full">
      <canvas
        ref={canvasElementRef}
        width={CANVAS_WIDTH}
        height={CANVAS_HEIGHT}
        style={{ display: 'block' }}
      />
    </div>
  );
}

export type FabricRenderOptions = {
  elements: LayoutElement[];
  accentColor: string;
  textColor: string;
  secondaryColor: string;
  badgeColor: string;
  qrCodeDataUrl: string | null;
  logoDataUrl: string | null;
  backgroundColor: string;
  backgroundGradient: { angle?: number; stops?: string[] } | null;
  backgroundImageUrl?: string | null;
  readOnly: boolean;
};

export async function renderFabricLayout(
  canvas: fabric.Canvas,
  options: FabricRenderOptions,
): Promise<void> {
  const {
    elements,
    accentColor,
    textColor,
    secondaryColor,
    badgeColor,
    qrCodeDataUrl,
    logoDataUrl,
    backgroundColor,
    backgroundGradient,
    backgroundImageUrl,
    readOnly,
  } = options;

  canvas.discardActiveObject();
  // Aggressively clear previous objects/state to avoid stacking duplicates between renders.
  try {
    const existing = canvas.getObjects();
    existing.forEach((obj) => canvas.remove(obj));
  } catch (error) {
    console.warn('[Invites][Fabric] failed to remove existing objects', error);
  }
  canvas.clear();

  await applyBackground(canvas, backgroundColor, backgroundGradient, backgroundImageUrl);

  console.debug('[Invites][Fabric] render', {
    elementCount: elements.length,
    backgroundColor,
    hasGradient: Boolean(backgroundGradient),
    readOnly,
  });

  const abortController = new AbortController();
  const objectPromises = elements.map((element) =>
    createFabricObject({
      element,
      accentColor,
      textColor,
      secondaryColor,
      badgeColor,
      qrCodeDataUrl,
      logoDataUrl,
      readOnly,
    }),
  );

  const fabricObjects = await Promise.all(objectPromises);
  abortController.abort(); // Abort any pending loads
  console.debug('[Invites][Fabric] resolved objects', {
    count: fabricObjects.length,
    nulls: fabricObjects.filter((obj) => !obj).length,
  });

  fabricObjects.forEach((object) => {
    if (!object) {
      console.debug('[Invites][Fabric] Skip null fabric object');
      return;
    }
    if (readOnly) {
      object.set({
        selectable: false,
        hoverCursor: 'default',
      });
    }
    try {
      canvas.add(object);
      if (typeof object.setCoords === 'function') {
        object.setCoords();
      }
      const bounds = object.getBoundingRect();
      console.warn('[Invites][Fabric] added object', {
        elementId: (object as FabricObjectWithId).elementId,
        left: bounds.left,
        top: bounds.top,
        width: bounds.width,
        height: bounds.height,
      });
    } catch (error) {
      console.error('[Invites][Fabric] failed to add object', error);
    }
  });

  console.warn('[Invites][Fabric] object count', canvas.getObjects().length);

  canvas.renderAll();
}

export async function applyBackground(
  canvas: fabric.Canvas,
  color: string,
  gradient: { angle?: number; stops?: string[] } | null,
  backgroundImageUrl?: string | null,
): Promise<void> {
  try {
    if (typeof canvas.setBackgroundImage === 'function') {
      canvas.setBackgroundImage(null, canvas.requestRenderAll.bind(canvas));
    } else {
      // Fallback for environments where setBackgroundImage is not present
      (canvas as fabric.StaticCanvas).backgroundImage = null;
      canvas.requestRenderAll();
    }

    if (backgroundImageUrl) {
      try {
        const resolvedUrl = backgroundImageUrl.startsWith('http')
          ? backgroundImageUrl
          : `${window.location.origin}${backgroundImageUrl.startsWith('/') ? '' : '/'}${backgroundImageUrl}`;
        const image = await new Promise<fabric.Image | null>((resolve) => {
          const imgEl = new Image();
          imgEl.crossOrigin = 'anonymous';
          const timeoutId = window.setTimeout(() => resolve(null), 3000);
          imgEl.onload = () => {
            window.clearTimeout(timeoutId);
            resolve(new fabric.Image(imgEl, { crossOrigin: 'anonymous' }));
          };
          imgEl.onerror = () => {
            window.clearTimeout(timeoutId);
            resolve(null);
          };
          imgEl.src = resolvedUrl;
        });
        if (image) {
          const scaleX = CANVAS_WIDTH / (image.width || CANVAS_WIDTH);
          const scaleY = CANVAS_HEIGHT / (image.height || CANVAS_HEIGHT);
          const scale = Math.max(scaleX, scaleY);
          image.set({
            originX: 'left',
            originY: 'top',
            left: 0,
            top: 0,
            scaleX: scale,
            scaleY: scale,
            selectable: false,
            evented: false,
          });
          if (typeof canvas.setBackgroundImage === 'function') {
            canvas.setBackgroundImage(image, canvas.requestRenderAll.bind(canvas));
          } else {
            (canvas as fabric.StaticCanvas).backgroundImage = image;
            canvas.requestRenderAll();
          }
          return;
        }
      } catch (error) {
        console.warn('[Fabric] Failed to load background image', error);
      }
    }
  } catch (error) {
    console.warn('[Fabric] applyBackground failed', error);
  }

  let background: string | fabric.Gradient<'linear'> = color;

  if (gradient?.stops?.length) {
    const angle = ((gradient.angle ?? 180) * Math.PI) / 180;
    const halfWidth = CANVAS_WIDTH / 2;
    const halfHeight = CANVAS_HEIGHT / 2;
    const x = Math.cos(angle);
    const y = Math.sin(angle);

    background = new fabric.Gradient({
      type: 'linear',
      coords: {
        x1: halfWidth - x * halfWidth,
        y1: halfHeight - y * halfHeight,
        x2: halfWidth + x * halfWidth,
        y2: halfHeight + y * halfHeight,
      },
      colorStops: gradient.stops!.map((stop, index) => ({
        offset: gradient.stops!.length === 1 ? 0 : index / (gradient.stops!.length - 1),
        color: stop,
      })),
    });
  }

  const canvasWithBackgroundFn = canvas as fabric.Canvas & {
    setBackgroundColor?: (value: string | fabric.Gradient<'linear'>, callback?: () => void) => void;
  };

  if (typeof canvasWithBackgroundFn.setBackgroundColor === 'function') {
    canvasWithBackgroundFn.setBackgroundColor(background, () => canvas.requestRenderAll());
  } else {
    canvas.backgroundColor = background;
    canvas.requestRenderAll();
  }
}

export type FabricObjectFactoryContext = {
  element: LayoutElement;
  accentColor: string;
  textColor: string;
  secondaryColor: string;
  badgeColor: string;
  qrCodeDataUrl: string | null;
  logoDataUrl: string | null;
  readOnly: boolean;
};

export async function createFabricObject({
  element,
  accentColor,
  textColor,
  secondaryColor,
  badgeColor,
  qrCodeDataUrl,
  logoDataUrl,
  readOnly,
}: FabricObjectFactoryContext): Promise<fabric.Object | null> {
  console.debug('[Invites][Fabric] create element', {
    id: element.id,
    type: element.type,
    width: element.width,
    height: element.height,
    content: element.content,
  });

  const hasRotation = typeof element.rotation === 'number' && element.rotation !== 0;
  const baseConfig = {
    left: element.x,
    top: element.y,
    elementId: element.id,
    selectable: !readOnly,
    hasBorders: !readOnly,
    hasControls: !readOnly,
    originX: hasRotation ? 'center' : 'left',
    originY: hasRotation ? 'center' : 'top',
    angle: element.rotation ?? 0,
  } as FabricObjectWithId;

  switch (element.type) {
    case 'headline':
    case 'subtitle':
    case 'description':
    case 'text':
      return new fabric.Textbox(element.content ?? '', {
        ...baseConfig,
        width: element.width,
        height: element.height,
        fontSize: element.fontSize ?? 36,
        fill: element.fill ?? textColor,
        fontFamily: element.fontFamily ?? 'Manrope',
        textAlign: mapTextAlign(element.align),
        lineHeight: element.lineHeight ?? 1.5,
        charSpacing: element.letterSpacing ?? 0.5,
        padding: 12, // Enhanced padding for better readability
      });
    case 'link':
      return new fabric.Textbox(element.content ?? '', {
        ...baseConfig,
        width: element.width,
        height: element.height,
        fontSize: element.fontSize ?? 24,
        fill: element.fill ?? accentColor,
        fontFamily: element.fontFamily ?? 'Manrope',
        underline: true,
        textAlign: mapTextAlign(element.align),
        lineHeight: element.lineHeight ?? 1.5,
        charSpacing: element.letterSpacing ?? 0.5,
        padding: 10,
      });
    case 'badge':
      return createTextBadge({
        baseConfig,
        text: element.content ?? '',
        width: element.width,
        height: element.height,
        backgroundColor: element.fill ?? badgeColor,
        textColor: '#ffffff',
        fontSize: element.fontSize ?? 22,
        lineHeight: element.lineHeight ?? 1.5,
        letterSpacing: element.letterSpacing ?? 0.5,
      });
    case 'cta':
      return createTextBadge({
        baseConfig,
        text: element.content ?? '',
        width: element.width,
        height: element.height,
        backgroundColor: element.fill ?? accentColor,
        textColor: '#ffffff',
        fontSize: element.fontSize ?? 24,
        cornerRadius: 18,
        lineHeight: element.lineHeight ?? 1.5,
        letterSpacing: element.letterSpacing ?? 0.5,
      });
    case 'logo':
      if (logoDataUrl) {
        return loadImageObject(logoDataUrl, element, baseConfig, {
          objectFit: 'contain',
        });
      }
      return null;
    case 'qr':
      if (qrCodeDataUrl) {
        console.debug(
          '[Invites][Fabric] qr image source',
          qrCodeDataUrl.length,
          qrCodeDataUrl.slice(0, 48),
        );
        const qrImage = await loadImageObject(qrCodeDataUrl, element, baseConfig, {
          shadow: 'rgba(15,23,42,0.25)',
          padding: 0, // No padding to fix large frame
        });
        if (qrImage) {
          if (qrImage instanceof fabric.Image) {
            (qrImage as fabric.Image & { uniformScaling?: boolean }).uniformScaling = true; // Lock aspect ratio
          }
          qrImage.lockScalingFlip = true;
          qrImage.padding = 0;
          qrImage.cornerColor = 'transparent';
          qrImage.borderScaleFactor = 1; // Prevent border inflation on scale
        }
        console.log('QR DataURL:', qrCodeDataUrl ? 'Loaded' : 'Fallback');
        return qrImage;
      }
      console.log('QR Fallback used - DataURL missing');
      return new fabric.Rect({
        ...baseConfig,
        width: element.width,
        height: element.height,
        fill: 'white',
        stroke: secondaryColor,
        strokeWidth: 2,
        rx: 20,
        ry: 20,
      });
    default:
      return new fabric.Textbox(element.content ?? '', {
        ...baseConfig,
        width: element.width,
        height: element.height,
        fontSize: element.fontSize ?? 24,
        fill: secondaryColor,
        fontFamily: element.fontFamily ?? 'Manrope',
        textAlign: mapTextAlign(element.align),
      });
  }
}

export function createTextBadge({
  baseConfig,
  text,
  width,
  height,
  backgroundColor,
  textColor,
  fontSize,
  cornerRadius = 12,
  lineHeight = 1.5,
  letterSpacing = 0.5,
}: {
  baseConfig: FabricObjectWithId;
  text: string;
  width: number;
  height: number;
  backgroundColor: string;
  textColor: string;
  fontSize: number;
  cornerRadius?: number;
  lineHeight?: number;
  letterSpacing?: number;
}): fabric.Group {
  const rect = new fabric.Rect({
    width,
    height,
    rx: cornerRadius,
    ry: cornerRadius,
    fill: backgroundColor,
    left: 0,
    top: 0,
    selectable: false,
    evented: false,
  });

  const label = new fabric.Textbox(text, {
    width: width - 32,
    left: 16,
    top: height / 2,
    fontSize,
    fill: textColor,
    fontFamily: 'Manrope',
    originY: 'center',
    textAlign: 'center',
    lineHeight,
    charSpacing: letterSpacing,
    selectable: false,
    evented: false,
  });

  return new fabric.Group([rect, label], {
    ...baseConfig,
    width,
    height,
    originX: 'left',
    originY: 'top',
  }) as fabric.Group & FabricObjectWithId;
}

export async function loadImageObject(
  source: string,
  element: LayoutElement,
  baseConfig: FabricObjectWithId,
  options?: { objectFit?: 'contain' | 'cover'; shadow?: string; padding?: number },
  abortSignal?: AbortSignal,
): Promise<fabric.Object | null> {
  return new Promise((resolve) => {
    let resolved = false;
    const resolveSafely = (value: fabric.Object | null) => {
      if (resolved) {
        return;
      }
      resolved = true;
      resolve(value);
    };

    const isDataUrl = source.startsWith('data:');

    const onImageLoaded = (img?: HTMLImageElement | HTMLCanvasElement | null) => {
      if (!img || resolved) {
        console.warn('[Invites][Fabric] image load returned empty', { source });
        resolveSafely(null);
        return;
      }

      const image = new fabric.Image(img, { ...baseConfig });

      const intrinsicWidth = image.width ?? element.width;
      const intrinsicHeight = image.height ?? element.height;
      const safeIntrinsicWidth = intrinsicWidth || 1;
      const safeIntrinsicHeight = intrinsicHeight || 1;

      let targetLeft = element.x;
      let targetTop = element.y;
      let scaleX = element.width / safeIntrinsicWidth;
      let scaleY = element.height / safeIntrinsicHeight;

      if (options?.objectFit === 'contain') {
        const ratio = Math.min(scaleX, scaleY);
        scaleX = ratio;
        scaleY = ratio;
        const renderedWidth = safeIntrinsicWidth * ratio;
        const renderedHeight = safeIntrinsicHeight * ratio;
        targetLeft = element.x + (element.width - renderedWidth) / 2;
        targetTop = element.y + (element.height - renderedHeight) / 2;
      }

      image.set({
        ...baseConfig,
        originX: 'left',
        originY: 'top',
        width: safeIntrinsicWidth,
        height: safeIntrinsicHeight,
        scaleX,
        scaleY,
        left: targetLeft,
        top: targetTop,
        padding: options?.padding ?? 0,
      });

      if (options?.shadow) {
        image.set('shadow', options.shadow);
      }

      resolveSafely(image);
    };

    const onError = (error?: unknown) => {
      if (resolved) return;
      console.warn('[Invites][Fabric] failed to load image', source, error);
      resolveSafely(null);
    };

    const abortHandler = () => {
      if (resolved) return;
      console.debug('[Invites][Fabric] Image load aborted', { source });
      resolveSafely(null);
    };

    if (abortSignal) {
      abortSignal.addEventListener('abort', abortHandler);
    }

    try {
      if (isDataUrl) {
        const imageElement = new Image();
        imageElement.onload = () => {
          if (resolved) return;
          console.debug('[Invites][Fabric] image loaded (data-url)', {
            source: source.slice(0, 48),
            width: imageElement.naturalWidth,
            height: imageElement.naturalHeight,
          });
          onImageLoaded(imageElement);
        };
        imageElement.onerror = onError;
        imageElement.src = source;
      } else {
        // Use direct Image constructor approach for better compatibility
        const img = new Image();
        // Avoid tainting canvas when reading remote QR images / logos
        img.crossOrigin = 'anonymous';
        img.onload = () => {
          if (resolved) return;
          console.debug('[Invites][Fabric] image loaded', {
            source: source.slice(0, 48),
            width: img.width,
            height: img.height,
          });
          onImageLoaded(img);
        };
        img.onerror = onError;
        img.src = source;
      }
    } catch (error) {
      onError(error);
    }

    const timeoutId = window.setTimeout(() => {
      if (resolved) return;
      resolveSafely(null);
    }, 3000);

    return () => {
      clearTimeout(timeoutId);
      if (abortSignal) {
        abortSignal.removeEventListener('abort', abortHandler);
      }
    };
  });
}

export function mapTextAlign(align?: LayoutElement['align']): 'left' | 'center' | 'right' {
  switch (align) {
    case 'center':
      return 'center';
    case 'right':
      return 'right';
    default:
      return 'left';
  }
}
