import { prefersReducedMotion } from '@yggdrasil/shared';
import { fitCentered, originalUrl, type Rect, transformFor } from './geometry';
import './style.css';

interface LightboxState {
  overlay: HTMLDivElement;
  img: HTMLImageElement;
  caption: HTMLElement;
  counter: HTMLDivElement;
  prevBtn: HTMLButtonElement | null;
  nextBtn: HTMLButtonElement | null;
  originNode: HTMLElement;
  gallery: HTMLElement[];
  index: number | null;
  isSingle: boolean;
  openScrollY: number;
  origSrc: string;
  altText: string;
  closing: boolean;
  reduced: boolean;
  scrollHandler: ((this: Window, ev: Event) => void) | null;
  keyHandler: ((this: Document, ev: KeyboardEvent) => void) | null;
  target?: Rect;
  baseW?: number;
  baseH?: number;
}

declare global {
  interface Window {
    __initLightbox: (selectors: string | string[]) => void;
    __lightboxSelectors?: string[];
  }
}

// ============ 工具函数 ============

// prefersReducedMotion 由 @yggdrasil/shared 提供（lightbox.test.ts 会 mock matchMedia）。

// 读取元素当前在视口里的 rect（用于飞行起点/终点）。
// 统一映射成 {x,y,w,h}：getBoundingClientRect 返回的 DOMRect 用
// left/top/width/height，而 fitCentered/transformFor 用 x/y/w/h，
// 这里转成一致格式，避免 .w 读到 undefined。
function rectOf(el: Element): Rect {
  const r = el.getBoundingClientRect();
  return {
    x: r.left,
    y: r.top,
    w: r.width,
    h: r.height,
  };
}

// 注意：fitCentered / transformFor / originalUrl 已抽到 ./geometry.ts。

// ============ 懒加载 ============

// 为单个 .blur-img 容器初始化高清图懒加载。
// IO 进入视口后把 data-src 写入 src，加载完成加 is-loaded 触发 CSS 淡入。
function initLazyLoad(container: Element): void {
  const raw = container.querySelector('.blur-img-full');
  if (!(raw instanceof HTMLImageElement)) return;
  const fullImg: HTMLImageElement = raw;
  if (container.getAttribute('data-blur-init')) return;
  container.setAttribute('data-blur-init', 'true');

  const fullSrc = fullImg.getAttribute('data-src');
  if (!fullSrc) return;

  const onFullLoaded = (): void => {
    // 给容器加 is-loaded，CSS 据此显式隐藏 placeholder。
    // 直接把 full 层 opacity 设为 1（清掉 transition），不依赖 CSS 的 opacity
    // 过渡：合成层重绘时机不稳定，可能导致 full 层卡在 opacity:0，直到一次
    // 强制重排才更新。
    container.classList.add('is-loaded');
    fullImg.style.transition = 'none';
    fullImg.style.opacity = '1';
  };
  fullImg.addEventListener('load', onFullLoaded);
  // 缓存兜底：若设 src 时图片已在缓存（load 几乎立即触发，可能早于监听注册），
  // 用 complete 补一次。注意无 src 的 img complete 也为 true，故先判 src。
  if (fullImg.getAttribute('src') && fullImg.complete) {
    onFullLoaded();
  }

  if ('IntersectionObserver' in window) {
    const io = new IntersectionObserver(
      (entries: IntersectionObserverEntry[]): void => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            fullImg.src = fullSrc;
            io.unobserve(container);
          }
        }
      },
      { rootMargin: '200px' },
    );
    io.observe(container);
  } else {
    fullImg.src = fullSrc;
  }
}

// ============ 图像收集 ============

// 收集所有 selectors命中的 .blur-img 节点。
// gallery: 正文图（组成图集）；singles: 带 lightbox-single class 的单张图（如封面）。
function collectImages(roots: Element[]): { gallery: HTMLElement[]; singles: HTMLElement[] } {
  const gallery: HTMLElement[] = [];
  const singles: HTMLElement[] = [];
  for (const root of roots) {
    const nodes = root.querySelectorAll('.blur-img');
    for (const n of nodes) {
      if (!(n instanceof HTMLElement)) continue;
      if (n.classList.contains('lightbox-single')) {
        singles.push(n);
      } else {
        gallery.push(n);
      }
    }
  }
  return { gallery, singles };
}

// ============ 灯箱状态与开/关 ============

// 当前灯箱状态（同时只允许一个灯箱）。
let state: LightboxState | null = null;

function openLightbox(originNode: HTMLElement, gallery: HTMLElement[], index: number | null): void {
  if (state) closeLightbox(true);

  const fullImgEl = originNode.querySelector('.blur-img-full');
  if (!(fullImgEl instanceof HTMLImageElement)) return;
  const dataSrc = fullImgEl.getAttribute('data-src') || '';
  const origSrc = originalUrl(dataSrc);
  const altText = fullImgEl.getAttribute('alt') || '';
  const isSingle = originNode.classList.contains('lightbox-single') || gallery.length === 0;

  const vw = window.innerWidth;
  const vh = window.innerHeight;

  // 建 DOM
  const overlay = document.createElement('div');
  overlay.className = 'lightbox-overlay';
  overlay.setAttribute('role', 'dialog');
  overlay.setAttribute('aria-modal', 'true');
  overlay.setAttribute('aria-label', '图片预览');
  overlay.setAttribute('tabindex', '-1');
  // 创建后立即设 opacity 0：append 到 DOM 时是透明的，避免在图片加载期间
  // （start() 执行前）显示全黑背景造成闪烁。start() 的渐变会把 opacity 升到 1。
  overlay.style.opacity = '0';

  const img = document.createElement('img');
  img.className = 'lightbox-img';
  img.setAttribute('alt', altText);
  // 加载前先占 0 尺寸，避免原图（可能数千 px）在加载期间撑大文档
  // 可滚动区、触发非预期的 scroll 事件。start() 拿到 natural 尺寸后再设真实值。
  img.style.width = '0px';
  img.style.height = '0px';

  const caption = document.createElement('figcaption');
  caption.className = 'lightbox-caption';
  caption.textContent = altText;
  if (!altText) caption.style.display = 'none';

  const counter = document.createElement('div');
  counter.className = 'lightbox-counter';
  if (isSingle || gallery.length === 0) {
    counter.style.display = 'none';
  } else {
    counter.textContent = `${(index ?? 0) + 1} / ${gallery.length}`;
  }

  // 图集模式（>1 张）才加左右导航箭头；单张不显示。
  let prevBtn: HTMLButtonElement | null = null;
  let nextBtn: HTMLButtonElement | null = null;
  if (!isSingle && gallery.length > 1) {
    prevBtn = document.createElement('button');
    prevBtn.className = 'lightbox-nav lightbox-prev';
    prevBtn.setAttribute('type', 'button');
    prevBtn.setAttribute('aria-label', '上一张');
    prevBtn.textContent = '\u2039';

    nextBtn = document.createElement('button');
    nextBtn.className = 'lightbox-nav lightbox-next';
    nextBtn.setAttribute('type', 'button');
    nextBtn.setAttribute('aria-label', '下一张');
    nextBtn.textContent = '\u203a';
  }

  overlay.appendChild(img);
  overlay.appendChild(caption);
  overlay.appendChild(counter);
  if (prevBtn) overlay.appendChild(prevBtn);
  if (nextBtn) overlay.appendChild(nextBtn);
  document.body.appendChild(overlay);

  state = {
    overlay,
    img,
    caption,
    counter,
    prevBtn,
    nextBtn,
    originNode,
    gallery,
    index,
    isSingle,
    openScrollY: window.scrollY,
    origSrc,
    altText,
    closing: false,
    reduced: prefersReducedMotion(),
    scrollHandler: null,
    keyHandler: null,
  };

  // 焦点移入灯箱
  overlay.focus();
  // 立即绑定交互（不等图片加载）：加载期间 Esc/滚动/点背景也须可关闭。
  bindInteractions();

  // 图片加载后再做动画（naturalW/H 要等加载）
  const start = (): void => {
    if (!state) return; // 加载前可能已被关闭
    const naturalW = img.naturalWidth || img.clientWidth || 1;
    const naturalH = img.naturalHeight || img.clientHeight || 1;
    const originRect = rectOf(originNode);

    // 基准 = originRect（文章里图片的实际尺寸）。
    // img 的布局尺寸固定为 originRect，transform 的 scale 相对它缩放：
    // 首帧（文章位置）scale=1，居中态 scale=target.w/originRect.w。
    // 这样无论 target 比 originRect 大或小，动画都是「从文章图原样连续缩放」，
    // 视觉上是原地展开，不会像「从外面飞来」。灯箱图尺寸恒为视口最大（fitCentered）。
    const target = fitCentered(naturalW, naturalH, vw, vh);
    const baseW = originRect.w;
    const baseH = originRect.h;
    // 存基准与目标，供关闭/滚动关闭复用同一对（保证 scale 连续）。
    state.target = target;
    state.baseW = baseW;
    state.baseH = baseH;
    img.style.width = `${baseW}px`;
    img.style.height = `${baseH}px`;

    // reduced-motion：直接淡入居中
    if (state.reduced) {
      img.style.opacity = '0';
      img.style.transform = transformFor(target, baseW, baseH);
      img.style.left = '0';
      img.style.top = '0';
      overlay.style.opacity = '0';
      // 下一帧淡入
      requestAnimationFrame((): void => {
        if (!state) return;
        overlay.style.transition = 'opacity 200ms ease-out';
        img.style.transition = 'opacity 200ms ease-out';
        overlay.style.opacity = '1';
        img.style.opacity = '1';
      });
      return;
    }

    // 首帧：文章位置 + 原尺寸（scale=1），透明，且关闭 transition
    img.style.transition = 'none';
    img.style.left = '0';
    img.style.top = '0';
    img.style.transform = transformFor(originRect, baseW, baseH);
    img.style.opacity = '0';
    overlay.style.opacity = '0';
    // 强制 reflow，确保首帧的 transform 已提交到渲染层。
    // 否则单层 rAF 里浏览器可能合并首帧与目标帧，动画从错误位置起跳。
    void img.offsetHeight;

    // double-rAF：第一帧绘制首帧（无动画），第二帧才启动 transition 到居中。
    requestAnimationFrame((): void => {
      if (!state) return;
      requestAnimationFrame((): void => {
        if (!state) return;
        img.style.transition = 'transform 250ms ease-out, opacity 250ms ease-out';
        overlay.style.transition = 'opacity 250ms ease-out';
        img.style.transform = transformFor(target, baseW, baseH);
        img.style.opacity = '1';
        overlay.style.opacity = '1';
      });
    });
  };

  if (img.complete && img.naturalWidth) {
    start();
  } else {
    img.addEventListener('load', start, { once: true });
  }
  img.src = origSrc;
}

function closeLightbox(immediate: boolean): void {
  if (!state || state.closing) return;
  state.closing = true;
  cleanupInteractions();

  const s = state;

  // 基准 = originRect 尺寸（与打开时一致），scale 相对它缩放。
  const baseW = s.baseW || (s.target ? s.target.w : 1);
  const baseH = s.baseH || (s.target ? s.target.h : 1);
  const originRect = rectOf(s.originNode); // 实时读，处理期间滚动过的情况

  if (s.reduced || immediate) {
    removeOverlay();
    return;
  }

  // 飞回 originRect：scale 从 1 缩到 originRect.w/baseW
  s.img.style.transition = 'transform 250ms ease-out, opacity 250ms ease-out';
  s.overlay.style.transition = 'opacity 250ms ease-out';
  s.img.style.transform = transformFor(originRect, baseW, baseH);
  s.img.style.opacity = '0';
  s.overlay.style.opacity = '0';

  const done = (): void => {
    removeOverlay();
  };
  // 250ms 兜底，避免 transitionend 不触发
  const timer = setTimeout(done, 280);
  s.img.addEventListener(
    'transitionend',
    (): void => {
      clearTimeout(timer);
      done();
    },
    { once: true },
  );
}

function removeOverlay(): void {
  if (!state) return;
  const prev = state.originNode;
  if (state.overlay?.parentNode) {
    state.overlay.parentNode.removeChild(state.overlay);
  }
  state = null;
  // 焦点归还：.blur-img 是 span 不可聚焦，让其内部 full img 获得焦点。
  // 用 preventScroll 抑制 focus() 默认的 scrollIntoView 行为——否则关闭灯箱后
  // 页面会自动滚动把原图完整纳入视口（用户只点了一半露出的图时尤其明显）。
  if (prev) {
    const f = prev.querySelector('.blur-img-full');
    if (f instanceof HTMLImageElement) {
      f.setAttribute('tabindex', '-1');
      f.focus({ preventScroll: true });
    }
  }
}

// ============ 图集切换 ============

// 图集切换：淡入淡出，不飞行。newIndex 循环（首尾衔接）。
function gotoIndex(rawIndex: number): void {
  if (!state || state.isSingle) return;
  const s = state;
  if (!s.gallery || s.gallery.length === 0) return;
  let newIndex = rawIndex;
  if (newIndex < 0) newIndex = s.gallery.length - 1;
  if (newIndex >= s.gallery.length) newIndex = 0;
  if (newIndex === s.index) return;

  const newNode = s.gallery[newIndex];
  const fullImgEl = newNode.querySelector('.blur-img-full');
  if (!(fullImgEl instanceof HTMLImageElement)) return;
  const origSrc = originalUrl(fullImgEl.getAttribute('data-src') || '');
  const altText = fullImgEl.getAttribute('alt') || '';

  // 淡出当前图
  s.img.style.transition = 'opacity 150ms ease-out';
  s.img.style.opacity = '0';

  // 150ms 后换图淡入
  const swap = (): void => {
    if (!state) return; // 切换中可能已关闭
    // 换图后必须按新图真实尺寸重算几何，否则新图沿用第一张的
    // target/scale，宽高比不同的图会被压扁/拉伸。
    // 布局盒直接设为 target 尺寸（宽高比 = 新图），transform 归位 scale(1,1)；
    // baseW/H 同步为 target 尺寸，关闭/滚动关闭的飞回动画以它为 scale 基准。
    const applyGeometry = (): void => {
      if (!state) return;
      const naturalW = s.img.naturalWidth || 1;
      const naturalH = s.img.naturalHeight || 1;
      const target = fitCentered(naturalW, naturalH, window.innerWidth, window.innerHeight);
      s.target = target;
      s.baseW = target.w;
      s.baseH = target.h;
      // 几何跳变不播动画（transition 只作用于随后的 opacity 淡入）。
      s.img.style.transition = 'none';
      s.img.style.width = `${target.w}px`;
      s.img.style.height = `${target.h}px`;
      s.img.style.transform = transformFor(target, target.w, target.h);
    };
    const fade = (): void => {
      if (!state) return;
      s.img.style.transition = 'opacity 150ms ease-out';
      s.img.style.opacity = '1';
    };
    const onReady = (): void => {
      applyGeometry();
      fade();
    };
    // 先换 src 再判 complete：换之前判的是旧图（必命中），会按旧图尺寸算几何。
    s.img.src = origSrc;
    if (s.img.complete && s.img.naturalWidth) {
      onReady(); // 缓存命中，新图尺寸同步可用
    } else {
      s.img.addEventListener('load', onReady, { once: true });
    }
    s.caption.textContent = altText;
    s.caption.style.display = altText ? '' : 'none';
    s.counter.textContent = `${newIndex + 1} / ${s.gallery.length}`;
    // 更新 originNode 为新图，使后续关闭/滚动关闭飞回新图位置
    s.originNode = newNode;
    s.index = newIndex;
    s.openScrollY = window.scrollY; // 重置滚动关闭基线
  };
  setTimeout(swap, 150);
}

// ============ 交互绑定 ============

function bindInteractions(): void {
  const s = state;
  if (!s) return;

  // 点背景关闭（点图片本身不关，因箭头在图上、避免误关）
  s.overlay.addEventListener('click', (ev: MouseEvent): void => {
    if (state && ev.target === state.overlay) closeLightbox(false);
  });

  // 滚动驱动关闭：任何 scroll 都触发，用 scrollY 偏移算进度。
  // 关键：逐帧读 originNode 实时 rect，文章滚多少图就回多少。
  s.scrollHandler = (): void => {
    if (!state) return;
    const st = state;
    if (st.closing) return;
    const dy = Math.abs(window.scrollY - st.openScrollY);
    if (st.reduced) {
      // reduced-motion：立即关
      closeLightbox(true);
      return;
    }
    const target = st.target;
    const baseW = st.baseW || (target ? target.w : 1);
    const baseH = st.baseH || (target ? target.h : 1);
    const originRect = rectOf(st.originNode);
    if (!target) return; // 无 target 时不插值（忠实原 JS 的兜底语义）
    // 在 originRect 与居中 target 之间按 progress 线性插值
    const progress = Math.min(dy / 120, 1);
    const cur: Rect = {
      x: target.x + (originRect.x - target.x) * progress,
      y: target.y + (originRect.y - target.y) * progress,
      w: target.w + (originRect.w - target.w) * progress,
      h: target.h + (originRect.h - target.h) * progress,
    };
    st.img.style.transition = 'none';
    st.img.style.transform = transformFor(cur, baseW, baseH);
    st.img.style.opacity = String(1 - progress);
    st.overlay.style.opacity = String(1 - progress);
    if (progress >= 1) {
      // 已飞回原位：文章停在当前滚动位置，移除灯箱
      st.closing = true;
      cleanupInteractions();
      removeOverlay();
    }
  };
  window.addEventListener('scroll', s.scrollHandler, { passive: true });

  // 键盘：Esc 关；图集模式 ←→ 切换
  s.keyHandler = (ev: KeyboardEvent): void => {
    if (!state) return;
    if (ev.key === 'Escape') {
      closeLightbox(false);
    } else if (!state.isSingle && state.gallery.length > 1) {
      if (ev.key === 'ArrowLeft') {
        ev.preventDefault();
        gotoIndex((state.index ?? 0) - 1);
      } else if (ev.key === 'ArrowRight') {
        ev.preventDefault();
        gotoIndex((state.index ?? 0) + 1);
      }
    }
  };
  document.addEventListener('keydown', s.keyHandler);

  // 图集导航箭头点击（stopPropagation 防止冒泡到 overlay 触发关闭）
  s.prevBtn?.addEventListener('click', (ev: MouseEvent): void => {
    ev.stopPropagation();
    if (state) gotoIndex((state.index ?? 0) - 1);
  });
  s.nextBtn?.addEventListener('click', (ev: MouseEvent): void => {
    ev.stopPropagation();
    if (state) gotoIndex((state.index ?? 0) + 1);
  });
}

function cleanupInteractions(): void {
  if (!state) return;
  if (state.scrollHandler) {
    window.removeEventListener('scroll', state.scrollHandler);
    state.scrollHandler = null;
  }
  if (state.keyHandler) {
    document.removeEventListener('keydown', state.keyHandler);
    state.keyHandler = null;
  }
}

// ============ 初始化入口 ============

window.__initLightbox = (selectors: string | string[]): void => {
  // selectors 可以是字符串、字符串数组
  const sels = Array.isArray(selectors) ? selectors : [selectors];
  const roots: Element[] = [];
  for (const sel of sels) {
    const found = document.querySelectorAll(sel);
    for (const el of found) roots.push(el);
  }

  // 先对所有图片做懒加载（图集与单张都做）
  const collected = collectImages(roots);
  for (const node of collected.gallery.concat(collected.singles)) {
    initLazyLoad(node);
  }

  // 幂等守卫：SPA 页面（如 /admin/assets）数据刷新后会重复调用 __initLightbox，
  // 而 Dioxus keyed diff 可能复用同一批 DOM 节点——无守卫会叠加 click 监听，
  // 点一次连续触发多次 openLightbox（飞行动画重启/闪烁）。
  // initLazyLoad 已有 data-blur-init 守卫，这里给 click 绑定补同款。
  const bindClick = (node: HTMLElement, handler: (e: MouseEvent) => void): void => {
    if (node.getAttribute('data-lb-bound')) return;
    node.setAttribute('data-lb-bound', 'true');
    node.addEventListener('click', handler);
  };

  // 正文图：带 index。for..of + const 天然捕获每次迭代的 idx，
  // 无需旧 IIFE 包装（旧 var 循环闭包必须立即执行函数固定变量）。
  const gallery = collected.gallery;
  gallery.forEach((node, idx) => {
    bindClick(node, (e: MouseEvent) => {
      e.preventDefault();
      openLightbox(node, gallery, idx);
    });
  });
  // 单张图（封面）：index = null，gallery 传空数组表示单张
  for (const node of collected.singles) {
    bindClick(node, (e: MouseEvent) => {
      e.preventDefault();
      openLightbox(node, [], null);
    });
  }
};

// ============ 自启动 ============
// 方案 iii：双保险契约，无需轮询。
// 1) Rust 内联 eval 先跑（常态）：设 __lightboxSelectors，此时 __initLightbox 可能未定义 → 只设配置；
//    lightbox.js 后加载完 → 读到配置 → 这里自启动。
// 2) lightbox.js 先加载完：__initLightbox 就绪但无配置 → 不自启动；
//    Rust eval 后跑 → 设配置 + 兜底 if(__initLightbox) 显式调用 → 初始化。
if (Array.isArray(window.__lightboxSelectors)) {
  window.__initLightbox(window.__lightboxSelectors);
}
