import { BLOG_ANIMATION_TIMING_MS, runAfterBlogDelay } from '@/factories/blogAnimationFactory';
import { upgradeArgonCodeblocks } from '@/factories/argonCodeblockFactory';

interface ArgonPostContentRuntimeState {
  closeFancybox?: () => void;
}

/**
 * @param root Rendered WordPress article body whose Argon controls are injected through `innerHTML`.
 * @returns Cleanup callback that removes delegated handlers and clears transient fullscreen state.
 */
export function bindArgonPostContentEffects(root: HTMLElement) {
  const runtimeState: ArgonPostContentRuntimeState = {};

  upgradeArgonCodeblocks(root);
  normalizeArgonCodeblockLineNumbers(root);

  /**
   * @param event Click bubbling from Argon-rendered controls inside the current post body.
   */
  const handleClick = (event: MouseEvent) => {
    handleArgonPostContentClick(event, root, runtimeState);
  };

  root.addEventListener('click', handleClick);

  return () => {
    root.removeEventListener('click', handleClick);
    runtimeState.closeFancybox?.();
    clearFullscreenCodeblocks(root);
  };
}

/**
 * @param event Delegated click from WordPress/Argon HTML rendered inside `#post_content`.
 * @param root Current post content root; prevents stale controls from affecting another route.
 * @param runtimeState Current route's transient Argon runtime state, such as an open Fancybox shell.
 */
function handleArgonPostContentClick(
  event: MouseEvent,
  root: HTMLElement,
  runtimeState: ArgonPostContentRuntimeState,
) {
  const target = event.target instanceof Element ? event.target : null;
  const fancyboxWrapper = target?.closest<HTMLElement>('.fancybox-wrapper[href]');
  if (fancyboxWrapper && root.contains(fancyboxWrapper)) {
    event.preventDefault();
    runtimeState.closeFancybox?.();
    runtimeState.closeFancybox = openArgonFancybox(fancyboxWrapper, root, () => {
      runtimeState.closeFancybox = undefined;
    });
    return;
  }

  const collapseTitle = target?.closest<HTMLElement>('.collapse-block-title');
  if (collapseTitle && root.contains(collapseTitle)) {
    event.preventDefault();
    toggleCollapseBlock(collapseTitle);
    return;
  }

  const control = target?.closest<HTMLElement>('.hljs-control-btn');
  if (!control || !root.contains(control)) {
    return;
  }

  const codeBlock = control.closest<HTMLElement>('pre.hljs-codeblock, pre.wp-block-code.hljs-codeblock');
  if (!codeBlock || !root.contains(codeBlock)) {
    return;
  }

  event.preventDefault();
  handleCodeblockControl(control, codeBlock);
}

/**
 * @param activeWrapper Image wrapper generated by Argon's WordPress `argon_fancybox` content filter.
 * @param root Current rendered post body that owns the image gallery group.
 * @param onClose Callback used to release the parent runtime state's close handle.
 * @returns Cleanup callback that removes the Fancybox DOM and body scroll compensation classes.
 */
function openArgonFancybox(activeWrapper: HTMLElement, root: HTMLElement, onClose: () => void) {
  const galleryItems = collectFancyboxItems(root);
  const activeIndex = Math.max(
    0,
    galleryItems.findIndex((item) => item.wrapper === activeWrapper),
  );
  let currentIndex = activeIndex;
  const container = createFancyboxContainer(galleryItems.length);
  const stage = container.querySelector<HTMLElement>('.fancybox-stage')!;
  const indexText = container.querySelector<HTMLElement>('[data-fancybox-index]')!;

  /**
   * @param nextIndex Gallery index requested by the navigation controls.
   */
  const renderSlide = (nextIndex: number) => {
    currentIndex = normalizeGalleryIndex(nextIndex, galleryItems.length);
    const item = galleryItems[currentIndex] ?? galleryItems[0];
    if (!item) {
      return;
    }

    indexText.textContent = String(currentIndex + 1);
    stage.replaceChildren(createFancyboxSlide(item));
  };

  /**
   * Removes the current Fancybox shell and restores the body classes touched by Argon.
   */
  const close = () => {
    document.removeEventListener('keydown', handleKeydown);
    container.remove();
    document.body.classList.remove('fancybox-active', 'compensate-for-scrollbar');
    onClose();
  };

  /**
   * @param event Keyboard event used to match Fancybox close and gallery navigation shortcuts.
   */
  const handleKeydown = (event: KeyboardEvent) => {
    if (event.key === 'Escape') {
      event.preventDefault();
      close();
      return;
    }

    if (event.key === 'ArrowLeft') {
      event.preventDefault();
      renderSlide(currentIndex - 1);
      return;
    }

    if (event.key === 'ArrowRight') {
      event.preventDefault();
      renderSlide(currentIndex + 1);
    }
  };

  container.querySelector('.fancybox-bg')?.addEventListener('click', close);
  container.querySelector('.fancybox-button--close')?.addEventListener('click', close);
  container.querySelector('.fancybox-button--arrow_left')?.addEventListener('click', () => renderSlide(currentIndex - 1));
  container.querySelector('.fancybox-button--arrow_right')?.addEventListener('click', () => renderSlide(currentIndex + 1));
  container
    .querySelector('.fancybox-button--fsenter')
    ?.addEventListener('click', () => container.classList.toggle('fancybox-is-fullscreen'));
  container
    .querySelector('.fancybox-button--zoom')
    ?.addEventListener('click', () => container.classList.toggle('fancybox-is-zoomed'));

  document.body.classList.add('fancybox-active', 'compensate-for-scrollbar');
  document.body.append(container);
  document.addEventListener('keydown', handleKeydown);
  renderSlide(currentIndex);

  return close;
}

/**
 * @param root Current WordPress post body containing Argon's `.fancybox-wrapper[href]` image wrappers.
 * @returns Gallery entries in DOM order, including the full image URL and optional alt text.
 */
function collectFancyboxItems(root: HTMLElement) {
  return Array.from(root.querySelectorAll<HTMLElement>('.fancybox-wrapper[href]')).map((wrapper) => ({
    alt: wrapper.querySelector('img')?.getAttribute('alt') ?? '',
    href: wrapper.getAttribute('href') ?? wrapper.querySelector('img')?.getAttribute('src') ?? '',
    wrapper,
  }));
}

/**
 * @param count Total gallery size shown in the live Fancybox infobar.
 * @returns DOM shell whose class names mirror WordPress Argon / Fancybox 3.
 */
function createFancyboxContainer(count: number) {
  const container = document.createElement('div');
  container.className =
    'fancybox-container fancybox-show-toolbar fancybox-show-infobar fancybox-show-nav fancybox-is-open fancybox-can-swipe';
  container.setAttribute('role', 'dialog');
  container.setAttribute('tabindex', '-1');
  container.innerHTML = `
    <div class="fancybox-bg"></div>
    <div class="fancybox-inner">
      <div class="fancybox-infobar"><span data-fancybox-index>1</span>&nbsp;/&nbsp;<span data-fancybox-count>${count}</span></div>
      <div class="fancybox-toolbar">
        ${createFancyboxButtonHtml('zoom', '搜索', 'fa fa-search-plus')}
        ${createFancyboxButtonHtml('fsenter', '全屏', 'fa fa-expand')}
        ${createFancyboxButtonHtml('thumbs', '缩略图', 'fa fa-th')}
        ${createFancyboxButtonHtml('close', '关闭', 'fa fa-times')}
      </div>
      <div class="fancybox-navigation">
        ${createFancyboxButtonHtml('arrow_left', '上一张', 'fa fa-angle-left')}
        ${createFancyboxButtonHtml('arrow_right', '下一张', 'fa fa-angle-right')}
      </div>
      <div class="fancybox-stage"></div>
    </div>
  `;

  return container;
}

/**
 * @param item Gallery entry selected by the current Fancybox index.
 * @returns Current slide node with Fancybox's image/content class structure.
 */
function createFancyboxSlide(item: ReturnType<typeof collectFancyboxItems>[number]) {
  const slide = document.createElement('div');
  slide.className = 'fancybox-slide fancybox-slide--image fancybox-slide--current fancybox-slide--complete';

  const content = document.createElement('div');
  content.className = 'fancybox-content';

  const image = document.createElement('img');
  image.className = 'fancybox-image';
  image.src = item.href;
  image.alt = item.alt;

  content.append(image);
  slide.append(content);

  return slide;
}

/**
 * @param nextIndex Candidate gallery index after keyboard or arrow navigation.
 * @param count Total gallery size. Empty galleries collapse to index zero.
 * @returns Wrapped gallery index in the same cyclic style as Fancybox navigation.
 */
function normalizeGalleryIndex(nextIndex: number, count: number) {
  if (count <= 0) {
    return 0;
  }

  return (nextIndex + count) % count;
}

/**
 * @param modifier Fancybox button modifier appended after `fancybox-button--`.
 * @param label Accessible label for the icon-only toolbar button.
 * @param iconClass Font Awesome class that mirrors Argon's toolbar symbols.
 * @returns Button markup inserted into the Fancybox shell.
 */
function createFancyboxButtonHtml(modifier: string, label: string, iconClass: string) {
  return `<button type="button" class="fancybox-button fancybox-button--${modifier}" aria-label="${label}"><i class="${iconClass}"></i></button>`;
}

/**
 * @param title Collapse title generated by Argon shortcodes; the parent block owns the collapsed state.
 */
function toggleCollapseBlock(title: HTMLElement) {
  const block = title.closest<HTMLElement>('.collapse-block');
  if (!block) {
    return;
  }

  const collapsed = block.classList.toggle('collapsed');
  title.setAttribute('aria-expanded', String(!collapsed));
}

/**
 * @param root Rendered WordPress article body that may contain saved Highlight.js line-number tables.
 *
 * Restores the `data-line-number` attributes normally written by Argon's `highlightjs-line-numbers`
 * runtime so static WordPress snapshots keep visible line gutters and copy-selection metadata.
 */
function normalizeArgonCodeblockLineNumbers(root: HTMLElement) {
  root
    .querySelectorAll<HTMLElement>('pre.hljs-codeblock, pre.wp-block-code.hljs-codeblock')
    .forEach((codeBlock) => {
      const rows = Array.from(codeBlock.querySelectorAll<HTMLTableRowElement>('table.hljs-ln > tbody > tr'));

      rows.forEach((row, index) => {
        const lineNumber = row.querySelector<HTMLElement>('.hljs-ln-numbers');
        const lineNumberInner = lineNumber?.querySelector<HTMLElement>('.hljs-ln-n');
        const lineCode = row.querySelector<HTMLElement>('.hljs-ln-code');
        const lineNumberValue =
          lineNumber?.getAttribute('data-line-number') ||
          lineNumberInner?.getAttribute('data-line-number') ||
          lineCode?.getAttribute('data-line-number') ||
          String(index + 1);

        lineNumber?.setAttribute('data-line-number', lineNumberValue);
        lineNumberInner?.setAttribute('data-line-number', lineNumberValue);
        lineCode?.setAttribute('data-line-number', lineNumberValue);
      });
    });
}

/**
 * @param control Argon codeblock control button clicked by the user.
 * @param codeBlock Highlight.js `<pre>` block that owns the clicked control.
 */
function handleCodeblockControl(control: HTMLElement, codeBlock: HTMLElement) {
  if (control.classList.contains('hljs-control-toggle-linenumber')) {
    codeBlock.classList.toggle('hljs-hide-linenumber');
    return;
  }

  if (control.classList.contains('hljs-control-toggle-break-line')) {
    codeBlock.classList.toggle('hljs-break-line');
    return;
  }

  if (control.classList.contains('hljs-control-fullscreen')) {
    const fullscreen = codeBlock.classList.toggle('hljs-codeblock-fullscreen');
    document.documentElement.classList.toggle('noscroll', fullscreen);
    document.documentElement.classList.toggle('codeblock-fullscreen', fullscreen);
    return;
  }

  if (control.classList.contains('hljs-control-copy')) {
    copyCodeblockText(codeBlock, control);
  }
}

/**
 * @param codeBlock Highlight.js block whose line table should be converted back into plain code text.
 * @param control Copy button used as the small status holder when clipboard access succeeds or fails.
 */
function copyCodeblockText(codeBlock: HTMLElement, control: HTMLElement) {
  const text = readCodeblockText(codeBlock);

  if (!navigator.clipboard?.writeText) {
    control.dataset.ktCopyStatus = 'unsupported';
    showArgonCodeCopyToast(false);
    return;
  }

  navigator.clipboard
    .writeText(text)
    .then(() => {
      control.dataset.ktCopyStatus = 'success';
      showArgonCodeCopyToast(true);
    })
    .catch(() => {
      control.dataset.ktCopyStatus = 'failed';
      showArgonCodeCopyToast(false);
    });
}

/**
 * @param success Whether the clipboard operation succeeded; controls the Argon iziToast text and color.
 */
function showArgonCodeCopyToast(success: boolean) {
  const wrapper = ensureArgonToastWrapper();
  const toast = document.createElement('div');
  toast.className = 'iziToast shadow';
  toast.style.backgroundColor = success ? '#2dce89' : '#f5365c';
  toast.innerHTML = `
    <span class="iziToast-title">${success ? '复制成功' : '复制失败'}</span>
    <span class="iziToast-message">${success ? '代码已复制到剪贴板' : '请手动复制代码'}</span>
  `;
  wrapper.append(toast);
  runAfterBlogDelay(() => {
    toast.remove();
    if (wrapper.childElementCount === 0) {
      wrapper.remove();
    }
  }, BLOG_ANIMATION_TIMING_MS.toastVisible);
}

/**
 * @returns Existing or newly created top-right toast wrapper that mirrors Argon's iziToast placement.
 */
function ensureArgonToastWrapper() {
  const existing = document.querySelector<HTMLElement>('.iziToast-wrapper.iziToast-wrapper-topRight');
  if (existing) {
    return existing;
  }

  const wrapper = document.createElement('div');
  wrapper.className = 'iziToast-wrapper iziToast-wrapper-topRight';
  document.body.append(wrapper);

  return wrapper;
}

/**
 * @param codeBlock Highlight.js block rendered by Argon with either line-number table cells or a plain `<code>`.
 * @returns Plain text copied from code cells, preserving Argon's line order.
 */
function readCodeblockText(codeBlock: HTMLElement) {
  const lineCells = Array.from(codeBlock.querySelectorAll<HTMLElement>('.hljs-ln-code'));
  if (lineCells.length > 0) {
    return lineCells.map((line) => line.innerText).join('\n');
  }

  return codeBlock.querySelector('code')?.textContent ?? '';
}

/**
 * @param root Post content root whose transient fullscreen blocks and global scroll lock should be reset.
 */
function clearFullscreenCodeblocks(root: HTMLElement) {
  root
    .querySelectorAll<HTMLElement>('.hljs-codeblock-fullscreen')
    .forEach((block) => block.classList.remove('hljs-codeblock-fullscreen'));
  document.documentElement.classList.remove('noscroll', 'codeblock-fullscreen');
}
