import React, { useRef, useState } from 'react';

import type {
  NpcDisposition,
  ProjectNpc,
  SceneGrid,
  SceneNpcToken,
  SceneNpcTokensSessionState,
} from '../../../shared/types';
import { clampSceneNpcTokenSizeN, DEFAULT_NPC_TOKEN_SESSION_SCALE } from '../../../shared/types/appPlayers';
import {
  normalizeNpcDisposition,
  npcDispositionRingColor,
} from '../../../shared/types/npcDisposition';
import { sceneGridTokenFitFactor } from '../../../shared/types/sceneGrid';
import { useAssetUrl } from '../useAssetImageUrl';

import { PlayerTokenView } from './PlayerTokenView';
import { useLiveDragBroadcast, useLiveResizeBroadcast } from './useLiveDragBroadcast';
import styles from './SceneNpcTokensOverlay.module.css';

type Viewport = { x: number; y: number; w: number; h: number };

export function resolveNpcTokenDisposition(
  placement: SceneNpcToken,
  npc: ProjectNpc,
  sessionDisposition?: NpcDisposition,
): NpcDisposition {
  if (sessionDisposition) return normalizeNpcDisposition(sessionDisposition);
  return normalizeNpcDisposition(placement.disposition ?? npc.disposition);
}

function NpcSprite({
  placement,
  npc,
  nx,
  ny,
  sizeN,
  viewport,
  editable,
  dragEnabled,
  displayScale,
  gridFit,
  disposition,
  inactive,
  onMove,
  onResize,
  resizeTitle,
  onContextMenu,
  snapNorm,
}: {
  placement: SceneNpcToken;
  npc: ProjectNpc;
  nx: number;
  ny: number;
  sizeN: number;
  viewport: Viewport;
  editable: boolean;
  dragEnabled: boolean;
  displayScale: number;
  gridFit: number;
  disposition: NpcDisposition;
  inactive: boolean;
  onMove?: (placementId: string, nx: number, ny: number) => void;
  onResize?: (placementId: string, sizeN: number) => void;
  resizeTitle?: string;
  onContextMenu?: (e: React.MouseEvent, placement: SceneNpcToken) => void;
  snapNorm?: (nx: number, ny: number) => { nx: number; ny: number };
}) {
  const imageUrl = useAssetUrl(npc.avatarAssetId);
  const [localPos, setLocalPos] = useState<{ nx: number; ny: number } | null>(null);
  const [localSize, setLocalSize] = useState<number | null>(null);
  const dragRef = useRef<{
    pointerId: number;
    startNx: number;
    startNy: number;
    pointerNx: number;
    pointerNy: number;
    lastNx: number;
    lastNy: number;
  } | null>(null);
  const resizeRef = useRef<{
    pointerId: number;
    startSize: number;
    startDist: number;
    lastSize: number;
  } | null>(null);
  const { schedule, flush } = useLiveDragBroadcast(onMove, String(placement.id));
  const { schedule: scheduleResize, flush: flushResize } = useLiveResizeBroadcast(
    onResize,
    String(placement.id),
  );
  const pos = localPos ?? { nx, ny };
  const shownSizeN = localSize ?? sizeN;
  const minDim = Math.min(viewport.w, viewport.h);
  const sizePx = Math.max(16, shownSizeN * gridFit * displayScale * minDim);
  const canDrag = editable && dragEnabled && onMove !== undefined;
  const canResize = editable && dragEnabled && onResize !== undefined;

  const point = (e: React.PointerEvent, host?: HTMLElement | null) => {
    const rect = (host ?? e.currentTarget.parentElement)?.getBoundingClientRect();
    return {
      x: Math.max(0, Math.min(1, (e.clientX - ((rect?.left ?? 0) + viewport.x)) / Math.max(1, viewport.w))),
      y: Math.max(0, Math.min(1, (e.clientY - ((rect?.top ?? 0) + viewport.y)) / Math.max(1, viewport.h))),
    };
  };

  const end = (e: React.PointerEvent<HTMLDivElement>) => {
    const drag = dragRef.current;
    if (drag?.pointerId !== e.pointerId) return;
    dragRef.current = null;
    flush(drag.lastNx, drag.lastNy);
    setLocalPos(null);
  };

  const endResize = (e: React.PointerEvent<HTMLDivElement>) => {
    const d = resizeRef.current;
    if (d?.pointerId !== e.pointerId) return;
    resizeRef.current = null;
    flushResize(d.lastSize);
    setLocalSize(null);
  };

  return (
    <div
      className={[styles.token, canDrag ? styles.editable : ''].filter(Boolean).join(' ')}
      data-testid={`session-npc-token-${placement.id}`}
      style={{
        left: viewport.x + pos.nx * viewport.w,
        top: viewport.y + pos.ny * viewport.h,
        width: sizePx,
        height: sizePx,
        pointerEvents: canDrag || canResize || onContextMenu ? 'auto' : undefined,
      }}
      onContextMenu={
        onContextMenu
          ? (e) => {
              e.preventDefault();
              e.stopPropagation();
              onContextMenu(e, placement);
            }
          : undefined
      }
      onPointerDown={
        canDrag
          ? (e) => {
              if (e.button !== 0) return;
              e.preventDefault();
              e.stopPropagation();
              const p = point(e);
              dragRef.current = {
                pointerId: e.pointerId,
                startNx: pos.nx,
                startNy: pos.ny,
                pointerNx: p.x,
                pointerNy: p.y,
                lastNx: pos.nx,
                lastNy: pos.ny,
              };
              e.currentTarget.setPointerCapture(e.pointerId);
            }
          : undefined
      }
      onPointerMove={
        canDrag
          ? (e) => {
              const drag = dragRef.current;
              if (drag?.pointerId !== e.pointerId) return;
              const p = point(e);
              const rawNx = Math.max(0, Math.min(1, drag.startNx + p.x - drag.pointerNx));
              const rawNy = Math.max(0, Math.min(1, drag.startNy + p.y - drag.pointerNy));
              const snapped = snapNorm ? snapNorm(rawNx, rawNy) : { nx: rawNx, ny: rawNy };
              drag.lastNx = snapped.nx;
              drag.lastNy = snapped.ny;
              setLocalPos({ nx: drag.lastNx, ny: drag.lastNy });
              schedule(drag.lastNx, drag.lastNy);
            }
          : undefined
      }
      onPointerUp={end}
      onPointerCancel={end}
    >
      <PlayerTokenView
        name={npc.name}
        imageUrl={imageUrl}
        ringColor={npcDispositionRingColor(disposition, inactive)}
        imageOffset={npc.imageOffset}
        imageScale={npc.imageScale}
        sizePx={sizePx}
        inactive={inactive}
      />
      {canResize ? (
        <div
          className={styles.resizeHandle}
          title={resizeTitle}
          onPointerDown={(e) => {
            if (e.button !== 0) return;
            e.preventDefault();
            e.stopPropagation();
            const host = e.currentTarget.closest('[data-token-layer="1"]') as HTMLElement | null;
            const p = point(e, host);
            const dist = Math.max(1e-4, Math.hypot(p.x - pos.nx, p.y - pos.ny));
            resizeRef.current = {
              pointerId: e.pointerId,
              startSize: shownSizeN,
              startDist: dist,
              lastSize: shownSizeN,
            };
            setLocalSize(shownSizeN);
            e.currentTarget.setPointerCapture(e.pointerId);
          }}
          onPointerMove={(e) => {
            const d = resizeRef.current;
            if (d?.pointerId !== e.pointerId) return;
            const host = e.currentTarget.closest('[data-token-layer="1"]') as HTMLElement | null;
            const p = point(e, host);
            const dist = Math.max(1e-4, Math.hypot(p.x - pos.nx, p.y - pos.ny));
            const next = clampSceneNpcTokenSizeN(d.startSize * (dist / d.startDist));
            d.lastSize = next;
            setLocalSize(next);
            scheduleResize(next);
          }}
          onPointerUp={endResize}
          onPointerCancel={endResize}
        />
      ) : null}
    </div>
  );
}

export function SceneNpcTokensOverlay({
  placements,
  library,
  session,
  viewport,
  grid = null,
  editable = false,
  onMove,
  onResize,
  resizeTitle,
  onContextMenu,
  snapNorm,
  poseOverrides = null,
  dragEnabledById = null,
}: {
  placements: readonly SceneNpcToken[];
  library: readonly ProjectNpc[];
  session: SceneNpcTokensSessionState | null;
  viewport: Viewport | null;
  /** Для гекса — вписанный диаметр ячейки, не описанный. */
  grid?: SceneGrid | null;
  editable?: boolean;
  onMove?: (placementId: string, nx: number, ny: number) => void;
  onResize?: (placementId: string, sizeN: number) => void;
  resizeTitle?: string;
  onContextMenu?: (e: React.MouseEvent, placement: SceneNpcToken) => void;
  snapNorm?: (nx: number, ny: number) => { nx: number; ny: number };
  poseOverrides?: Record<string, { nx: number; ny: number }> | null;
  dragEnabledById?: Record<string, boolean> | null;
}) {
  if (!viewport) return null;
  const byId = new Map(library.map((npc) => [npc.id, npc]));
  const displayScale = session?.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE;
  const gridFit = sceneGridTokenFitFactor(grid);
  return (
    <div className={styles.layer} data-token-layer="1">
      {placements.map((placement) => {
        const npc = byId.get(placement.npcId);
        if (!npc) return null;
        const key = String(placement.id);
        const pose = poseOverrides?.[key];
        const override = session?.byPlacementId[key];
        const disposition = resolveNpcTokenDisposition(placement, npc, override?.disposition);
        const inactive = Boolean(override?.inactive);
        const dragEnabled = dragEnabledById ? Boolean(dragEnabledById[key]) : true;
        return (
          <NpcSprite
            key={placement.id}
            placement={placement}
            npc={npc}
            nx={pose?.nx ?? override?.nx ?? placement.nx}
            ny={pose?.ny ?? override?.ny ?? placement.ny}
            sizeN={override?.sizeN ?? placement.sizeN}
            viewport={viewport}
            editable={editable}
            dragEnabled={dragEnabled}
            displayScale={displayScale}
            gridFit={gridFit}
            disposition={disposition}
            inactive={inactive}
            {...(onMove ? { onMove } : {})}
            {...(onResize ? { onResize } : {})}
            {...(resizeTitle ? { resizeTitle } : {})}
            {...(onContextMenu ? { onContextMenu } : {})}
            {...(snapNorm ? { snapNorm } : {})}
          />
        );
      })}
    </div>
  );
}
