import apiFetch from '@wordpress/api-fetch';
import { useState, useEffect } from '@wordpress/element';
import { buildTransformStyle } from '../style-controls/Effect/transformUtils';

// Shared hook — imported and re-exported by both blockStyles.js and blockStylesHover.js.
export function useDeviceSettings() {
  const [devices, setDevices] = useState({
    mobile:  { media: '' },
    tablet:  { media: '@media (min-width: 769px)' },
    desktop: { media: '@media (min-width: 1025px)' },
  });

  useEffect(() => {
    const load = async () => {
      try {
        const settings = await apiFetch({ path: '/axcelersblocks/v1/device-settings' });
        const tabletMin  = parseInt(settings.devices.mobile.width  || '768px',  10) + 1;
        const desktopMin = parseInt(settings.devices.tablet.width  || '1024px', 10) + 1;
        setDevices({
          mobile:  { media: '' },
          tablet:  { media: `@media (min-width: ${tabletMin}px)` },
          desktop: { media: `@media (min-width: ${desktopMin}px)` },
        });
      } catch {
        setDevices({
          mobile:  { media: '' },
          tablet:  { media: '@media (min-width: 769px)' },
          desktop: { media: '@media (min-width: 1025px)' },
        });
      }
    };
    load();
  }, []);

  return devices;
}

// Shared helpers
const hasCssValue = (v) => v !== undefined && v !== null && v !== '';

const spacingShorthand = (t, r, b, l) => {
  if (t === r && r === b && b === l) return t;
  if (t === b && r === l) return `${t} ${r}`;
  if (r === l) return `${t} ${r} ${b}`;
  return `${t} ${r} ${b} ${l}`;
};

const borderRadiusShorthand = (tl, tr, br, bl) => {
  if (tl === tr && tr === br && br === bl) return tl;
  if (tl === br && tr === bl) return `${tl} ${tr}`;
  return `${tl} ${tr} ${br} ${bl}`;
};

const hexToRgb = (hex) => {
  if (!hex || typeof hex !== 'string') return '0, 0, 0';
  return `${parseInt(hex.slice(1, 3), 16)}, ${parseInt(hex.slice(3, 5), 16)}, ${parseInt(hex.slice(5, 7), 16)}`;
};

const normaliseIconSize = (raw) => {
  if (raw == null || raw === '') return { width: '', height: '' };
  if (typeof raw === 'string') return { width: raw, height: raw };
  if (typeof raw === 'object') {
    return {
      width: typeof raw.width === 'string' ? raw.width : '',
      height: typeof raw.height === 'string' ? raw.height : '',
    };
  }
  return { width: '', height: '' };
};

// Background-layers transforms (same logic for normal and hover)
const bgImageTransform = (layers) => {
  if (!Array.isArray(layers)) return '';
  return layers.map((layer) => {
    switch (layer?.type) {
      case 'image':   return layer.url ? `url(${layer.url})` : '';
      case 'gradient': return layer.gradient || '';
      case 'overlay':  return layer.color ? `linear-gradient(to left, rgba(${hexToRgb(layer.color)}, ${layer.opacity || 0.5}) 0%, rgba(${hexToRgb(layer.color)}, ${layer.opacity || 0.5}) 100%)` : '';
      case 'none':     return 'none';
      default:         return '';
    }
  }).filter(Boolean).join(', ') || '';
};

const bgPositionTransform = (layers) => {
  if (!Array.isArray(layers)) return '';
  return layers.map((layer) => layer?.type === 'image' ? (layer.position || 'center center') : '').filter(Boolean).join(', ') || '';
};

const bgSizeTransform = (layers) => {
  if (!Array.isArray(layers)) return '';
  return layers.map((layer) => layer?.type === 'image' ? (layer.size || 'cover') : '').filter(Boolean).join(', ') || '';
};

const bgRepeatTransform = (layers) => {
  if (!Array.isArray(layers)) return '';
  return layers.map((layer) => layer?.type === 'image' ? (layer.repeat || 'no-repeat') : '').filter(Boolean).join(', ') || '';
};

const effectValue = (value, fallback) =>
  value === undefined || value === null || value === '' ? fallback : value;

const boxShadowTransform = (effect) => {
  if (!effect?.boxShadow) return null;
  const { inset } = effect.boxShadow;
  const x = effectValue(effect.boxShadow.xOffset, '0px');
  const y = effectValue(effect.boxShadow.yOffset, '0px');
  const blur = effectValue(effect.boxShadow.blurRadius, '0px');
  const spread = effectValue(effect.boxShadow.spreadRadius, '0px');
  const col = effectValue(effect.boxShadow.color, 'transparent');
  const ins = inset ? 'inset ' : '';
  if (x === '0px' && y === '0px' && blur === '0px' && spread === '0px' && col === 'transparent' && !ins) return null;
  return `${ins}${x} ${y} ${blur} ${spread} ${col}`;
};

const transitionTransform = (effect) => {
  if (!effect?.transition) return null;
  const prop = effectValue(effect.transition.property, 'all');
  const dur = effectValue(effect.transition.duration, '0s');
  const timing = effectValue(effect.transition.timingFunction, 'ease');
  const del = effectValue(effect.transition.delay, '0s');
  if (prop === 'all' && dur === '0s' && timing === 'ease' && del === '0s') return null;
  return `${prop} ${dur} ${timing} ${del}`;
};

const filterTransform = (effect) => {
  if (!effect?.filter) return null;
  const { blur, brightness, contrast, grayscale, hueRotate, invert, opacity, saturate, sepia } = effect.filter;
  const parts = [];
  if (blur) parts.push(`blur(${blur})`);
  if (brightness && brightness !== '100%') parts.push(`brightness(${brightness})`);
  if (contrast  && contrast  !== '100%') parts.push(`contrast(${contrast})`);
  if (grayscale && grayscale !== '0%')   parts.push(`grayscale(${grayscale})`);
  if (hueRotate && hueRotate !== '0deg') parts.push(`hue-rotate(${hueRotate})`);
  if (invert    && invert    !== '0%')   parts.push(`invert(${invert})`);
  if (opacity   && opacity   !== '100%') parts.push(`opacity(${opacity})`);
  if (saturate  && saturate  !== '100%') parts.push(`saturate(${saturate})`);
  if (sepia     && sepia     !== '0%')   parts.push(`sepia(${sepia})`);
  return parts.length ? parts.join(' ') : null;
};

const textShadowTransform = (effect) => {
  if (!effect?.textShadow) return null;
  const x = effectValue(effect.textShadow.xOffset, '0px');
  const y = effectValue(effect.textShadow.yOffset, '0px');
  const blur = effectValue(effect.textShadow.blurRadius, '0px');
  const col = effectValue(effect.textShadow.color, 'transparent');
  if (x === '0px' && y === '0px' && blur === '0px' && col === 'transparent') return null;
  return `${x} ${y} ${blur} ${col}`;
};

/**
 * Core CSS generator for both normal and hover states.
 *
 * @param {string}  uniqueClass Block unique class.
 * @param {object}  devices     Device map from useDeviceSettings().
 * @param {object}  attributes  Block attributes (all keys, normal and hover).
 * @param {object}  options
 * @param {boolean} options.hover  When true generates :hover selector rules from __hover attributes.
 */
export function _generateStyles(uniqueClass, devices, attributes, { hover = false, attrSuffix = null, pseudoSelector = null, elementSuffix = '', hasIcon = true } = {}) {
  // A "state" pass is any non-normal pass (hover / focus / active). It reads
  // suffixed attributes and emits rules under a pseudo-class selector, and
  // (like hover) omits img + custom CSS and compares icon size against the base.
  const resolvedSuffix   = attrSuffix !== null ? attrSuffix : (hover ? '__hover' : '');
  const resolvedSelector = pseudoSelector !== null ? pseudoSelector : (hover ? ':hover' : '');
  const isState          = resolvedSuffix !== '';
  // Backward-compat: fill empty mobile values from desktop for min-width output.
  const hasCssV = (v) => {
    if (typeof v === 'boolean' || v == null) return false;
    if (typeof v === 'string') return v !== '';
    if (typeof v === 'object') return Object.values(v).some(hasCssV);
    return false;
  };
  const normalAttrs = { ...attributes };
  const shouldSkipWholeObjectBackfill = (key) =>
    /^(border|iconBorder|linkBorder|linkHoverBorder|linkFocusBorder|linkActiveBorder)(__hover|__focus|__active)?$/.test(key);
  Object.keys(normalAttrs).forEach((key) => {
    if (shouldSkipWholeObjectBackfill(key)) return;
    const val = normalAttrs[key];
    if (val && typeof val === 'object' && 'desktop' in val && 'mobile' in val) {
      if (hasCssV(val.desktop) && !hasCssV(val.mobile)) {
        normalAttrs[key] = { ...val, mobile: val.desktop };
      }
    }
  });

  const stylesCSS = [];
  const deviceStyles          = { mobile: [], tablet: [], desktop: [] };
  const iconDeviceStyles      = { mobile: [], tablet: [], desktop: [] };
  const svgDeviceStyles       = { mobile: [], tablet: [], desktop: [] };
  const linkDeviceStyles      = { mobile: [], tablet: [], desktop: [] };
  const linkHoverDeviceStyles = { mobile: [], tablet: [], desktop: [] };
  const linkFocusDeviceStyles = { mobile: [], tablet: [], desktop: [] };
  const linkActiveDeviceStyles = { mobile: [], tablet: [], desktop: [] };
  // img styles only exist in the normal (non-state) pass
  const imgDeviceStyles  = isState ? null : { mobile: [], tablet: [], desktop: [] };
  const globalStyles = [];

  const styleCascade          = {};
  const iconStyleCascade      = {};
  const svgStyleCascade       = {};
  const linkStyleCascade      = {};
  const linkHoverStyleCascade = {};
  const linkFocusStyleCascade = {};
  const linkActiveStyleCascade = {};
  const imgStyleCascade  = {};
  const iconSizeCascade  = {};

  // Attribute accessor: appends the active state's suffix (e.g. __hover / __focus / __active).
  const s = resolvedSuffix;
  const a = (name) => normalAttrs[`${name}${s}`];

  // CSS selectors. `elementSuffix` targets a descendant element (e.g. the bodycopy
  // content span) so a sub-element can carry its own full style set.
  const selectorSuffix    = resolvedSelector;
  // Element suffix (descendant) before the pseudo-state so sub-element hover reads
  // e.g. `.uc table td:hover`, not `.uc:hover table td`.
  const baseSelector      = `.editor-styles-wrapper .${uniqueClass}${elementSuffix}${selectorSuffix}`;
  const iconSelector      = `${baseSelector} .icon`;
  const svgSelector       = `${baseSelector} .icon svg`;
  // Link wrapper is OUTSIDE the heading, so selector is NOT a child of baseSelector
  const linkSelector      = `.editor-styles-wrapper .${uniqueClass}-link`;
  const linkHoverSelector = `${linkSelector}:hover`;
  const linkFocusSelector = `${linkSelector}:focus`;
  const linkActiveSelector = `${linkSelector}:active`;

  const shouldAppend = (device, property, value, cascade) => {
    if (!hasCssValue(value)) return false;
    if (device !== 'mobile' && Object.prototype.hasOwnProperty.call(cascade, property) && cascade[property] === value) return false;
    cascade[property] = value;
    return true;
  };

  const addStyle = (device, property, value) => {
    if (shouldAppend(device, property, value, styleCascade)) {
      deviceStyles[device].push(`${property}: ${value};`);
    }
  };

  const addIconStyle = (device, property, value) => {
    if (shouldAppend(device, property, value, iconStyleCascade)) {
      iconDeviceStyles[device].push(`${property}: ${value};`);
    }
  };

  const addSvgStyle = (device, property, value) => {
    if (shouldAppend(device, property, value, svgStyleCascade)) {
      svgDeviceStyles[device].push(`${property}: ${value};`);
    }
  };

  const addLinkStyle = (device, property, value) => {
    if (shouldAppend(device, property, value, linkStyleCascade)) {
      linkDeviceStyles[device].push(`${property}: ${value};`);
    }
  };

  const addLinkHoverStyle = (device, property, value) => {
    if (shouldAppend(device, property, value, linkHoverStyleCascade)) {
      linkHoverDeviceStyles[device].push(`${property}: ${value};`);
    }
  };

  const addLinkFocusStyle = (device, property, value) => {
    if (shouldAppend(device, property, value, linkFocusStyleCascade)) {
      linkFocusDeviceStyles[device].push(`${property}: ${value};`);
    }
  };

  const addLinkActiveStyle = (device, property, value) => {
    if (shouldAppend(device, property, value, linkActiveStyleCascade)) {
      linkActiveDeviceStyles[device].push(`${property}: ${value};`);
    }
  };

  const addImgStyle = imgDeviceStyles
    ? (device, property, value) => {
        if (shouldAppend(device, property, value, imgStyleCascade)) {
          imgDeviceStyles[device].push(`${property}: ${value};`);
        }
      }
    : () => {};

  const borderFallbackDevices = (device) => {
    if (device === 'mobile') return ['mobile', 'tablet', 'desktop'];
    if (device === 'tablet') return ['tablet', 'desktop'];
    return ['desktop'];
  };
  const spacingFallbackDevices = borderFallbackDevices;
  const spacingSides = ['top', 'right', 'bottom', 'left'];
  const isZeroSpacingValue = (value) => {
    if (typeof value === 'number') return value === 0;
    if (typeof value !== 'string') return false;
    return /^0(?:\.0+)?(?:px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc)?$/i.test(value.trim());
  };
  const firstSet = (...vals) => {
    const value = vals.find((v) => hasCssValue(v));
    return hasCssValue(value) ? value : '';
  };
  const isDefaultZeroSpacingDevice = (dv = {}) =>
    !dv.isVHActive &&
    !dv.isSidesAllActive &&
    !hasCssValue(dv.all) &&
    !hasCssValue(dv.vertical) &&
    !hasCssValue(dv.horizontal) &&
    spacingSides.every((side) => isZeroSpacingValue(dv?.[side]));
  const spacingSideValueFromDevice = (dv = {}, side) => {
    const { isVHActive, isSidesAllActive } = dv;

    if (isVHActive) {
      return firstSet(dv.all, dv.top, dv.right, dv.bottom, dv.left);
    }

    if (isSidesAllActive) {
      if (side === 'top' || side === 'bottom') {
        return firstSet(dv.vertical, dv.top, dv.bottom);
      }
      return firstSet(dv.horizontal, dv.right, dv.left);
    }

    return hasCssValue(dv[side]) ? dv[side] : '';
  };
  const hasNonZeroSpacingValue = (dv = {}) =>
    spacingSides.some((side) => {
      const value = spacingSideValueFromDevice(dv, side);
      return hasCssValue(value) && !isZeroSpacingValue(value);
    });
  const spacingSideValue = (values = {}, device, side) => {
    const fallbackDevices = spacingFallbackDevices(device);
    for (let index = 0; index < fallbackDevices.length; index += 1) {
      const fallbackDevice = fallbackDevices[index];
      const dv = values?.[fallbackDevice];
      const value = spacingSideValueFromDevice(values?.[fallbackDevice], side);
      if (!hasCssValue(value)) continue;

      const laterDeviceHasRealValue = fallbackDevices
        .slice(index + 1)
        .some((laterDevice) => hasNonZeroSpacingValue(values?.[laterDevice]));

      if (isDefaultZeroSpacingDevice(dv) && laterDeviceHasRealValue) continue;

      return value;
    }
    return '';
  };
  const addResponsiveSpacing = (values = {}, type, add) => {
    Object.keys(devices).forEach((device) => {
      const t = spacingSideValue(values, device, 'top');
      const r = spacingSideValue(values, device, 'right');
      const b = spacingSideValue(values, device, 'bottom');
      const l = spacingSideValue(values, device, 'left');

      if (!hasCssValue(t) && !hasCssValue(r) && !hasCssValue(b) && !hasCssValue(l)) return;

      if (hasCssValue(t) && hasCssValue(r) && hasCssValue(b) && hasCssValue(l)) {
        add(device, type, spacingShorthand(t, r, b, l));
        return;
      }

      if (hasCssValue(t)) add(device, `${type}-top`, t);
      if (hasCssValue(r)) add(device, `${type}-right`, r);
      if (hasCssValue(b)) add(device, `${type}-bottom`, b);
      if (hasCssValue(l)) add(device, `${type}-left`, l);
    });
  };
  const borderSides = ['top', 'right', 'bottom', 'left'];
  const borderFields = ['width', 'style', 'color'];
  const isLinkedBorderDevice = (values, device) => values?.[device]?.isLinkActive !== false;
  const sideHasOwnBorderValue = (border, side) =>
    borderFields.some((field) => hasCssValue(border?.sides?.[side]?.[field]));
  const borderDeviceHasValues = (border) => {
    if (!border) return false;
    if (borderFields.some((field) => hasCssValue(border?.[field]))) return true;
    return borderSides.some((side) => sideHasOwnBorderValue(border, side));
  };
  const effectiveBorderSourceDevice = (values, device) => {
    for (const fallbackDevice of borderFallbackDevices(device)) {
      const border = values?.[fallbackDevice];
      if (!border) continue;
      if (borderDeviceHasValues(border) || (Object.prototype.hasOwnProperty.call(border, 'isLinkActive') && border.isLinkActive === false)) {
        return fallbackDevice;
      }
    }
    return '';
  };
  const matchingSideBorderValue = (border, field) => {
    let matched = '';
    for (const side of borderSides) {
      const value = border?.sides?.[side]?.[field] || '';
      if (!hasCssValue(value)) return '';
      if (!matched) {
        matched = value;
      } else if (matched !== value) {
        return '';
      }
    }
    return matched;
  };
  const linkedBorderValue = (values, device, field) => {
    for (const fallbackDevice of borderFallbackDevices(device)) {
      const border = values?.[fallbackDevice];
      if (!border) continue;
      const value = isLinkedBorderDevice(values, fallbackDevice)
        ? border?.[field] || ''
        : matchingSideBorderValue(border, field);
      if (hasCssValue(value)) return value;
    }
    return '';
  };
  const sideBorderValue = (values, device, side, field) => {
    for (const fallbackDevice of borderFallbackDevices(device)) {
      const border = values?.[fallbackDevice];
      if (!border) continue;
      let value = isLinkedBorderDevice(values, fallbackDevice)
        ? border?.[field] || ''
        : border?.sides?.[side]?.[field] || '';
      if (!hasCssValue(value) && !isLinkedBorderDevice(values, fallbackDevice)) {
        value = border?.[field] || '';
      }
      if (hasCssValue(value)) return value;
    }
    return '';
  };
  const activeBorderSides = (border) => borderSides.filter((side) => sideHasOwnBorderValue(border, side));
  const addResponsiveBorder = (values = {}, add) => {
    Object.keys(devices).forEach((device) => {
      const sourceDevice = effectiveBorderSourceDevice(values, device);
      if (!sourceDevice) return;

      if (isLinkedBorderDevice(values, sourceDevice)) {
        const w = linkedBorderValue(values, device, 'width');
        const sv = linkedBorderValue(values, device, 'style');
        const c = linkedBorderValue(values, device, 'color');
        if (w) add(device, 'border-width', w);
        if (sv) add(device, 'border-style', sv);
        if (c) add(device, 'border-color', c);
        return;
      }

      const activeSides = activeBorderSides(values?.[sourceDevice]);
      borderSides.forEach((side) => {
        if (!activeSides.includes(side)) {
          add(device, `border-${side}-width`, '0');
          return;
        }

        const w = sideBorderValue(values, device, side, 'width');
        const sv = sideBorderValue(values, device, side, 'style');
        const c = sideBorderValue(values, device, side, 'color');
        if (w) add(device, `border-${side}-width`, w);
        if (sv) add(device, `border-${side}-style`, sv);
        if (c) add(device, `border-${side}-color`, c);
      });
    });
  };

  // ── Spacing (padding / margin) ───────────────────────────────────────────
  [
    { type: 'padding', values: a('padding') },
    { type: 'margin',  values: a('margin') },
  ].forEach(({ type, values }) => {
    addResponsiveSpacing(values, type, addStyle);
  });

  // ── Simple per-device attribute groups ──────────────────────────────────
  const simpleGroups = [
    // display
    [{ property: 'display', value: a('display') }],
    // layout
    [
      { property: 'align-items',      value: a('alignItems') },
      { property: 'justify-content',  value: a('justifyContent') },
      { property: 'justify-items',    value: a('justifyItems') },
      { property: 'justify-self',     value: a('justifySelf') },
      { property: 'align-self',       value: a('alignSelf') },
    ],
    // grid
    [
      { property: 'grid-template-columns', value: a('gridTemplateColumns') },
      { property: 'grid-template-rows',    value: a('gridTemplateRows') },
      { property: 'column-gap',            value: a('columnGap') },
      { property: 'row-gap',               value: a('rowGap') },
      { property: 'grid-auto-flow',        value: a('gridAutoFlow') },
      { property: 'grid-column',           value: a('gridColumn') },
      { property: 'grid-row',              value: a('gridRow') },
      { property: 'order',                 value: a('order') },
    ],
    // flex
    [
      { property: 'flex-direction', value: a('flexDirection') },
      { property: 'flex-wrap',      value: a('flexWrap') },
      { property: 'flex-grow',      value: a('flexGrow') },
      { property: 'flex-shrink',    value: a('flexShrink') },
      { property: 'flex-basis',     value: a('flexBasis') },
    ],
    // float
    [
      { property: 'float', value: a('float') },
      { property: 'clear', value: a('clear') },
    ],
    // sizing
    [
      { property: 'width',        value: a('width') },
      { property: 'height',       value: a('height') },
      { property: 'min-width',    value: a('minWidth') },
      { property: 'min-height',   value: a('minHeight') },
      { property: 'max-width',    value: a('maxWidth') },
      { property: 'max-height',   value: a('maxHeight') },
      { property: 'aspect-ratio', value: a('aspectRatio') },
    ],
    // typography
    [
      { property: 'color',           value: a('textColor') },
      { property: 'font-size',       value: a('fontSize') },
      { property: 'font-weight',     value: a('fontWeight') },
      { property: 'font-style',      value: a('fontStyle') },
      { property: 'text-align',      value: a('textAlign') },
      { property: 'text-transform',  value: a('textTransform') },
      { property: 'text-decoration', value: a('textDecoration') },
      { property: 'line-height',     value: a('lineHeight') },
      { property: 'letter-spacing',  value: a('letterSpacing') },
      { property: 'font-family',     value: a('fontFamily') },
      // Table semantics (only emitted when the attribute exists — table family).
      { property: 'vertical-align',  value: a('verticalAlign') },
      { property: 'border-spacing',  value: a('borderSpacing') },
    ],
  ];

  simpleGroups.flat().forEach(({ property, value }) => {
    Object.keys(devices).forEach((device) => {
      addStyle(device, property, value?.[device] || '');
    });
  });

  // ── Background ───────────────────────────────────────────────────────────
  [
    { property: 'background-color',    value: a('backgroundColor') },
    { property: 'background-image',    value: a('backgroundLayers'), transform: bgImageTransform },
    { property: 'background-position', value: a('backgroundLayers'), transform: bgPositionTransform },
    { property: 'background-size',     value: a('backgroundLayers'), transform: bgSizeTransform },
    { property: 'background-repeat',   value: a('backgroundLayers'), transform: bgRepeatTransform },
  ].forEach(({ property, value, transform }) => {
    Object.keys(devices).forEach((device) => {
      const dv = transform ? transform(value?.[device]) : (value?.[device] || '');
      addStyle(device, property, dv);
    });
  });

  // ── Border ───────────────────────────────────────────────────────────────
  addResponsiveBorder(a('border') || {}, addStyle);

  Object.keys(devices).forEach((device) => {
    const br = a('borderRadius')?.[device];
    if (br?.isLinkActive && br.value) addStyle(device, 'border-radius', br.value);
  });

  Object.keys(devices).forEach((device) => {
    const br = a('borderRadius')?.[device] || { corners: {}, isLinkActive: true };
    if (!br.isLinkActive) {
      const tl = br.corners?.['top-left']     || '';
      const tr = br.corners?.['top-right']    || '';
      const brv = br.corners?.['bottom-right'] || '';
      const bl = br.corners?.['bottom-left']  || '';
      if (tl && tr && brv && bl) {
        addStyle(device, 'border-radius', borderRadiusShorthand(tl, tr, brv, bl));
      } else {
        if (tl)  addStyle(device, 'border-top-left-radius',     tl);
        if (tr)  addStyle(device, 'border-top-right-radius',    tr);
        if (brv) addStyle(device, 'border-bottom-right-radius', brv);
        if (bl)  addStyle(device, 'border-bottom-left-radius',  bl);
      }
    }
  });

  // Emit inset (top/right/bottom/left) self-contained per breakpoint. Inset is
  // authored desktop-first (a breakpoint may set only some sides, inheriting the
  // rest from a wider one). As mobile-first CSS, an unset side would otherwise
  // pick up the NARROWER breakpoint's value (e.g. desktop inheriting mobile's
  // `right`), mispositioning absolutely-positioned elements. Resolve every side
  // via the desktop-first fallback chain and emit all four (falling back to
  // `auto`) so each breakpoint is self-contained. addStyle de-dupes the cascade.
  const emitInset = (insetAttrName, addFn) => {
    const insetVal = a(insetAttrName);
    const insetSides = ['top', 'right', 'bottom', 'left'];
    const used = insetVal && Object.keys(devices).some((d) =>
      insetSides.some((s) => hasCssV(insetVal?.[d]?.[s]))
    );
    if (!used) return;
    Object.keys(devices).forEach((device) => {
      insetSides.forEach((side) => {
        let v = '';
        for (const fb of borderFallbackDevices(device)) {
          if (hasCssV(insetVal?.[fb]?.[side])) { v = insetVal[fb][side]; break; }
        }
        addFn(device, side, v !== '' ? v : 'auto');
      });
    });
  };

  // ── Position / Overflow / Z-index ────────────────────────────────────────
  [
    { property: 'position',   value: a('position'),  transform: (v) => v || null },
    { property: 'overflow-x', value: a('overflowX'), transform: (v) => v || null },
    { property: 'overflow-y', value: a('overflowY'), transform: (v) => v || null },
    { property: 'z-index',    value: a('zIndex'),    transform: (v) => v || null },
  ].forEach(({ property, value, transform }) => {
    Object.keys(devices).forEach((device) => {
      const dv = transform(value?.[device]);
      if (dv !== null && dv !== '') addStyle(device, property, dv);
    });
  });
  emitInset('inset', addStyle);

  // ── Effect ───────────────────────────────────────────────────────────────
  [
    { property: 'box-shadow',  transform: boxShadowTransform },
    { property: 'transition',  transform: transitionTransform },
    { property: 'filter',      transform: filterTransform },
    { property: 'text-shadow', transform: textShadowTransform },
    { property: 'transform',   transform: (e) => buildTransformStyle(e?.transform) },
    { property: 'opacity',     transform: (e) => (e?.opacity !== undefined && e?.opacity !== null && e?.opacity !== '' && e.opacity !== '1') ? e.opacity : null },
    { property: 'visibility',  transform: (e) => (e?.visibility && e.visibility !== 'visible') ? e.visibility : null },
  ].forEach(({ property, transform }) => {
    Object.keys(devices).forEach((device) => {
      const dv = transform(a('effect')?.[device]);
      if (dv !== null && dv !== '') addStyle(device, property, dv);
    });
  });

  // ── Outline (interactive states only, e.g. button :focus / :active) ──────────
  // Emits nothing in the normal pass / for blocks without an `outline` attribute.
  Object.keys(devices).forEach((device) => {
    const outline = a('outline')?.[device];
    if (outline) {
      const ow = outline.width || '', os = outline.style || '', oc = outline.color || '';
      if (os === 'none') {
        addStyle(device, 'outline', 'none');
      } else if (ow && os && oc) {
        addStyle(device, 'outline', `${ow} ${os} ${oc}`);
      } else {
        if (ow) addStyle(device, 'outline-width', ow);
        if (os) addStyle(device, 'outline-style', os);
        if (oc) addStyle(device, 'outline-color', oc);
      }
    }
    const offset = a('outlineOffset')?.[device] || '';
    if (offset) addStyle(device, 'outline-offset', offset);
  });

  // ── Img styles (normal mode only) ────────────────────────────────────────
  if (!isState) {
    Object.keys(devices).forEach((device) => {
      const fit  = normalAttrs.imgObjectFit?.[device]      || '';
      const pos  = normalAttrs.imgObjectPosition?.[device] || '';
      const iw   = normalAttrs.imgWidth?.[device]          || '';
      const ih   = normalAttrs.imgHeight?.[device]         || '';
      const imnw = normalAttrs.imgMinWidth?.[device]       || '';
      const imnh = normalAttrs.imgMinHeight?.[device]      || '';
      const imxw = normalAttrs.imgMaxWidth?.[device]       || '';
      const imxh = normalAttrs.imgMaxHeight?.[device]      || '';
      const ibr  = normalAttrs.imgBorderRadius?.[device]   || '';

      if (fit)  addImgStyle(device, 'object-fit',      fit);
      if (pos)  addImgStyle(device, 'object-position', pos);
      if (iw)   addImgStyle(device, 'width',           iw);
      if (ih)   addImgStyle(device, 'height',          ih);
      if (imnw) addImgStyle(device, 'min-width',       imnw);
      if (imnh) addImgStyle(device, 'min-height',      imnh);
      if (imxw) addImgStyle(device, 'max-width',       imxw);
      if (imxh) addImgStyle(device, 'max-height',      imxh);
      if (ibr)  addImgStyle(device, 'border-radius',   ibr);
    });
  }

  // ── Icon color ───────────────────────────────────────────────────────────
  const iconColor = a('iconColor');
  if (hasIcon && iconColor) {
    globalStyles.push(`${iconSelector} { color: ${iconColor}; }`);
    globalStyles.push(`${iconSelector} svg { fill: currentColor; }`);
  }

  // ── Icon size ────────────────────────────────────────────────────────────
  const iconSizeAttr = a('iconSize');
  if (hasIcon && iconSizeAttr && typeof iconSizeAttr === 'object') {
    Object.keys(devices).forEach((device) => {
      const { width, height } = normaliseIconSize(iconSizeAttr[device]);

      // In a state pass: skip if value matches the base (normal) size
      if (isState) {
        const base = normaliseIconSize(normalAttrs.iconSize?.[device]);
        if ((!width || width === base.width) && (!height || height === base.height)) return;
      }

      if (!width && !height) return;
      const cacheKey = `${width}|${height}`;
      if (!shouldAppend(device, '__icon-size__', cacheKey, iconSizeCascade)) return;

      const decls = [];
      if (width)  decls.push(`width:${width}`);
      if (height) decls.push(`height:${height}`);
      const rule = `${iconSelector}{${decls.join(';')};}`;
      stylesCSS.push(devices[device].media ? `${devices[device].media} { ${rule} }` : rule);
    });
  }

  // ── Icon display / alignment ─────────────────────────────────────────────
  [
    { property: 'display',         value: a('iconDisplay') },
    { property: 'align-items',     value: a('iconAlignItems') },
    { property: 'justify-content', value: a('iconJustifyContent') },
  ].forEach(({ property, value }) => {
    Object.keys(devices).forEach((device) => {
      addIconStyle(device, property, value?.[device] || '');
    });
  });

  // ── Icon position / overflow / z-index ───────────────────────────────────
  [
    { property: 'position',   value: a('iconPosition'),  transform: (v) => v || null },
    { property: 'overflow-x', value: a('iconOverflowX'), transform: (v) => v || null },
    { property: 'overflow-y', value: a('iconOverflowY'), transform: (v) => v || null },
    { property: 'z-index',    value: a('iconZIndex'),    transform: (v) => v || null },
  ].forEach(({ property, value, transform }) => {
    Object.keys(devices).forEach((device) => {
      const dv = transform(value?.[device]);
      if (dv !== null && dv !== '') addIconStyle(device, property, dv);
    });
  });
  emitInset('iconInset', addIconStyle);

  // ── Icon effect ──────────────────────────────────────────────────────────
  [
    { property: 'box-shadow',  transform: boxShadowTransform },
    { property: 'transition',  transform: transitionTransform },
    { property: 'filter',      transform: filterTransform },
    { property: 'text-shadow', transform: textShadowTransform },
    { property: 'transform',   transform: (e) => buildTransformStyle(e?.transform) },
    { property: 'opacity',     transform: (e) => (e?.opacity !== undefined && e?.opacity !== null && e?.opacity !== '' && e.opacity !== '1') ? e.opacity : null },
    { property: 'visibility',  transform: (e) => (e?.visibility && e.visibility !== 'visible') ? e.visibility : null },
  ].forEach(({ property, transform }) => {
    Object.keys(devices).forEach((device) => {
      const dv = transform(a('iconEffect')?.[device]);
      if (dv !== null && dv !== '') addIconStyle(device, property, dv);
    });
  });

  // ── Icon spacing (padding / margin) ──────────────────────────────────────
  [
    { type: 'padding', values: a('iconPadding') },
    { type: 'margin',  values: a('iconMargin') },
  ].forEach(({ type, values }) => {
    addResponsiveSpacing(values, type, addIconStyle);
  });

  // ── Icon background ───────────────────────────────────────────────────────
  [
    { property: 'background-color',    value: a('iconBackgroundColor') },
    { property: 'background-image',    value: a('iconBackgroundLayers'), transform: bgImageTransform },
    { property: 'background-position', value: a('iconBackgroundLayers'), transform: bgPositionTransform },
    { property: 'background-size',     value: a('iconBackgroundLayers'), transform: bgSizeTransform },
    { property: 'background-repeat',   value: a('iconBackgroundLayers'), transform: bgRepeatTransform },
  ].forEach(({ property, value, transform }) => {
    Object.keys(devices).forEach((device) => {
      const dv = transform ? transform(value?.[device]) : (value?.[device] || '');
      addIconStyle(device, property, dv);
    });
  });

  // ── Icon border ───────────────────────────────────────────────────────────
  addResponsiveBorder(a('iconBorder') || {}, addIconStyle);

  Object.keys(devices).forEach((device) => {
    const br = a('iconBorderRadius')?.[device];
    if (br?.isLinkActive && br.value) addIconStyle(device, 'border-radius', br.value);
  });

  Object.keys(devices).forEach((device) => {
    const br = a('iconBorderRadius')?.[device] || { corners: {}, isLinkActive: true };
    if (!br.isLinkActive) {
      const tl  = br.corners?.['top-left']     || '';
      const tr  = br.corners?.['top-right']    || '';
      const brv = br.corners?.['bottom-right'] || '';
      const bl  = br.corners?.['bottom-left']  || '';
      if (tl && tr && brv && bl) {
        addIconStyle(device, 'border-radius', borderRadiusShorthand(tl, tr, brv, bl));
      } else {
        if (tl)  addIconStyle(device, 'border-top-left-radius',     tl);
        if (tr)  addIconStyle(device, 'border-top-right-radius',    tr);
        if (brv) addIconStyle(device, 'border-bottom-right-radius', brv);
        if (bl)  addIconStyle(device, 'border-bottom-left-radius',  bl);
      }
    }
  });

  // ── SVG sizing + display ─────────────────────────────────────────────────
  [
    { property: 'display',      value: a('svgDisplay') },
    { property: 'width',        value: a('svgWidth') },
    { property: 'height',       value: a('svgHeight') },
    { property: 'min-width',    value: a('svgMinWidth') },
    { property: 'min-height',   value: a('svgMinHeight') },
    { property: 'max-width',    value: a('svgMaxWidth') },
    { property: 'max-height',   value: a('svgMaxHeight') },
    { property: 'aspect-ratio', value: a('svgAspectRatio') },
  ].forEach(({ property, value }) => {
    Object.keys(devices).forEach((device) => {
      addSvgStyle(device, property, value?.[device] || '');
    });
  });

  // ── SVG effect ────────────────────────────────────────────────────────────
  [
    { property: 'box-shadow',  transform: boxShadowTransform },
    { property: 'transition',  transform: transitionTransform },
    { property: 'filter',      transform: filterTransform },
    { property: 'text-shadow', transform: textShadowTransform },
    { property: 'transform',   transform: (e) => buildTransformStyle(e?.transform) },
    { property: 'opacity',     transform: (e) => (e?.opacity !== undefined && e?.opacity !== null && e?.opacity !== '' && e.opacity !== '1') ? e.opacity : null },
    { property: 'visibility',  transform: (e) => (e?.visibility && e.visibility !== 'visible') ? e.visibility : null },
  ].forEach(({ property, transform }) => {
    Object.keys(devices).forEach((device) => {
      const dv = transform(a('svgEffect')?.[device]);
      if (dv !== null && dv !== '') addSvgStyle(device, property, dv);
    });
  });

  // ── SVG position / overflow / z-index ─────────────────────────────────────
  [
    { property: 'position',   value: a('svgPosition'),  transform: (v) => v || null },
    { property: 'overflow-x', value: a('svgOverflowX'), transform: (v) => v || null },
    { property: 'overflow-y', value: a('svgOverflowY'), transform: (v) => v || null },
    { property: 'z-index',    value: a('svgZIndex'),    transform: (v) => v || null },
  ].forEach(({ property, value, transform }) => {
    Object.keys(devices).forEach((device) => {
      const dv = transform(value?.[device]);
      if (dv !== null && dv !== '') addSvgStyle(device, property, dv);
    });
  });
  emitInset('svgInset', addSvgStyle);

  // ── Link wrapper styles (.uniqueClass-link and interactive states) ───────
  // Generates CSS for the <a> that wraps the heading when linked.
  const linkStates = [
    { prefix: 'link',       add: addLinkStyle },
    { prefix: 'linkHover',  add: addLinkHoverStyle },
    { prefix: 'linkFocus',  add: addLinkFocusStyle },
    { prefix: 'linkActive', add: addLinkActiveStyle },
  ];
  const linkAttrMap = [
    { css: 'display',          suffix: 'Display' },
    { css: 'color',            suffix: 'TextColor' },
    { css: 'font-size',        suffix: 'FontSize' },
    { css: 'font-weight',      suffix: 'FontWeight' },
    { css: 'font-style',       suffix: 'FontStyle' },
    { css: 'text-decoration',  suffix: 'TextDecoration' },
    { css: 'text-transform',   suffix: 'TextTransform' },
    { css: 'line-height',      suffix: 'LineHeight' },
    { css: 'letter-spacing',   suffix: 'LetterSpacing' },
    { css: 'font-family',      suffix: 'FontFamily' },
    { css: 'background-color', suffix: 'BackgroundColor' },
  ];
  linkAttrMap.forEach(({ css, suffix }) => {
    Object.keys(devices).forEach((device) => {
      linkStates.forEach(({ prefix, add }) => {
        const v = normalAttrs[`${prefix}${suffix}`]?.[device] || '';
        if (v) add(device, css, v);
      });
    });
  });

  // Link background layers
  [
    { property: 'background-image',    transform: bgImageTransform },
    { property: 'background-position', transform: bgPositionTransform },
    { property: 'background-size',     transform: bgSizeTransform },
    { property: 'background-repeat',   transform: bgRepeatTransform },
  ].forEach(({ property, transform }) => {
    Object.keys(devices).forEach((device) => {
      linkStates.forEach(({ prefix, add }) => {
        const v = transform(normalAttrs[`${prefix}BackgroundLayers`]?.[device]);
        if (v) add(device, property, v);
      });
    });
  });

  // Link border / borderRadius
  linkStates.forEach(({ prefix, add }) => {
    addResponsiveBorder(normalAttrs[`${prefix}Border`] || {}, add);
  });

  Object.keys(devices).forEach((device) => {
    linkStates.forEach(({ prefix, add }) => {
      const br = normalAttrs[`${prefix}BorderRadius`]?.[device];
      if (!br) return;
      if (br.isLinkActive && br.value) { add(device, 'border-radius', br.value); }
      else if (!br.isLinkActive) {
        const tl = br.corners?.['top-left'] || '', tr = br.corners?.['top-right'] || '';
        const brv = br.corners?.['bottom-right'] || '', bl = br.corners?.['bottom-left'] || '';
        if (tl && tr && brv && bl) add(device, 'border-radius', borderRadiusShorthand(tl, tr, brv, bl));
        else { if (tl) add(device, 'border-top-left-radius', tl); if (tr) add(device, 'border-top-right-radius', tr); if (brv) add(device, 'border-bottom-right-radius', brv); if (bl) add(device, 'border-bottom-left-radius', bl); }
      }
    });
  });

  // Link outline / outline-offset
  Object.keys(devices).forEach((device) => {
    linkStates.forEach(({ prefix, add }) => {
      const outline = normalAttrs[`${prefix}Outline`]?.[device];
      if (outline) {
        const width = outline.width || '';
        const style = outline.style || '';
        const color = outline.color || '';
        if (style === 'none') {
          add(device, 'outline', 'none');
        } else if (width && style && color) {
          add(device, 'outline', `${width} ${style} ${color}`);
        } else {
          if (width) add(device, 'outline-width', width);
          if (style) add(device, 'outline-style', style);
          if (color) add(device, 'outline-color', color);
        }
      }
      const offset = normalAttrs[`${prefix}OutlineOffset`]?.[device] || '';
      if (offset) add(device, 'outline-offset', offset);
    });
  });

  // Link spacing (padding / margin)
  ['padding', 'margin'].forEach((type) => {
    linkStates.forEach(({ prefix, add }) => {
      addResponsiveSpacing(
        normalAttrs[`${prefix}${type.charAt(0).toUpperCase()}${type.slice(1)}`] || {},
        type,
        add
      );
    });
  });

  // Link effect
  [
    { property: 'box-shadow',  transform: boxShadowTransform },
    { property: 'transition',  transform: transitionTransform },
    { property: 'filter',      transform: filterTransform },
    { property: 'text-shadow', transform: textShadowTransform },
    { property: 'transform',   transform: (e) => buildTransformStyle(e?.transform) },
    { property: 'opacity',     transform: (e) => (e?.opacity !== undefined && e?.opacity !== null && e?.opacity !== '' && e.opacity !== '1') ? e.opacity : null },
    { property: 'visibility',  transform: (e) => (e?.visibility && e.visibility !== 'visible') ? e.visibility : null },
  ].forEach(({ property, transform }) => {
    Object.keys(devices).forEach((device) => {
      linkStates.forEach(({ prefix, add }) => {
        const v = transform(normalAttrs[`${prefix}Effect`]?.[device]);
        if (v !== null && v !== '') add(device, property, v);
      });
    });
  });

  // ── Custom CSS (normal mode only) ─────────────────────────────────────────
  if (!isState && normalAttrs.customCss?.trim()) {
    stylesCSS.push(normalAttrs.customCss);
  }

  // ── Emit per-device rules ─────────────────────────────────────────────────
  const emit = (styles, selector) => {
    Object.keys(devices).forEach((device) => {
      if (!styles[device].length) return;
      const css = styles[device].join(' ');
      stylesCSS.push(
        devices[device].media
          ? `${devices[device].media} { ${selector} { ${css} } }`
          : `${selector} { ${css} }`
      );
    });
  };

  emit(deviceStyles, baseSelector);
  if (hasIcon) {
    emit(iconDeviceStyles, iconSelector);
    emit(svgDeviceStyles, svgSelector);
  }
  emit(linkDeviceStyles, linkSelector);
  emit(linkHoverDeviceStyles, linkHoverSelector);
  emit(linkFocusDeviceStyles, linkFocusSelector);
  emit(linkActiveDeviceStyles, linkActiveSelector);
  if (imgDeviceStyles) emit(imgDeviceStyles, `.editor-styles-wrapper .${uniqueClass} img`);

  if (globalStyles.length) stylesCSS.push(...globalStyles);

  return stylesCSS.join('\n');
}
