import React from 'react';
import * as fabric from 'fabric';

import {
  CANVAS_HEIGHT,
  CANVAS_WIDTH,
  LayoutElement,
  clamp,
  LayoutElementType,
} 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;
  layoutKey?: string;
  readOnly?: boolean;
};

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

export function DesignerCanvas({
  elements,
  selectedId,
  onSelect,
  onChange,
  background,
  gradient,
  accent,
  text,
  secondary,
  badge,
  qrCodeDataUrl,
  logoDataUrl,
  layoutKey,
  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 destroyCanvas = React.useCallback((canvas: fabric.Canvas | null) => {
    if (!canvas) {
      return;
    }

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

    const upperEl = canvas.upperCanvasEl 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 (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,
    });

    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 (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(() => {
        if (disposeTokenRef.current !== disposeToken) {
          return;
        }

        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) => {
      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;
      }
      onSelect(active.elementId);
    };

    const handleSelectionCleared = () => {
      if (readOnly) {
        return;
      }
      onSelect(null);
    };

    const handleObjectModified = (event: fabric.IEvent<fabric.Object>) => {
      if (readOnly) {
        return;
      }
      const target = event.target as FabricObjectWithId | undefined;
      if (!target || typeof target.elementId !== 'string') {
        return;
      }
      const elementId = target.elementId;

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

      target.set({
        scaleX: 1,
        scaleY: 1,
        left: nextPatch.x,
        top: nextPatch.y,
        width: nextPatch.width,
        height: nextPatch.height,
      });

      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);

    return () => {
      canvas.off('selection:created', handleSelection);
      canvas.off('selection:updated', handleSelection);
      canvas.off('selection:cleared', handleSelectionCleared);
      canvas.off('object:modified', handleObjectModified);
    };
  }, [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,
      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,
      readOnly,
    }).catch((error) => {
      console.error('[Fabric] Failed to render layout', error);
    });
  }, [
    elements,
    accent,
    text,
    secondary,
    badge,
    qrCodeDataUrl,
    logoDataUrl,
    background,
    gradient,
    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 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;
    }
    canvas.setZoom(1);
    canvas.setDimensions(
      {
        width: CANVAS_WIDTH,
        height: CANVAS_HEIGHT,
      },
      { cssOnly: true },
    );
    canvas.requestRenderAll();
  }, []);

  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;
  readOnly: boolean;
};

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

  canvas.discardActiveObject();
  canvas.getObjects().forEach((object) => canvas.remove(object));

  applyBackground(canvas, backgroundColor, backgroundGradient);

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

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

  const fabricObjects = await Promise.all(objectPromises);
  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(true, true);
      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 function applyBackground(
  canvas: fabric.Canvas,
  color: string,
  gradient: { angle?: number; stops?: string[] } | null,
): void {
  let background: string | fabric.Gradient = 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, 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 baseConfig = {
    left: element.x,
    top: element.y,
    elementId: element.id,
    selectable: !readOnly,
    hasBorders: !readOnly,
    hasControls: !readOnly,
  } 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 ?? 26,
        fill: textColor,
        textAlign: mapTextAlign(element.align),
      });
    case 'link':
      return new fabric.Textbox(element.content ?? '', {
        ...baseConfig,
        width: element.width,
        height: element.height,
        fontSize: element.fontSize ?? 24,
        fill: accentColor,
        underline: true,
        textAlign: mapTextAlign(element.align),
      });
    case 'badge':
      return createTextBadge({
        baseConfig,
        text: element.content ?? '',
        width: element.width,
        height: element.height,
        backgroundColor: badgeColor,
        textColor: '#ffffff',
        fontSize: element.fontSize ?? 22,
      });
    case 'cta':
      return createTextBadge({
        baseConfig,
        text: element.content ?? '',
        width: element.width,
        height: element.height,
        backgroundColor: accentColor,
        textColor: '#ffffff',
        fontSize: element.fontSize ?? 24,
        cornerRadius: 18,
      });
    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),
        );
        return loadImageObject(qrCodeDataUrl, element, baseConfig, {
          shadow: 'rgba(15,23,42,0.25)',
        });
      }
      return new fabric.Rect({
        ...baseConfig,
        width: element.width,
        height: element.height,
        fill: secondaryColor,
        rx: 20,
        ry: 20,
      });
    default:
      return new fabric.Textbox(element.content ?? '', {
        ...baseConfig,
        width: element.width,
        height: element.height,
        fontSize: element.fontSize ?? 24,
        fill: secondaryColor,
        textAlign: mapTextAlign(element.align),
      });
  }
}

export function createTextBadge({
  baseConfig,
  text,
  width,
  height,
  backgroundColor,
  textColor,
  fontSize,
  cornerRadius = 12,
}: {
  baseConfig: FabricObjectWithId;
  text: string;
  width: number;
  height: number;
  backgroundColor: string;
  textColor: string;
  fontSize: number;
  cornerRadius?: 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,
    originY: 'center',
    textAlign: 'center',
    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 },
): 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) {
        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 scaleX = element.width / intrinsicWidth;
      const scaleY = element.height / intrinsicHeight;

      image.set({
        ...baseConfig,
        width: element.width,
        height: element.height,
        scaleX,
        scaleY,
      });

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

      if (options?.objectFit === 'contain') {
        const ratio = Math.min(scaleX, scaleY);
        image.set({
          scaleX: ratio,
          scaleY: ratio,
          left: element.x + (element.width - intrinsicWidth * ratio) / 2,
          top: element.y + (element.height - intrinsicHeight * ratio) / 2,
        });
      }

      resolveSafely(image);
    };

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

    try {
      if (isDataUrl) {
        const imageElement = new Image();
        imageElement.onload = () => {
          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 {
        fabric.util.loadImage(
          source,
          (img) => {
            if (!img) {
              onError();
              return;
            }
            console.debug('[Invites][Fabric] image loaded', {
              source: source.slice(0, 48),
              width: (img as HTMLImageElement).width,
              height: (img as HTMLImageElement).height,
            });
            onImageLoaded(img);
          },
          undefined,
          'anonymous',
        );
      }
    } catch (error) {
      onError(error);
    }

    window.setTimeout(() => resolveSafely(null), 3000);
  });
}

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