

type SlotVisualType = 'text' | 'image' | 'design';

const SLOT_VISUALS: Record<SlotVisualType, { color: string; icon: Array<{ d: string; opacity?: number }> }> = {
  text: {
    color: '#059669',
    icon: [
      {
        d: 'm21,0H3C1.34,0,0,1.34,0,3v20.25c0,.28.16,.54,.4,.67,.25,.13,.55,.11,.78-.05l5.17-3.66c.95-.67,2.08-1.03,3.24-1.03h11.4c.8,0,1.56-.32,2.12-.88s.88-1.33,.88-2.12V3c0-.8-.32-1.56-.88-2.12S21.8,0,21,0h0Z',
        opacity: 0.3,
      },
      {
        d: 'm16.55,8.58H5.87c-.59,0-1.06-.47-1.06-1.06v-1.06c0-.59,.47-1.06,1.06-1.06h10.68c.59,0,1.06,.47,1.06,1.06v1.06c0,.59-.47,1.06-1.06,1.06Zm-5.33,5.98h-5.34c-.59,0-1.06-.47-1.06-1.06v-1.06c0-.59,.47-1.06,1.06-1.06h5.34c.59,0,1.06,.47,1.06,1.06v1.06c0,.59-.47,1.06-1.06,1.06Z',
      },
    ],
  },
  image: {
    color: '#2563eb',
    icon: [
      {
        d: 'm22.35,7.2H5.27c-.91,0-1.65,.74-1.65,1.65v6.87l1.5-1.65c1.62-1.78,4.42-1.78,6.04,0l2.63,2.88c.19,.21,.52,.21,.71,0l.92-.99c1.62-1.76,4.4-1.76,6.02,0l2.57,2.79v-9.9c0-.91-.74-1.65-1.65-1.65Zm-4.43,5.11c-1.16,.38-2.22-.68-1.84-1.84,.13-.4,.46-.73,.86-.86,1.16-.38,2.22,.68,1.84,1.84-.13,.4-.46,.73-.86,.86Z',
      },
      {
        d: 'm24,21.15c0,.94-.76,1.7-1.7,1.7H5.32c-.94,0-1.7-.76-1.7-1.7v-3.04l2.51-2.75c1.08-1.18,2.94-1.18,4.03,0l3.6,3.94c.21,.23,.57,.23,.78,0l1.88-2.04c1.08-1.17,2.93-1.17,4.01,0l3.57,3.88h0ZM4.05,5.27h15.89c.25,0,.45-.2,.45-.45v-2.05c0-.94-.76-1.7-1.7-1.7H1.7c-.94,0-1.7,.76-1.7,1.7v11.13c0,.94,.76,1.7,1.7,1.7h.19c.25,0,.45-.2,.45-.45V6.97c0-.94,.76-1.7,1.7-1.7Z',
        opacity: 0.3,
      },
    ],
  },
  design: {
    color: '#7c3aed',
    icon: [
      {
        d: 'm23.51,1.44c-.62-.62-1.62-.66-2.29-.09l-8.88,7.58c.31,.11,.61,.24,.9,.41,1.1,.67,2.17,1.87,2.62,2.93,.05,.13,.09,.26,.13,.38l7.61-8.93c.56-.67,.53-1.67-.1-2.29h0Zm-13.83,11.94c-2.52-1.54-5.77,.7-6.32,3.3C2.95,18.58,2.43,20.49,0,21.65c4.19,2.72,8.46,1.09,10.81-1.54,1.05-1.18,1.55-2.93,.93-4.43-.35-.86-1.27-1.82-2.07-2.3h0Z',
        opacity: 0.3,
      },
      {
        d: 'm14.26,12.93c-.3-.71-1.15-1.64-1.93-2.12-.31-.19-.63-.31-.94-.37l-.71-.12-1.27,1.08c.39,.12,.77,.29,1.13,.51,.98,.6,2.22,1.8,2.76,3.11,.07,.18,.12,.36,.17,.54l1.09-1.28-.13-.68c-.05-.27-.11-.48-.19-.69h0Z',
      },
    ],
  },
};

const normalizeSlotType = (type: any): SlotVisualType => {
  const normalized = String(type ?? '').trim().toLowerCase();
  return normalized === 'image' || normalized === 'design' ? normalized : 'text';
};

const getDefaultSlotColor = (type: SlotVisualType) => SLOT_VISUALS[type]?.color ?? SLOT_VISUALS.text.color;

const normalizeSlotColor = (slot: any, type: SlotVisualType) => {
  const color = String(slot?.color ?? '').trim();
  return color || getDefaultSlotColor(type);
};

const getSlotTargetWidth = (slot: any) => {
  const n = Number(slot?.targetWidth ?? slot?.objectWidth ?? slot?.width);
  return Number.isFinite(n) && n > 0 ? n : undefined;
};

const getSlotTargetHeight = (slot: any) => {
  const n = Number(slot?.targetHeight ?? slot?.objectHeight ?? slot?.height);
  return Number.isFinite(n) && n > 0 ? n : undefined;
};

const getSlotLabel = (slot: any) => {
  const type = normalizeSlotType(slot?.type);
  return String(slot?.label ?? '').trim() || type.toUpperCase();
};

const getObjectCenter = (obj: any) => {
  try {
    const center = obj?.getCenterPoint?.();
    const x = Number(center?.x);
    const y = Number(center?.y);
    if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
  } catch {}
  return {
    x: Number(obj?.left ?? 0) || 0,
    y: Number(obj?.top ?? 0) || 0,
  };
};

const stableJson = (value: any) => {
  try { return JSON.stringify(value ?? {}); } catch { return '{}'; }
};

const hasSlotBringToFront = (slot: any) => Boolean(slot?.bringToFront);

export const createFixedSlotsRuntime = (config: {
  canvas: any;
  fabric: any;
  getFixedSlotsState: () => any;
  findSlotById: (id: string) => any;
  ensureObjectId: (object: any) => string;
  applyFixedConstraintsToObject: (object: any, constraints?: any) => void;
}) => {
  const { canvas, fabric, getFixedSlotsState, findSlotById, ensureObjectId, applyFixedConstraintsToObject } = config;

  const isRenderableIndicator = (obj: any) => {
    if (!obj || !obj._asukaFixedSlotIsIndicator) return false;
    if (obj.visible === false) return false;
    if (typeof obj.opacity === 'number' && obj.opacity <= 0.01) return false;

    try {
      if (obj.type === 'group') {
        const children = obj.getObjects?.() ?? obj._objects ?? [];
        if (Array.isArray(children) && children.length === 0) return false;
      }
    } catch {
      return false;
    }

    return true;
  };

  const createIconObjects = (type: SlotVisualType, color: string) => {
    const iconSize = 17;
    const scale = iconSize / 24;
    const visual = SLOT_VISUALS[type] ?? SLOT_VISUALS.text;
    const out: any[] = [];

    for (const path of visual.icon) {
      try {
        const p = new fabric.Path(path.d, {
          left: 0,
          top: 0,
          originX: 'center',
          originY: 'center',
          scaleX: scale,
          scaleY: scale,
          fill: color,
          opacity: typeof path.opacity === 'number' ? path.opacity : 1,
          strokeWidth: 0,
          selectable: false,
          evented: false,
          excludeFromExport: true,
        });

        // fabric.Path normalizes each path around its own bounding box/pathOffset.
        // Marker icons are built from multiple paths that share a 24x24 SVG viewBox;
        // if every path is placed at the same left/top, multi-part icons drift apart.
        // Reposition each path by its original pathOffset so all pieces keep their
        // shared SVG coordinate system, then center the full 24x24 icon in the group.
        const offsetX = Number((p as any).pathOffset?.x);
        const offsetY = Number((p as any).pathOffset?.y);
        const centerX = Number.isFinite(offsetX) ? offsetX : 12;
        const centerY = Number.isFinite(offsetY) ? offsetY : 12;
        p.set?.({
          left: centerX * scale - iconSize / 2,
          top: centerY * scale - iconSize / 2,
        });
        p.setCoords?.();
        out.push(p);
      } catch {}
    }

    if (!out.length) {
      try {
        out.push(new fabric.Text(type === 'text' ? 'T' : type === 'image' ? 'I' : 'D', {
          left: 0,
          top: 0,
          originX: 'center',
          originY: 'center',
          fontSize: 15,
          fontWeight: 700,
          fill: color,
          selectable: false,
          evented: false,
          excludeFromExport: true,
        }));
      } catch {}
    }

    return out;
  };

  const createIndicator = (slot: any) => {
    const type = normalizeSlotType(slot?.type);
    const x = Number(slot?.x ?? 0);
    const y = Number(slot?.y ?? 0);
    const size = 38;
    const stroke = normalizeSlotColor(slot, type);
    const label = getSlotLabel(slot);

    // Keep a comfortable click/tap target without showing the old visible circle.
    // The marker should now look like an icon only, but still be easy to interact with.
    const hitArea = new fabric.Circle({
      left: 0,
      top: 0,
      radius: size / 2,
      originX: 'center',
      originY: 'center',
      fill: 'rgba(255,255,255,0.001)',
      strokeWidth: 0,
      selectable: false,
      evented: false,
      excludeFromExport: true,
    });

    const iconObjects = createIconObjects(type, stroke);

    // Keep the canvas marker as a compact icon-only Fabric group.
    // Previous versions included extra decoration below/around the icon inside the same group;
    // Fabric then centered the whole bounding box, so the visual
    // marker moved upward every time it was rebuilt/restored. The label is kept
    // in metadata/UI, while the canvas visual is centered exactly on slot.x/y.
    const indicator = new fabric.Group([hitArea, ...iconObjects], {
      left: x,
      top: y,
      originX: 'center',
      originY: 'center',
      selectable: false,
      evented: true,
      lockMovementX: true,
      lockMovementY: true,
      hoverCursor: 'pointer',
      perPixelTargetFind: false,
      excludeFromExport: true,
    });

    (indicator as any).itemGroup = 'Tools';
    (indicator as any)._asukaRole = 'tool';
    (indicator as any)._asukaFixedSlotId = String(slot.id);
    (indicator as any)._asukaFixedSlotType = String(type);
    (indicator as any)._asukaFixedSlotLabel = label;
    (indicator as any)._asukaFixedSlotColor = stroke;
    (indicator as any)._asukaFixedSlotTargetWidth = getSlotTargetWidth(slot);
    (indicator as any)._asukaFixedSlotTargetHeight = getSlotTargetHeight(slot);
    (indicator as any)._asukaBringToFront = hasSlotBringToFront(slot);
    (indicator as any).bringToFront = hasSlotBringToFront(slot);
    (indicator as any)._asukaFixedSlotX = x;
    (indicator as any)._asukaFixedSlotY = y;
    (indicator as any)._asukaFixedSlotConstraints = slot.constraints ?? {};
    (indicator as any)._asukaFixedSlotConstraintsSignature = stableJson(slot.constraints ?? {});
    (indicator as any)._asukaFixedSlotIsIndicator = true;

    try { applyFixedConstraintsToObject(indicator, slot.constraints ?? {}); } catch {}
    ensureObjectId(indicator);
    canvas.add(indicator);
    try { indicator.setPositionByOrigin?.({ x, y }, 'center', 'center'); } catch { try { indicator.set?.({ left: x, top: y }); } catch {} }
    indicator.setCoords?.();
    canvas.requestRenderAll?.();
    return indicator;
  };

  const getIndicatorsForSlot = (slotId: string) => {
    const normalizedSlotId = String(slotId ?? '');
    if (!normalizedSlotId) return [];

    return (canvas.getObjects?.() ?? []).filter(
      (o: any) => o && (o as any)._asukaFixedSlotIsIndicator && String((o as any)?._asukaFixedSlotId ?? '') === normalizedSlotId,
    );
  };

  const removeIndicator = (slotId: string) => {
    const normalizedSlotId = String(slotId ?? '');
    if (!normalizedSlotId) return;

    let changed = false;
    let safety = 0;

    while (safety < 10) {
      safety += 1;
      const indicators = getIndicatorsForSlot(normalizedSlotId);
      if (!indicators.length) break;

      for (const indicator of indicators) {
        try {
          indicator.visible = false;
          indicator.opacity = 0;
          indicator.evented = false;
          indicator.selectable = false;
          (indicator as any).hasControls = false;
          (indicator as any).hasBorders = false;
          (indicator as any).hoverCursor = 'default';
          indicator.excludeFromExport = true;
          indicator.setCoords?.();
        } catch {}

        try {
          const children = indicator.type === 'group' ? (indicator.getObjects?.() ?? indicator._objects ?? []) : [];
          if (Array.isArray(children)) {
            for (const child of children) {
              try {
                child.visible = false;
                child.opacity = 0;
                child.evented = false;
                child.selectable = false;
                child.excludeFromExport = true;
                child.setCoords?.();
              } catch {}
            }
          }
        } catch {}

        try {
          if (canvas.getActiveObject?.() === indicator) {
            canvas.discardActiveObject?.();
          }
        } catch {}

        try { canvas.remove(indicator); changed = true; } catch {}
      }
    }

    if (changed) {
      try { canvas.requestRenderAll?.(); } catch {}
    }
  };

  const consumeActiveSlot = () => {
    const { activeId } = getFixedSlotsState();
    if (!activeId) return null;
    const slot = findSlotById(activeId);
    if (!slot) return null;
    return slot;
  };

  const isIndicatorStaleForSlot = (indicator: any, slot: any) => {
    if (!indicator || !slot) return true;
    const type = normalizeSlotType(slot?.type);
    const label = getSlotLabel(slot);
    const x = Number(slot?.x ?? 0);
    const y = Number(slot?.y ?? 0);
    const storedX = Number((indicator as any)._asukaFixedSlotX);
    const storedY = Number((indicator as any)._asukaFixedSlotY);
    const center = getObjectCenter(indicator);
    const currentX = Number.isFinite(storedX) ? storedX : Number(center.x ?? 0);
    const currentY = Number.isFinite(storedY) ? storedY : Number(center.y ?? 0);

    const color = normalizeSlotColor(slot, type);
    const targetWidth = getSlotTargetWidth(slot);
    const targetHeight = getSlotTargetHeight(slot);

    if (String((indicator as any)._asukaFixedSlotType ?? '') !== type) return true;
    if (String((indicator as any)._asukaFixedSlotLabel ?? '') !== label) return true;
    if (String((indicator as any)._asukaFixedSlotColor ?? '') !== color) return true;
    if (Number((indicator as any)._asukaFixedSlotTargetWidth ?? 0) !== Number(targetWidth ?? 0)) return true;
    if (Number((indicator as any)._asukaFixedSlotTargetHeight ?? 0) !== Number(targetHeight ?? 0)) return true;
    if (stableJson((indicator as any)._asukaFixedSlotConstraints ?? {}) !== stableJson(slot.constraints ?? {})) return true;
    if (Boolean((indicator as any)._asukaBringToFront || (indicator as any).bringToFront) !== hasSlotBringToFront(slot)) return true;
    if (Math.abs(currentX - x) > 0.5 || Math.abs(currentY - y) > 0.5) return true;
    return false;
  };

  const ensureIndicatorsForSlots = () => {
    let changed = false;
    const slots = Array.isArray(getFixedSlotsState()?.slots) ? (getFixedSlotsState()?.slots ?? []) : [];
    const slotIds = new Set(slots.map((slot: any) => String(slot?.id ?? '')).filter(Boolean));

    for (const indicator of (canvas.getObjects?.() ?? [])) {
      const indicatorSlotId = String((indicator as any)?._asukaFixedSlotId ?? '');
      if ((indicator as any)?._asukaFixedSlotIsIndicator && indicatorSlotId && !slotIds.has(indicatorSlotId)) {
        removeIndicator(indicatorSlotId);
        changed = true;
      }
    }

    for (const slot of slots) {
      const slotId = String(slot?.id ?? '');
      if (!slotId) continue;

      const currentObjects = canvas.getObjects?.() ?? [];
      const hasContent = currentObjects.some((o: any) => o && !o._asukaFixedSlotIsIndicator && o._asukaFixedSlotIsContent && String(o._asukaFixedSlotId) === slotId);
      const indicators = currentObjects.filter((o: any) => o && o._asukaFixedSlotIsIndicator && String(o._asukaFixedSlotId) === slotId);
      const renderableIndicator = indicators.find((o: any) => isRenderableIndicator(o));

      if (hasContent) {
        if (indicators.length) {
          removeIndicator(slotId);
          changed = true;
        }
        continue;
      }

      if (renderableIndicator && isIndicatorStaleForSlot(renderableIndicator, slot)) {
        removeIndicator(slotId);
        try { createIndicator(slot); changed = true; } catch {}
        continue;
      }

      if (!renderableIndicator) {
        if (indicators.length) {
          removeIndicator(slotId);
        }
        try { createIndicator(slot); changed = true; } catch {}
      }
    }

    if (changed) {
      try { canvas.requestRenderAll?.(); } catch {}
    }

    return changed;
  };

  const restoreIndicatorForRemovedContent = (obj: any) => {
    const slotId = obj?._asukaFixedSlotId ? String(obj._asukaFixedSlotId) : '';
    if (!slotId || !obj?._asukaFixedSlotIsContent || obj?._asukaFixedSlotIsIndicator) return false;

    const slot = findSlotById(slotId);
    if (!slot) return false;

    const canvasObjectsForSlot = (canvas.getObjects?.() ?? []).filter(
      (o: any) => o && String(o?._asukaFixedSlotId ?? '') === slotId,
    );

    if (canvasObjectsForSlot.some((o: any) => isRenderableIndicator(o))) {
      return false;
    }

    try {
      createIndicator(slot);
      return true;
    } catch {
      return false;
    }
  };

  return {
    createIndicator,
    removeIndicator,
    consumeActiveSlot,
    restoreIndicatorForRemovedContent,
    ensureIndicatorsForSlots,
  };
};
