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

import { computePathPlaybackSample } from '../../../shared/types/tokenPathPlayback';
import {
  tokenPathKey,
  type TokenPathPlaybackPhase,
  type TokenPathSessionState,
} from '../../../shared/types/tokenPathSession';
import type { SceneNpcToken, SceneToken } from '../../../shared/types';

export type TokenPathLivePose = {
  nx: number;
  ny: number;
  rotationDeg: number;
  phase: TokenPathPlaybackPhase;
  dist: number;
};

type PoseMaps = {
  tokenPoses: Record<string, TokenPathLivePose>;
  npcPoses: Record<string, TokenPathLivePose>;
};

const EMPTY: PoseMaps = { tokenPoses: {}, npcPoses: {} };

function posesEqual(a: PoseMaps, b: PoseMaps): boolean {
  const aT = a.tokenPoses;
  const bT = b.tokenPoses;
  const aN = a.npcPoses;
  const bN = b.npcPoses;
  const aTk = Object.keys(aT);
  const bTk = Object.keys(bT);
  const aNk = Object.keys(aN);
  const bNk = Object.keys(bN);
  if (aTk.length !== bTk.length || aNk.length !== bNk.length) return false;
  for (const k of aTk) {
    const x = aT[k];
    const y = bT[k];
    if (!y || x!.nx !== y.nx || x!.ny !== y.ny || x!.rotationDeg !== y.rotationDeg || x!.phase !== y.phase) {
      return false;
    }
  }
  for (const k of aNk) {
    const x = aN[k];
    const y = bN[k];
    if (!y || x!.nx !== y.nx || x!.ny !== y.ny || x!.rotationDeg !== y.rotationDeg || x!.phase !== y.phase) {
      return false;
    }
  }
  return true;
}

/**
 * RAF poses for control/presentation while path playback is active.
 * When phase === 'stopped', pose is omitted so session/placement (drag) wins.
 *
 * Uses local Date.now() — segmentStartedAtMs is also wall-clock from main (same machine).
 * Do NOT clamp to stale serverNowMs: that froze motion after ~250ms until the next IPC bump.
 */
export function useTokenPathLivePoses(args: {
  tokens: readonly SceneToken[];
  npcTokens: readonly SceneNpcToken[];
  pathSession: TokenPathSessionState | null;
  enabled?: boolean;
  onMarkDone?: (kind: 'token' | 'npcToken', placementId: string, atDist: number) => void;
}): PoseMaps {
  const { tokens, npcTokens, pathSession, enabled = true, onMarkDone } = args;
  const [poses, setPoses] = useState<PoseMaps>(EMPTY);
  const markedDoneRef = useRef<Set<string>>(new Set());
  const onMarkDoneRef = useRef(onMarkDone);
  onMarkDoneRef.current = onMarkDone;

  const pathSessionRef = useRef(pathSession);
  pathSessionRef.current = pathSession;
  const tokensRef = useRef(tokens);
  tokensRef.current = tokens;
  const npcTokensRef = useRef(npcTokens);
  npcTokensRef.current = npcTokens;

  useEffect(() => {
    markedDoneRef.current.clear();
  }, [pathSession?.revision]);

  useEffect(() => {
    if (!enabled) {
      setPoses(EMPTY);
      return;
    }
    let raf = 0;
    let alive = true;

    const tick = () => {
      if (!alive) return;
      const session = pathSessionRef.current;
      if (!session) {
        setPoses((prev) => (prev === EMPTY || Object.keys(prev.tokenPoses).length + Object.keys(prev.npcPoses).length === 0 ? prev : EMPTY));
        raf = requestAnimationFrame(tick);
        return;
      }

      // Wall clock: matches main's Date.now() for segmentStartedAtMs (Electron, one host).
      const sampleNow = Date.now();
      const tokenPoses: Record<string, TokenPathLivePose> = {};
      const npcPoses: Record<string, TokenPathLivePose> = {};

      const sampleOne = (
        kind: 'token' | 'npcToken',
        placementId: string,
        path: NonNullable<SceneToken['path']>,
        out: Record<string, TokenPathLivePose>,
      ) => {
        const key = tokenPathKey(kind, placementId);
        const entry = session.playback[key];
        if (!entry) return;
        if (entry.phase === 'stopped') return;
        const result = computePathPlaybackSample({ path, entry, nowMs: sampleNow });
        if (!result) return;
        out[placementId] = {
          nx: result.sample.nx,
          ny: result.sample.ny,
          rotationDeg: result.sample.rotationDeg,
          phase: result.phase,
          dist: result.sample.dist,
        };
        if (result.markDone && !markedDoneRef.current.has(key)) {
          markedDoneRef.current.add(key);
          onMarkDoneRef.current?.(kind, placementId, result.sample.dist);
        }
      };

      for (const t of tokensRef.current) {
        if (t.path && t.path.points.length >= 2) {
          sampleOne('token', String(t.id), t.path, tokenPoses);
        }
      }
      for (const t of npcTokensRef.current) {
        if (t.path && t.path.points.length >= 2) {
          sampleOne('npcToken', String(t.id), t.path, npcPoses);
        }
      }

      const next: PoseMaps = { tokenPoses, npcPoses };
      setPoses((prev) => (posesEqual(prev, next) ? prev : next));
      raf = requestAnimationFrame(tick);
    };

    raf = requestAnimationFrame(tick);
    return () => {
      alive = false;
      cancelAnimationFrame(raf);
    };
  }, [enabled]);

  return poses;
}
