/* global window */
(function (window) {
  'use strict';

  // IDs whose serialized block markup is independent of that ID. Blocks whose
  // IDs are interpolated into saved HTML are deliberately absent from this map.
  const providerAttributes = {
    'generateblocks/query': { attribute: 'uniqueId', provider: 'generateblocks' },
    'generateblocks/query-page-numbers': { attribute: 'uniqueId', provider: 'generateblocks' },
    'generateblocks/text': { attribute: 'uniqueId', provider: 'generateblocks' },
    'kadence/progress-bar': { attribute: 'uniqueID', provider: 'kadence' },
  };
  const allowedNamespaces = new Set(['core', 'generateblocks', 'kadence', 'uagb']);
  const deniedBlocks = new Set(['core/html', 'core/freeform', 'core/shortcode']);

  function editor(frame) {
    const view = frame.contentWindow;
    const wp = view?.wp;
    const select = wp?.data?.select?.('core/block-editor');
    const dispatch = wp?.data?.dispatch?.('core/block-editor');
    if (!view || !select?.getBlocks || !dispatch?.selectBlock || !wp?.blocks?.getBlockType) {
      throw new Error('The Gutenberg block-editor API is unavailable.');
    }
    return { view, wp, select, dispatch };
  }

  function validIdentity(value) {
    return typeof value === 'string' && /^[A-Za-z0-9_-]{6,64}$/.test(value);
  }

  function blockPath(path) {
    return path.join('.');
  }

  function nextFrame(view) {
    const requestFrame = view.requestAnimationFrame || window.requestAnimationFrame;
    return new Promise((resolve) => requestFrame(() => requestFrame(resolve)));
  }

  function treeShape(blocks) {
    return blocks.map((block) => ({ name: block.name, innerBlocks: treeShape(block.innerBlocks || []) }));
  }

  function collect(blocks, wp, entries, path = []) {
    blocks.forEach((block, index) => {
      const currentPath = [...path, index];
      const namespace = String(block.name || '').split('/')[0];
      const type = wp.blocks.getBlockType(block.name);
      if (!allowedNamespaces.has(namespace) || deniedBlocks.has(block.name) || !type) {
        throw new Error(`Unsupported block namespace or registration: ${block.name || 'unknown'}.`);
      }
      const rule = providerAttributes[block.name];
      if (rule) {
        const schema = type.attributes?.[rule.attribute];
        const hasValue = Object.prototype.hasOwnProperty.call(block.attributes || {}, rule.attribute);
        if (schema?.type !== 'string' || (hasValue && !validIdentity(block.attributes[rule.attribute]))) {
          throw new Error(`The registered ${block.name} provider block exposed an invalid identity.`);
        }
        entries.set(blockPath(currentPath), {
          path: currentPath,
          block_name: block.name,
          attribute: rule.attribute,
          value: hasValue ? block.attributes[rule.attribute] : null,
          clientId: block.clientId,
        });
      }
      collect(block.innerBlocks || [], wp, entries, currentPath);
    });
  }

  async function capture(frame, command) {
    const runtime = window.wpagentlyLiveEditorRuntime;
    if (!runtime?.editorState) throw new Error('The WPAgently live-editor runtime is unavailable.');
    const beforeState = await runtime.editorState(frame);
    if (beforeState.post_id !== command.expected_post_id || beforeState.state_hash !== command.expected_state_hash) {
      throw new Error('The editor document changed before provider finalization.');
    }
    const { view, wp, select, dispatch } = editor(frame);
    const beforeBlocks = select.getBlocks();
    const beforeShape = JSON.stringify(treeShape(beforeBlocks));
    const beforeCandidates = new Map();
    collect(beforeBlocks, wp, beforeCandidates);
    if (beforeCandidates.size > 100) throw new Error('Too many provider blocks are present for finalization.');
    const selectedClientId = select.getSelectedBlockClientId?.() || null;
    try {
      for (const entry of beforeCandidates.values()) {
        if (entry.value === null) {
          dispatch.selectBlock(entry.clientId);
          await nextFrame(view);
        }
      }
    } finally {
      dispatch.selectBlock(selectedClientId);
      await nextFrame(view);
    }
    const mountedState = await runtime.editorState(frame);
    const afterBlocks = select.getBlocks();
    if (mountedState.post_id !== beforeState.post_id || mountedState.block_count !== beforeState.block_count
      || beforeShape !== JSON.stringify(treeShape(afterBlocks))) {
      throw new Error('Provider mounting changed the editor document structure.');
    }
    const afterCandidates = new Map();
    collect(afterBlocks, wp, afterCandidates);
    const newlyAssigned = [];
    for (const [key, before] of beforeCandidates) {
      const after = afterCandidates.get(key);
      if (!after || before.value !== null || after.value === null) continue;
      newlyAssigned.push({
        path: after.path,
        block_name: after.block_name,
        attribute: after.attribute,
        value: after.value,
      });
    }
    return {
      post_id: beforeState.post_id,
      before_state_hash: beforeState.state_hash,
      mounted_state_hash: mountedState.state_hash,
      candidates: newlyAssigned,
    };
  }

  window.wpagentlyLiveEditorProviderFinalizer = { capture };
})(window);
