// ---------------------------------------------------------------------------
// wrapResizableMedia — legacy HTML migration
//
// Bridges HTML created by legacy editors (e.g. TinyMCE) so it renders
// correctly inside the new TextArea editor (TipTap + `resizable-media`).
//
// The `resizable-media` extension expects each `<img>`/`<video>`/`<iframe>`
// to be wrapped in a specific `<span data-type="resizable-media">` structure
// and reads sizes/alignment from CSS — not HTML attributes nor
// `margin: auto` / `float`, which is what legacy editors produced.
// ---------------------------------------------------------------------------
// Tags the `resizable-media` extension handles. `table` is intentionally
// left out — TipTap renders tables with its own extension.
const RESIZABLE_TAGS = ['img', 'video', 'iframe'];
// Block-level tags we walk up to when locating the paragraph that contains
// a floated media (used when trimming trailing empty <p><br></p>s).
const BLOCK_TAGS = new Set([
    'P',
    'DIV',
    'BLOCKQUOTE',
    'LI',
    'TD',
    'TH',
    'H1',
    'H2',
    'H3',
    'H4',
    'H5',
    'H6',
]);
// Safety limit so we never spin trimming through a runaway document.
const MAX_EMPTY_PARAGRAPHS_TO_TRIM = 20;
// Shape of the wrapper the `resizable-media` extension parses.
const RESIZABLE_MEDIA_ATTR = 'data-type';
const RESIZABLE_MEDIA_VALUE = 'resizable-media';
const RESIZABLE_MEDIA_SELECTOR = `[${RESIZABLE_MEDIA_ATTR}="${RESIZABLE_MEDIA_VALUE}"]`;
const OUTER_WRAPPER_BASE_STYLE = 'display: block; max-width: 100%;';
const INNER_WRAPPER_BASE_STYLE = 'display: inline-block;';
// Cheap regex fast-paths — let us skip the full DOM parse when the HTML
// has no media or is already in the shape the editor expects.
const MEDIA_TAG_REGEX = /<(?:img|video|iframe)\b/i;
const MEDIA_TAG_REGEX_GLOBAL = /<(?:img|video|iframe)\b/gi;
const RESIZABLE_WRAPPER_REGEX_GLOBAL = /data-type="resizable-media"/gi;
// `width` / `height` as HTML attributes on a media tag is a legacy marker —
// TipTap exclusively reads sizes from `style.width`.
const LEGACY_SIZE_ATTR_REGEX = /<(?:img|video|iframe)[^>]*\s(?:width|height)\s*=/i;
/**
 * Cheap regex pre-check that decides whether the full DOM-based migration
 * needs to run. Returns `true` when we detect anything that requires
 * rewriting; `false` means the input can be returned untouched.
 *
 * Legacy alignment idioms (`margin-left/right: auto`, `float`) are NOT
 * checked here because a plain regex would match them on any element, not
 * just on media tags. Those are detected during the DOM walk itself
 * (see `detectFloat` / `inferTextAlign`), which only inspects the media
 * element's own style.
 *
 * Order matters — we go from cheapest to most discriminating:
 *   1. No media tags at all → nothing to do.
 *   2. Legacy size attributes on a media tag → must process.
 *   3. Some media is not wrapped yet → must process (e.g. plain `<img>`
 *      sitting outside of `<span data-type="resizable-media">`).
 */
const needsProcessing = (html) => {
    if (!MEDIA_TAG_REGEX.test(html))
        return false;
    if (LEGACY_SIZE_ATTR_REGEX.test(html))
        return true;
    const mediaCount = html.match(MEDIA_TAG_REGEX_GLOBAL)?.length ?? 0;
    const wrapperCount = html.match(RESIZABLE_WRAPPER_REGEX_GLOBAL)?.length ?? 0;
    return wrapperCount < mediaCount;
};
// Numbers like "260" become "260px"; values that already carry a unit
// (`%`, `px`, `em`, …) pass through.
const normalizeSize = (value) => {
    const trimmed = value.trim();
    return /^\d+(?:\.\d+)?$/.test(trimmed) ? `${trimmed}px` : trimmed;
};
/**
 * Promotes HTML `width="X"` / `height="X"` attributes to CSS so TipTap's
 * `findWidthInElement` (which only inspects `style.width`) sees them.
 * Existing CSS values are respected and never overwritten.
 */
const ensureStyleSize = (el) => {
    const style = el.getAttribute('style') ?? '';
    const hasWidthInStyle = /(?:^|;)\s*width\s*:/i.test(style);
    const hasHeightInStyle = /(?:^|;)\s*height\s*:/i.test(style);
    const widthAttr = el.getAttribute('width');
    const heightAttr = el.getAttribute('height');
    const additions = [];
    if (!hasWidthInStyle && widthAttr) {
        additions.push(`width: ${normalizeSize(widthAttr)}`);
    }
    if (!hasHeightInStyle && heightAttr) {
        additions.push(`height: ${normalizeSize(heightAttr)}`);
    }
    if (!additions.length)
        return;
    const cleanStyle = style.trim().replace(/;\s*$/, '');
    el.setAttribute('style', cleanStyle
        ? `${cleanStyle}; ${additions.join('; ')}`
        : additions.join('; '));
};
const detectFloat = (el) => {
    const style = el.getAttribute('style') ?? '';
    const match = style.match(/(?:^|;)\s*float\s*:\s*(left|right)/i);
    return match ? match[1].toLowerCase() : null;
};
/**
 * Picks the `text-align` value the wrapper should expose, translating
 * legacy alignment idioms to what TipTap understands.
 * - `float: left|right` → `left|right`
 * - `margin-left:auto + margin-right:auto` → `center`
 */
const inferTextAlign = (el, float) => {
    if (float)
        return float;
    const style = el.getAttribute('style') ?? '';
    const hasAutoMargins = /margin-left\s*:\s*auto/i.test(style) &&
        /margin-right\s*:\s*auto/i.test(style);
    if (hasAutoMargins)
        return 'center';
    return null;
};
/**
 * When the wrapper already exists but its inner `<span>` says `width: auto`,
 * copies the real size from the media element into the inner span — that's
 * where TipTap actually reads it from.
 */
const syncInnerSpanWidth = (wrapper, mediaWidth) => {
    const inner = wrapper.querySelector(':scope > span');
    if (!inner)
        return;
    const innerStyle = inner.getAttribute('style') ?? '';
    const widthIsAuto = /(?:^|;)\s*width\s*:\s*auto/i.test(innerStyle);
    const widthMissing = !/(?:^|;)\s*width\s*:/i.test(innerStyle);
    if (!widthIsAuto && !widthMissing)
        return;
    const cleanStyle = innerStyle
        .replace(/(?:^|;)\s*width\s*:[^;]*/i, '')
        .trim()
        .replace(/^;\s*/, '')
        .replace(/;\s*$/, '');
    inner.setAttribute('style', cleanStyle
        ? `${cleanStyle}; width: ${mediaWidth};`
        : `${INNER_WRAPPER_BASE_STYLE} width: ${mediaWidth};`);
};
/**
 * Sets `text-align` on an existing wrapper, but only if it wasn't already
 * set to a real value — we never overwrite a previous, intentional choice.
 */
const applyTextAlignToWrapper = (wrapper, textAlign) => {
    const style = wrapper.getAttribute('style') ?? '';
    const currentMatch = style.match(/(?:^|;)\s*text-align\s*:\s*([^;]+)/i);
    const current = currentMatch?.[1]?.trim().toLowerCase();
    if (current && current !== 'inherit')
        return;
    const cleanStyle = style
        .replace(/(?:^|;)\s*text-align\s*:[^;]*/i, '')
        .trim()
        .replace(/^;\s*/, '')
        .replace(/;\s*$/, '');
    wrapper.setAttribute('style', cleanStyle
        ? `${cleanStyle}; text-align: ${textAlign};`
        : `text-align: ${textAlign};`);
};
const getMediaWidth = (el) => {
    const styleMatch = (el.getAttribute('style') ?? '').match(/(?:^|;)\s*width\s*:\s*([^;]+)/i);
    return styleMatch?.[1]?.trim() ?? null;
};
// Walks up from the media element to the nearest block-level ancestor —
// typically the `<p>` that holds the float — so we can clean up the
// trailing empty paragraphs that were used to push content past the float.
const findEnclosingBlock = (el) => {
    let current = el.parentElement;
    while (current && !BLOCK_TAGS.has(current.tagName)) {
        current = current.parentElement;
    }
    return current;
};
// A `<p>` is "empty" for our purposes if it contains only `<br>`s or
// whitespace — the shape legacy editors leave when the user pressed Enter
// repeatedly to make room below a floated element.
const isEmptyParagraph = (el) => {
    if (el.tagName !== 'P')
        return false;
    if ((el.textContent ?? '').trim() !== '')
        return false;
    for (const child of el.children) {
        if (child.tagName !== 'BR')
            return false;
    }
    return true;
};
/**
 * Strips the run of empty paragraphs immediately following a block that
 * holds a floated media. Only called when we actually detected a float —
 * we never touch surrounding empty paragraphs otherwise.
 */
const trimEmptyParagraphsAfter = (block) => {
    let next = block.nextElementSibling;
    let removed = 0;
    while (next &&
        removed < MAX_EMPTY_PARAGRAPHS_TO_TRIM &&
        isEmptyParagraph(next)) {
        const toRemove = next;
        next = next.nextElementSibling;
        toRemove.remove();
        removed++;
    }
};
/**
 * Adapts HTML coming from a legacy editor (TinyMCE-shaped output) so that
 * it renders correctly inside the TextArea editor (TipTap with
 * `resizable-media`).
 *
 * The legacy markup stores sizes as HTML attributes and uses `margin:auto`/
 * `float` for alignment, none of which TipTap reads. This function moves
 * those to CSS, wraps `img`/`video`/`iframe` in the `resizable-media`
 * structure when missing, and trims the empty paragraphs that legacy
 * authors used to push content below a floated element.
 *
 * Idempotent: re-running it on already-wrapped HTML only syncs missing
 * width or text-align on existing wrappers.
 *
 * Nullish inputs (`undefined` / `null` / empty string) pass through
 * unchanged so the caller can keep distinguishing "field never set" from
 * "field explicitly empty".
 *
 * @example
 * ```ts
 * const safe = wrapResizableMedia(legacyHtml);
 * <FormTextArea name="description" defaultValue={safe} />
 * ```
 */
export const wrapResizableMedia = (html) => {
    if (!html)
        return html;
    if (typeof DOMParser === 'undefined')
        return html;
    // Cheap regex bailout — avoids parsing a DOM for HTML that has no media
    // or is already in the shape TipTap expects.
    if (!needsProcessing(html))
        return html;
    const doc = new DOMParser().parseFromString(html, 'text/html');
    RESIZABLE_TAGS.forEach(tag => {
        doc.body.querySelectorAll(tag).forEach(el => {
            const element = el;
            // 1. Read legacy positioning hints from the media element itself.
            const float = detectFloat(element);
            // 2. Normalize the element: move width/height attrs into `style`,
            //    derive what `text-align` should be on the wrapper.
            ensureStyleSize(element);
            const textAlign = inferTextAlign(element, float);
            const mediaWidth = getMediaWidth(element);
            const existingWrapper = element.closest(RESIZABLE_MEDIA_SELECTOR);
            if (existingWrapper) {
                // 3a. Wrapper is already in place — only fix what's missing:
                //     size on the inner span and any alignment that TipTap won't
                //     infer from the element.
                if (mediaWidth)
                    syncInnerSpanWidth(existingWrapper, mediaWidth);
                if (textAlign)
                    applyTextAlignToWrapper(existingWrapper, textAlign);
            }
            else {
                // 3b. Bare media (pure legacy output) — build the wrapper from
                //     scratch around it.
                const outer = doc.createElement('span');
                outer.setAttribute(RESIZABLE_MEDIA_ATTR, RESIZABLE_MEDIA_VALUE);
                outer.setAttribute('style', textAlign
                    ? `${OUTER_WRAPPER_BASE_STYLE} text-align: ${textAlign};`
                    : `${OUTER_WRAPPER_BASE_STYLE} text-align: inherit;`);
                const inner = doc.createElement('span');
                inner.setAttribute('style', `${INNER_WRAPPER_BASE_STYLE} width: ${mediaWidth ?? 'auto'};`);
                element.parentNode?.insertBefore(outer, element);
                outer.appendChild(inner);
                inner.appendChild(element);
            }
            // 4. For floated media specifically, legacy authors usually added a
            //    stack of empty paragraphs after the element so the next block
            //    would visually land below the float. TipTap doesn't float, so
            //    those paragraphs would now show up as real empty lines — strip
            //    them.
            if (float) {
                const block = findEnclosingBlock(element);
                if (block)
                    trimEmptyParagraphsAfter(block);
            }
        });
    });
    return doc.body.innerHTML;
};
