import {
  ArrowLeft,
  CheckCircle2,
  Clock3,
  RotateCcw,
  Sparkles,
  XCircle,
} from 'lucide-react';
import { type PointerEvent, useEffect, useMemo, useRef, useState } from 'react';

import type {
  Match3DClickItemRequest,
  Match3DClickItemResult,
  Match3DItemSnapshot,
  Match3DRunSnapshot,
  Match3DTraySlot,
} from '../../../packages/shared/src/contracts/match3dRuntime';
import { MATCH3D_VISUAL_SEEDS } from '../../services/match3d-runtime';

type Match3DRuntimeShellProps = {
  run: Match3DRunSnapshot | null;
  isBusy?: boolean;
  error?: string | null;
  onBack: () => void;
  onRestart: () => void;
  onOptimisticRunChange: (run: Match3DRunSnapshot) => void;
  onClickItem: (
    payload: Match3DClickItemRequest,
  ) => Promise<Match3DClickItemResult>;
  onTimeExpired?: () => void;
};

type PendingClick = {
  clientEventId: string;
  itemInstanceId: string;
  previousRun: Match3DRunSnapshot;
};

type Match3DFeedbackEvent = {
  id: string;
  kind: 'cleared' | 'rejected';
  itemIds: string[];
};

function formatTimer(value: number) {
  const totalSeconds = Math.max(0, Math.ceil(value / 1000));
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}

function formatElapsed(startedAtMs: number, remainingMs: number, durationLimitMs: number) {
  const elapsedMs = Math.max(0, durationLimitMs - remainingMs);
  const totalSeconds = Math.floor(elapsedMs / 1000);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}

function resolveVisualSeed(visualKey: string) {
  return (
    MATCH3D_VISUAL_SEEDS.find((seed) => seed.visualKey === visualKey) ??
    MATCH3D_VISUAL_SEEDS[0]!
  );
}

function buildClientEventId(itemInstanceId: string) {
  return `match3d-click-${itemInstanceId}-${Date.now()}-${Math.round(
    Math.random() * 1_000_000,
  )}`;
}

function isPointInsideCircle(
  pointX: number,
  pointY: number,
  item: Match3DItemSnapshot,
) {
  return Math.hypot(pointX - item.x, pointY - item.y) <= item.radius;
}

function findHitItem(
  run: Match3DRunSnapshot,
  pointX: number,
  pointY: number,
) {
  return run.items
    .filter(
      (item) =>
        item.state === 'InBoard' &&
        item.clickable &&
        isPointInsideCircle(pointX, pointY, item),
    )
    .sort((left, right) => right.layer - left.layer)[0];
}

function buildOptimisticRun(
  run: Match3DRunSnapshot,
  item: Match3DItemSnapshot,
) {
  const nextSlot = run.traySlots.find((slot) => !slot.itemInstanceId);
  if (!nextSlot) {
    return run;
  }
  return {
    ...run,
    items: run.items.map((entry) =>
      entry.itemInstanceId === item.itemInstanceId
        ? {
            ...entry,
            state: 'Flying' as const,
            clickable: false,
          }
        : entry,
    ),
    traySlots: run.traySlots.map((slot) =>
      slot.slotIndex === nextSlot.slotIndex
        ? {
            slotIndex: slot.slotIndex,
            itemInstanceId: item.itemInstanceId,
            itemTypeId: item.itemTypeId,
            visualKey: item.visualKey,
          }
        : slot,
    ),
  };
}

function Match3DToken({
  item,
  disabled,
  onClick,
}: {
  item: Match3DItemSnapshot;
  disabled: boolean;
  onClick: (item: Match3DItemSnapshot) => void;
}) {
  const visualSeed = resolveVisualSeed(item.visualKey);
  const size = `${item.radius * 200}%`;
  const itemStateClass =
    item.state === 'Flying'
      ? 'scale-75 opacity-0'
      : item.clickable
        ? 'cursor-pointer opacity-100 hover:scale-105 active:scale-95'
        : 'opacity-48';

  if (item.state !== 'InBoard' && item.state !== 'Flying') {
    return null;
  }

  return (
    <button
      type="button"
      className={`absolute flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-white/35 bg-gradient-to-br ${visualSeed.colorClassName} text-sm font-black text-white shadow-[0_10px_18px_rgba(15,23,42,0.32)] transition-all duration-300 [text-shadow:0_1px_2px_rgba(15,23,42,0.65)] ${itemStateClass}`}
      style={{
        left: `${item.x * 100}%`,
        top: `${item.y * 100}%`,
        width: size,
        height: size,
        zIndex: item.layer,
      }}
      aria-label={`${visualSeed.label} ${item.clickable ? '可点击' : '被遮挡'}`}
      data-testid={`match3d-item-${item.itemInstanceId}`}
      disabled={disabled || !item.clickable || item.state !== 'InBoard'}
      onClick={() => onClick(item)}
    >
      <span className="relative z-10">{visualSeed.label}</span>
      <span className="absolute inset-[16%] rounded-full bg-white/24" />
      <span className="absolute left-[18%] top-[14%] h-[18%] w-[28%] rounded-full bg-white/42 blur-[1px]" />
    </button>
  );
}

function Match3DTrayToken({ slot }: { slot: Match3DTraySlot }) {
  if (!slot.visualKey) {
    return <span className="h-full w-full rounded-xl border border-dashed border-slate-300/35 bg-white/8" />;
  }
  const visualSeed = resolveVisualSeed(slot.visualKey);
  return (
    <span
      className={`flex h-full w-full items-center justify-center rounded-xl border border-white/35 bg-gradient-to-br ${visualSeed.colorClassName} text-xs font-black text-white shadow-[0_8px_16px_rgba(15,23,42,0.24)] [text-shadow:0_1px_2px_rgba(15,23,42,0.62)]`}
    >
      {visualSeed.label}
    </span>
  );
}

function Match3DSettlement({
  run,
  onBack,
  onRestart,
}: {
  run: Match3DRunSnapshot;
  onBack: () => void;
  onRestart: () => void;
}) {
  if (run.status === 'Running') {
    return null;
  }
  const won = run.status === 'Won';
  const stopped = run.status === 'Stopped';
  const title = won ? '通关完成' : stopped ? '已停止' : '本轮失败';
  const description = won
    ? `用时 ${formatElapsed(run.startedAtMs, run.remainingMs, run.durationLimitMs)}`
    : `已清除 ${run.clearedItemCount}/${run.totalItemCount}`;
  return (
    <div className="absolute inset-0 z-[80] flex items-center justify-center bg-slate-950/62 px-5 backdrop-blur-sm">
      <section
        className="w-full max-w-sm rounded-[1.5rem] border border-white/18 bg-white/94 p-5 text-slate-950 shadow-[0_26px_70px_rgba(15,23,42,0.34)]"
        role="dialog"
        aria-label={title}
      >
        <div className="mb-4 flex items-center gap-3">
          <span
            className={`flex h-11 w-11 items-center justify-center rounded-full ${
              won ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700'
            }`}
          >
            {won ? <CheckCircle2 size={24} /> : <XCircle size={24} />}
          </span>
          <div>
            <h2 className="text-xl font-black">{title}</h2>
            <p className="text-sm font-semibold text-slate-500">{description}</p>
          </div>
        </div>
        <div className="grid grid-cols-2 gap-2">
          <button
            type="button"
            className="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-black text-slate-700"
            onClick={onBack}
          >
            返回
          </button>
          <button
            type="button"
            className="rounded-xl bg-slate-950 px-4 py-3 text-sm font-black text-white"
            onClick={onRestart}
          >
            再来一局
          </button>
        </div>
      </section>
    </div>
  );
}

export function Match3DRuntimeShell({
  run,
  isBusy = false,
  error = null,
  onBack,
  onRestart,
  onOptimisticRunChange,
  onClickItem,
  onTimeExpired,
}: Match3DRuntimeShellProps) {
  const stageRef = useRef<HTMLDivElement | null>(null);
  const [pendingClick, setPendingClick] = useState<PendingClick | null>(null);
  const [feedbackEvent, setFeedbackEvent] = useState<Match3DFeedbackEvent | null>(
    null,
  );
  const [timeLeftMs, setTimeLeftMs] = useState(run?.remainingMs ?? 0);

  useEffect(() => {
    setTimeLeftMs(run?.remainingMs ?? 0);
  }, [run?.remainingMs, run?.snapshotVersion]);

  useEffect(() => {
    if (!run || run.status !== 'Running') {
      return undefined;
    }
    const timer = window.setInterval(() => {
      setTimeLeftMs((current) => {
        const next = Math.max(0, current - 1000);
        if (next <= 0) {
          onTimeExpired?.();
        }
        return next;
      });
    }, 1000);
    return () => window.clearInterval(timer);
  }, [onTimeExpired, run]);

  useEffect(() => {
    if (!feedbackEvent) {
      return undefined;
    }
    const timer = window.setTimeout(() => setFeedbackEvent(null), 520);
    return () => window.clearTimeout(timer);
  }, [feedbackEvent]);

  const progressText = useMemo(() => {
    if (!run) {
      return '0/0';
    }
    return `${run.clearedItemCount}/${run.totalItemCount}`;
  }, [run]);

  const handleItemClick = async (item: Match3DItemSnapshot) => {
    if (!run || run.status !== 'Running' || pendingClick) {
      return;
    }
    const optimisticRun = buildOptimisticRun(run, item);
    const clientEventId = buildClientEventId(item.itemInstanceId);
    // 中文注释：先更新前端即时反馈，再等待后端确认；确认失败时用权威快照回滚校正。
    setPendingClick({
      clientEventId,
      itemInstanceId: item.itemInstanceId,
      previousRun: run,
    });
    onOptimisticRunChange(optimisticRun);

    const result = await onClickItem({
      runId: run.runId,
      itemInstanceId: item.itemInstanceId,
      clientSnapshotVersion: run.snapshotVersion,
      clientEventId,
      clickedAtMs: Date.now(),
    });
    if (result.status === 'Accepted') {
      if (result.clearedItemInstanceIds.length > 0) {
        setFeedbackEvent({
          id: clientEventId,
          kind: 'cleared',
          itemIds: result.clearedItemInstanceIds,
        });
      }
      onOptimisticRunChange(result.run);
    } else {
      setFeedbackEvent({
        id: clientEventId,
        kind: 'rejected',
        itemIds: [item.itemInstanceId],
      });
      onOptimisticRunChange(result.run ?? run);
    }
    setPendingClick(null);
  };

  const handleBoardPointerDown = (event: PointerEvent<HTMLDivElement>) => {
    if (!run || run.status !== 'Running' || pendingClick) {
      return;
    }
    const rect = stageRef.current?.getBoundingClientRect();
    if (!rect) {
      return;
    }
    const pointX = (event.clientX - rect.left) / rect.width;
    const pointY = (event.clientY - rect.top) / rect.height;
    const item = findHitItem(run, pointX, pointY);
    if (item) {
      void handleItemClick(item);
    }
  };

  if (!run) {
    return (
      <div className="flex min-h-dvh items-center justify-center bg-slate-950 text-white">
        {isBusy ? '载入中' : error ?? '暂无运行态'}
      </div>
    );
  }

  return (
    <main className="relative flex min-h-dvh w-full justify-center overflow-hidden bg-[#16221f] text-white">
      <div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_12%,rgba(255,255,255,0.22),transparent_26%),linear-gradient(180deg,#b8e28d_0%,#377569_52%,#14201f_100%)]" />
      <div className="relative flex min-h-dvh w-full max-w-md flex-col px-3 pb-[calc(env(safe-area-inset-bottom,0px)+0.8rem)] pt-[calc(env(safe-area-inset-top,0px)+0.65rem)]">
        <header className="flex items-center justify-between gap-2">
          <button
            type="button"
            className="flex h-10 w-10 items-center justify-center rounded-full border border-white/16 bg-black/22 text-white backdrop-blur"
            onClick={onBack}
            aria-label="返回"
          >
            <ArrowLeft size={20} />
          </button>
          <div className="flex items-center gap-2 rounded-full border border-white/16 bg-black/24 px-3 py-2 text-sm font-black backdrop-blur">
            <Clock3 size={16} />
            <span>{formatTimer(timeLeftMs)}</span>
          </div>
          <button
            type="button"
            className="flex h-10 w-10 items-center justify-center rounded-full border border-white/16 bg-black/22 text-white backdrop-blur"
            onClick={onRestart}
            aria-label="重新开始"
          >
            <RotateCcw size={18} />
          </button>
        </header>

        <section className="mt-3 grid grid-cols-3 gap-2 text-center text-[0.72rem] font-black">
          <div className="rounded-2xl border border-white/14 bg-black/18 px-2 py-2 backdrop-blur">
            {progressText}
          </div>
          <div className="rounded-2xl border border-white/14 bg-black/18 px-2 py-2 backdrop-blur">
            {run.clearCount} 组
          </div>
          <div className="rounded-2xl border border-white/14 bg-black/18 px-2 py-2 backdrop-blur">
            v{run.snapshotVersion}
          </div>
        </section>

        <section className="relative mt-3 flex flex-1 min-h-0 items-center justify-center">
          <div
            ref={stageRef}
            className="relative aspect-square w-full max-w-[min(92vw,58dvh)] overflow-hidden rounded-full border-[10px] border-[#e6d19b] bg-[radial-gradient(circle_at_50%_42%,#f2d993_0%,#c88f43_56%,#835223_100%)] shadow-[inset_0_8px_34px_rgba(72,41,16,0.34),0_22px_42px_rgba(15,23,42,0.28)]"
            onPointerDown={handleBoardPointerDown}
            data-testid="match3d-board"
          >
            <div className="absolute inset-[7%] rounded-full border border-white/22 bg-[radial-gradient(circle_at_44%_35%,rgba(255,255,255,0.22),transparent_28%)]" />
            {run.items.map((item) => (
              <Match3DToken
                key={item.itemInstanceId}
                item={item}
                disabled={Boolean(pendingClick)}
                onClick={handleItemClick}
              />
            ))}
            {feedbackEvent?.kind === 'cleared' ? (
              <div className="pointer-events-none absolute inset-0 z-[70] flex items-center justify-center">
                <div className="flex h-24 w-24 items-center justify-center rounded-full bg-white/24 text-amber-100 shadow-[0_0_42px_rgba(255,255,255,0.72)] backdrop-blur-sm">
                  <Sparkles size={42} />
                </div>
              </div>
            ) : null}
          </div>
        </section>

        <section className="mt-3 rounded-[1.35rem] border border-white/14 bg-black/24 p-2 shadow-[0_16px_36px_rgba(15,23,42,0.24)] backdrop-blur">
          <div className="grid grid-cols-7 gap-1.5" data-testid="match3d-tray">
            {run.traySlots.map((slot) => (
              <div
                key={slot.slotIndex}
                className="aspect-square min-w-0 rounded-xl bg-white/10 p-1"
                data-testid="match3d-tray-slot"
              >
                <Match3DTrayToken slot={slot} />
              </div>
            ))}
          </div>
        </section>
      </div>

      {feedbackEvent?.kind === 'rejected' ? (
        <div className="pointer-events-none absolute left-1/2 top-24 z-[90] -translate-x-1/2 rounded-full border border-rose-200/60 bg-rose-500/88 px-4 py-2 text-xs font-black text-white shadow-lg">
          已校正
        </div>
      ) : null}

      <Match3DSettlement run={run} onBack={onBack} onRestart={onRestart} />
    </main>
  );
}

export default Match3DRuntimeShell;
