import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import { ipcChannels, type SessionState } from '../../shared/ipc/contracts';
import type { SceneNpcToken, SceneToken, TokenPath, TokenPathPoint } from '../../shared/types';
import {
  createEmptyTokenPath,
  normalizeTokenPath,
  reverseTokenPathPoints,
  sampleTokenPathAtProgress,
  tokenPathPolyline,
  tryAppendPathPoint,
} from '../../shared/types/tokenPath';
import type { TokenPathTargetKind } from '../../shared/types/tokenPathSession';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import { getDndApi } from '../shared/dndApi';
import { ContainedVideo } from '../shared/ContainedVideo';
import { RotatedImage } from '../shared/RotatedImage';
import { useAppTokens } from '../shared/tokens/useAppTokens';
import { useTokenImageUrl } from '../shared/tokens/useTokenImageUrl';
import { PlayerTokenView } from '../shared/playerToken/PlayerTokenView';
import { normalizeNpcDisposition, npcDispositionRingColor } from '../../shared/types/npcDisposition';
import { sceneGridTokenFitFactor } from '../../shared/types/sceneGrid';
import { useAssetUrl } from '../shared/useAssetImageUrl';
import { Button, Input, Select } from '../shared/ui/controls';

import styles from './TokenPathEditorApp.module.css';

type Target = { kind: TokenPathTargetKind; placementId: string };

type Draft = TokenPath;

type PointMenu = { x: number; y: number; index: number };

function cloneDraft(path: TokenPath | null | undefined): Draft {
  if (!path) return createEmptyTokenPath();
  return {
    ...path,
    points: path.points.map((p) => ({ ...p })),
  };
}

function TokenPreview({
  kind,
  token,
  npcToken,
  npcName,
  npcAvatarUrl,
  ringColor,
  imageOffset,
  imageScale,
  left,
  top,
  sizePx,
  rotationDeg,
}: {
  kind: TokenPathTargetKind;
  token?: SceneToken;
  npcToken?: SceneNpcToken;
  npcName?: string;
  npcAvatarUrl?: string | null;
  ringColor?: string;
  imageOffset?: { x: number; y: number };
  imageScale?: number;
  left: number;
  top: number;
  sizePx: number;
  rotationDeg: number;
}) {
  const url = useTokenImageUrl(token?.tokenId ?? null);
  if (kind === 'token' && token) {
    return (
      <div
        className={styles.tokenPreview}
        style={{
          left,
          top,
          width: sizePx,
          height: sizePx,
          transform: `translate(-50%, -50%) rotate(${String(rotationDeg)}deg)`,
        }}
      >
        {url ? <img src={url} alt="" draggable={false} /> : null}
      </div>
    );
  }
  if (kind === 'npcToken' && npcToken) {
    return (
      <div className={styles.tokenPreview} style={{ left, top, width: sizePx, height: sizePx }}>
        <PlayerTokenView
          name={npcName ?? ''}
          imageUrl={npcAvatarUrl ?? null}
          ringColor={ringColor ?? '#888'}
          sizePx={sizePx}
          {...(imageOffset ? { imageOffset } : {})}
          {...(typeof imageScale === 'number' ? { imageScale } : {})}
        />
      </div>
    );
  }
  return null;
}

export function TokenPathEditorApp() {
  const api = getDndApi();
  const appTokens = useAppTokens();
  const [session, setSession] = useState<SessionState | null>(null);
  const [target, setTarget] = useState<Target | null>(null);
  const [draft, setDraft] = useState<Draft>(createEmptyTokenPath());
  const [contentRect, setContentRect] = useState<{ x: number; y: number; w: number; h: number } | null>(
    null,
  );
  const [pointMenu, setPointMenu] = useState<PointMenu | null>(null);
  const [previewPlaying, setPreviewPlaying] = useState(false);
  const [previewU, setPreviewU] = useState(0);
  const [dirty, setDirty] = useState(false);
  const [status, setStatus] = useState<string | null>(null);
  const hostRef = useRef<HTMLDivElement | null>(null);
  const dragPointRef = useRef<{ index: number; pointerId: number } | null>(null);
  const saveTimerRef = useRef(0);
  const draftRef = useRef(draft);
  draftRef.current = draft;

  const project = session?.project ?? null;
  const sceneId = project?.currentSceneId ?? null;
  const scene = sceneId && project ? project.scenes[sceneId] : undefined;
  const url = useAssetUrl(scene?.previewAssetId ?? null);
  const rot = scene?.previewRotationDeg ?? 0;
  const isImage = scene?.previewAssetType === 'image';
  const isVideo = scene?.previewAssetType === 'video';

  const placement = useMemo(() => {
    if (!target || !scene) return null;
    if (target.kind === 'token') {
      return (scene.tokens ?? []).find((t) => String(t.id) === target.placementId) ?? null;
    }
    return (scene.npcTokens ?? []).find((t) => String(t.id) === target.placementId) ?? null;
  }, [scene, target]);

  const npcMeta = useMemo(() => {
    if (!target || target.kind !== 'npcToken' || !placement || !('npcId' in placement)) return null;
    const npc = project?.npcs.find((n) => n.id === placement.npcId);
    return npc ?? null;
  }, [placement, project?.npcs, target]);

  const npcAvatarUrl = useAssetUrl(npcMeta?.avatarAssetId ?? null);

  useEffect(() => {
    void api.invoke(ipcChannels.project.get, {}).then(({ project: p }) => {
      setSession({ project: p, currentSceneId: p?.currentSceneId ?? null });
    });
    return api.on(ipcChannels.session.stateChanged, ({ state }) => setSession(state));
  }, [api]);

  useEffect(() => {
    void api.invoke(ipcChannels.windows.getTokenPathEditorTarget, {}).then((t) => {
      setTarget(t);
    });
    return api.on(ipcChannels.windows.tokenPathEditorTargetChanged, (t) => {
      setTarget(t);
    });
  }, [api]);

  useEffect(() => {
    if (!placement) {
      setDraft(createEmptyTokenPath());
      setDirty(false);
      return;
    }
    setDraft(cloneDraft(placement.path ?? null));
    setDirty(false);
    setPreviewPlaying(false);
    setPreviewU(0);
    setStatus(null);
  }, [placement?.id, target?.kind, target?.placementId]);

  const persistDraft = useCallback(
    (next: Draft, immediate = false) => {
      if (!sceneId || !target) return;
      const normalized = normalizeTokenPath(next);
      const run = () => {
        if (target.kind === 'token') {
          const tokens = (scene?.tokens ?? []).map((t) =>
            String(t.id) === target.placementId ? { ...t, path: normalized } : t,
          );
          void api.invoke(ipcChannels.project.updateScene, { sceneId, patch: { tokens } });
        } else {
          const npcTokens = (scene?.npcTokens ?? []).map((t) =>
            String(t.id) === target.placementId ? { ...t, path: normalized } : t,
          );
          void api.invoke(ipcChannels.project.updateScene, { sceneId, patch: { npcTokens } });
        }
        setDirty(false);
        setStatus(normalized ? 'Сохранено' : 'Путь очищен (нужно ≥2 точки)');
      };
      if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current);
      if (immediate) {
        run();
        return;
      }
      saveTimerRef.current = window.setTimeout(run, 180);
    },
    [api, scene?.npcTokens, scene?.tokens, sceneId, target],
  );

  const updateDraft = useCallback(
    (updater: (prev: Draft) => Draft, opts?: { save?: boolean; immediate?: boolean }) => {
      setDraft((prev) => {
        const next = updater(prev);
        draftRef.current = next;
        if (opts?.save !== false) {
          setDirty(true);
          persistDraft(next, opts?.immediate);
        }
        return next;
      });
    },
    [persistDraft],
  );

  useEffect(() => {
    if (!previewPlaying) return;
    const started = performance.now();
    const durationMs = Math.max(0.5, draft.durationSec) * 1000;
    const loopMode = draft.loopMode;
    const closed = draft.closed;
    let raf = 0;
    let stopped = false;
    const tick = (now: number) => {
      if (stopped) return;
      const elapsed = Math.max(0, now - started);
      let u = 0;
      if (loopMode === 'pingpong') {
        const period = Math.max(durationMs * 2, 1e-9);
        let t = elapsed % period;
        if (t > durationMs) t = period - t;
        u = t / durationMs;
      } else if (loopMode === 'loop' && closed) {
        u = (elapsed % durationMs) / durationMs;
      } else {
        // once (и loop без замыкания)
        u = Math.min(1, elapsed / durationMs);
        setPreviewU(u);
        if (u >= 1) {
          stopped = true;
          setPreviewPlaying(false);
          return;
        }
        raf = requestAnimationFrame(tick);
        return;
      }
      setPreviewU(u);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => {
      stopped = true;
      cancelAnimationFrame(raf);
    };
  }, [previewPlaying, draft.durationSec, draft.points, draft.closed, draft.loopMode]);

  const hostToNorm = useCallback(
    (clientX: number, clientY: number) => {
      const host = hostRef.current;
      if (!host || !contentRect) return null;
      const r = host.getBoundingClientRect();
      return {
        nx: Math.max(0, Math.min(1, (clientX - (r.left + contentRect.x)) / Math.max(1e-6, contentRect.w))),
        ny: Math.max(0, Math.min(1, (clientY - (r.top + contentRect.y)) / Math.max(1e-6, contentRect.h))),
      };
    },
    [contentRect],
  );

  const previewSample = useMemo(() => {
    if (!previewPlaying || draft.points.length < 2) return null;
    const normalized = normalizeTokenPath(draft);
    if (!normalized) return null;
    return sampleTokenPathAtProgress(normalized, previewU);
  }, [draft, previewPlaying, previewU]);

  const tokenLeftTop = useMemo(() => {
    if (!contentRect || !placement) return null;
    const nx = previewSample?.nx ?? placement.nx;
    const ny = previewSample?.ny ?? placement.ny;
    const minDim = Math.min(contentRect.w, contentRect.h);
    const sizeN =
      target?.kind === 'npcToken' && 'sizeN' in placement
        ? placement.sizeN * sceneGridTokenFitFactor(scene?.grid ?? null)
        : placement.sizeN;
    const sizePx = Math.max(16, sizeN * minDim);
    return {
      left: contentRect.x + nx * contentRect.w,
      top: contentRect.y + ny * contentRect.h,
      sizePx,
      rotationDeg:
        previewSample?.rotationDeg ??
        (target?.kind === 'token' && 'rotationDeg' in placement ? placement.rotationDeg : 0),
    };
  }, [contentRect, placement, previewSample, scene?.grid, target?.kind]);

  const title = useMemo(() => {
    if (!target) return 'Движение токена';
    if (target.kind === 'token') {
      const tok = placement && 'tokenId' in placement ? appTokens.find((a) => a.id === placement.tokenId) : null;
      return tok ? `Движение: ${tok.name}` : 'Движение токена';
    }
    return npcMeta ? `Движение: ${npcMeta.name}` : 'Движение НПС';
  }, [appTokens, npcMeta, placement, target]);

  if (!USERS_BRANCH_FEATURES_ENABLED && target?.kind === 'npcToken') {
    return (
      <div className={styles.page}>
        <div className={styles.empty}>НПС недоступны в этой сборке.</div>
      </div>
    );
  }

  return (
    <div className={styles.page}>
      <aside className={styles.sidebar}>
        <div className={styles.title}>{title}</div>
        <p className={styles.hint}>
          ЛКМ по карте — добавить точку. Перетаскивайте точки для правки. ПКМ по точке — замкнуть или удалить.
        </p>

        <label className={styles.field}>
          <span>Длительность, сек</span>
          <Input
            value={String(draft.durationSec)}
            onChange={(v) => {
              const n = Number(v);
              updateDraft((d) => ({
                ...d,
                durationSec: Number.isFinite(n) ? n : d.durationSec,
              }));
            }}
          />
        </label>

        <label className={styles.field}>
          <span>Режим цикла</span>
          <Select
            value={draft.loopMode}
            options={[
              { value: 'once', label: 'Один раз' },
              { value: 'pingpong', label: 'Туда-обратно' },
              {
                value: 'loop',
                label: draft.closed ? 'Зациклить' : 'Зациклить (нужно замкнуть)',
                disabled: !draft.closed,
              },
            ]}
            onChange={(v) => {
              updateDraft((d) => ({
                ...d,
                loopMode: v === 'loop' && !d.closed ? 'once' : (v as Draft['loopMode']),
              }));
            }}
          />
        </label>

        <label className={styles.field}>
          <span>Старт</span>
          <Select
            value={draft.startMode}
            options={[
              { value: 'onEnter', label: 'При входе в сцену' },
              { value: 'delayed', label: 'С задержкой' },
            ]}
            onChange={(v) => {
              updateDraft((d) => ({
                ...d,
                startMode: v === 'delayed' ? 'delayed' : 'onEnter',
              }));
            }}
          />
        </label>

        {draft.startMode === 'delayed' ? (
          <label className={styles.field}>
            <span>Задержка, сек</span>
            <Input
              value={String(draft.delaySec)}
              onChange={(v) => {
                const n = Number(v);
                updateDraft((d) => ({
                  ...d,
                  delaySec: Number.isFinite(n) ? n : d.delaySec,
                }));
              }}
            />
          </label>
        ) : null}

        <label className={styles.field}>
          <span>Ориентация</span>
          <Select
            value={draft.facingMode}
            options={[
              { value: 'tangentSmooth', label: 'По касательной' },
              { value: 'fixed', label: 'Фиксированный угол' },
            ]}
            onChange={(v) => {
              updateDraft((d) => ({
                ...d,
                facingMode: v === 'fixed' ? 'fixed' : 'tangentSmooth',
              }));
            }}
          />
        </label>

        {draft.facingMode === 'fixed' ? (
          <label className={styles.field}>
            <span>Угол, °</span>
            <Input
              value={String(draft.fixedRotationDeg)}
              onChange={(v) => {
                const n = Number(v);
                updateDraft((d) => ({
                  ...d,
                  fixedRotationDeg: Number.isFinite(n) ? n : d.fixedRotationDeg,
                }));
              }}
            />
          </label>
        ) : null}

        <div className={styles.actions}>
          <Button
            onClick={() => {
              updateDraft((d) => ({
                ...d,
                points: reverseTokenPathPoints(d.points),
              }));
            }}
            disabled={draft.points.length < 2}
          >
            Обратить путь
          </Button>
          <Button
            onClick={() => {
              setPreviewPlaying((p) => !p);
              setPreviewU(0);
            }}
            disabled={draft.points.length < 2}
          >
            {previewPlaying ? 'Стоп превью' : 'Превью'}
          </Button>
          <Button
            onClick={() => {
              updateDraft(() => createEmptyTokenPath(), { immediate: true });
              setPreviewPlaying(false);
            }}
          >
            Очистить путь
          </Button>
          <Button
            onClick={() => {
              persistDraft(draftRef.current, true);
            }}
          >
            Сохранить
          </Button>
        </div>

        <div className={styles.meta}>
          Точек: {draft.points.length}
          {draft.closed ? ' · замкнут' : ''}
          {dirty ? ' · есть изменения' : ''}
          {status ? ` · ${status}` : ''}
        </div>
      </aside>

      <main className={styles.main}>
        {!scene || (!isImage && !isVideo) || !url ? (
          <div className={styles.empty}>Нет карты сцены для редактирования пути.</div>
        ) : !target || !placement ? (
          <div className={styles.empty}>Выберите токен в редакторе сцены: ПКМ → «Указать движение».</div>
        ) : (
          <div
            ref={hostRef}
            className={styles.host}
            onContextMenu={(e) => e.preventDefault()}
            onPointerDown={(e) => {
              if (e.button !== 0) return;
              if ((e.target as HTMLElement).closest('[data-path-point]')) return;
              const p = hostToNorm(e.clientX, e.clientY);
              if (!p) return;
              updateDraft((d) => {
                const next = tryAppendPathPoint(d.points, p);
                if (!next) return d;
                return { ...d, points: next, closed: false, loopMode: d.loopMode === 'loop' ? 'once' : d.loopMode };
              });
            }}
            onPointerMove={(e) => {
              const drag = dragPointRef.current;
              if (!drag || drag.pointerId !== e.pointerId) return;
              const p = hostToNorm(e.clientX, e.clientY);
              if (!p) return;
              updateDraft((d) => {
                const points = d.points.map((pt, i) => (i === drag.index ? p : pt));
                return { ...d, points };
              });
            }}
            onPointerUp={(e) => {
              if (dragPointRef.current?.pointerId === e.pointerId) {
                dragPointRef.current = null;
                persistDraft(draftRef.current, true);
              }
            }}
            onPointerCancel={(e) => {
              if (dragPointRef.current?.pointerId === e.pointerId) {
                dragPointRef.current = null;
              }
            }}
          >
            {isImage ? (
              <RotatedImage url={url} rotationDeg={rot} mode="contain" onContentRectChange={setContentRect} />
            ) : (
              <ContainedVideo
                url={url}
                rotationDeg={rot}
                muted
                playsInline
                loop
                preload="metadata"
                onContentRectChange={setContentRect}
              />
            )}
            {contentRect ? (
              <svg className={styles.pathSvg} width="100%" height="100%" aria-hidden>
                {draft.points.length >= 2 ? (
                  <polyline
                    className={styles.pathLine}
                    fill="none"
                    points={tokenPathPolyline(draft)
                      .map((p) => {
                        const x = contentRect.x + p.nx * contentRect.w;
                        const y = contentRect.y + p.ny * contentRect.h;
                        return `${x},${y}`;
                      })
                      .join(' ')}
                  />
                ) : null}
              </svg>
            ) : null}
            {contentRect
              ? draft.points.map((p: TokenPathPoint, index: number) => {
                  const left = contentRect.x + p.nx * contentRect.w;
                  const top = contentRect.y + p.ny * contentRect.h;
                  return (
                    <button
                      key={`${index}_${p.nx}_${p.ny}`}
                      type="button"
                      data-path-point
                      className={styles.point}
                      style={{ left, top }}
                      onPointerDown={(e) => {
                        if (e.button !== 0) return;
                        e.stopPropagation();
                        e.preventDefault();
                        (e.currentTarget as HTMLButtonElement).setPointerCapture(e.pointerId);
                        dragPointRef.current = { index, pointerId: e.pointerId };
                      }}
                      onContextMenu={(e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        setPointMenu({ x: e.clientX, y: e.clientY, index });
                      }}
                    >
                      {index + 1}
                    </button>
                  );
                })
              : null}
            {contentRect && tokenLeftTop && target ? (
              <TokenPreview
                kind={target.kind}
                left={tokenLeftTop.left}
                top={tokenLeftTop.top}
                sizePx={tokenLeftTop.sizePx}
                rotationDeg={tokenLeftTop.rotationDeg}
                {...(target.kind === 'token' && placement && 'tokenId' in placement
                  ? { token: placement as SceneToken }
                  : {})}
                {...(target.kind === 'npcToken' && placement && 'npcId' in placement
                  ? { npcToken: placement as SceneNpcToken }
                  : {})}
                {...(npcMeta?.name ? { npcName: npcMeta.name } : {})}
                {...(npcAvatarUrl ? { npcAvatarUrl } : {})}
                {...(npcMeta
                  ? {
                      ringColor: npcDispositionRingColor(
                        normalizeNpcDisposition(
                          (placement && 'disposition' in placement ? placement.disposition : undefined) ??
                            npcMeta.disposition,
                        ),
                      ),
                    }
                  : {})}
                {...(npcMeta?.imageOffset ? { imageOffset: npcMeta.imageOffset } : {})}
                {...(typeof npcMeta?.imageScale === 'number'
                  ? { imageScale: npcMeta.imageScale }
                  : {})}
              />
            ) : null}
          </div>
        )}
      </main>

      {pointMenu
        ? createPortal(
            <>
              <button
                type="button"
                className={styles.ctxMenuBackdrop}
                aria-label="Закрыть"
                onClick={() => setPointMenu(null)}
              />
              <div className={styles.ctxMenu} style={{ left: pointMenu.x, top: pointMenu.y }} role="menu">
                <button
                  type="button"
                  className={styles.ctxItem}
                  role="menuitem"
                  onClick={() => {
                    updateDraft((d) => ({
                      ...d,
                      closed: true,
                      loopMode: d.loopMode === 'once' ? d.loopMode : d.loopMode,
                    }));
                    setPointMenu(null);
                  }}
                >
                  Замкнуть путь
                </button>
                <button
                  type="button"
                  className={styles.ctxItem}
                  role="menuitem"
                  onClick={() => {
                    updateDraft((d) => ({
                      ...d,
                      closed: false,
                      loopMode: d.loopMode === 'loop' ? 'once' : d.loopMode,
                    }));
                    setPointMenu(null);
                  }}
                >
                  Разомкнуть
                </button>
                <button
                  type="button"
                  className={styles.ctxItemDanger}
                  role="menuitem"
                  onClick={() => {
                    const idx = pointMenu.index;
                    updateDraft((d) => {
                      const points = d.points.filter((_, i) => i !== idx);
                      return {
                        ...d,
                        points,
                        closed: points.length >= 2 ? d.closed : false,
                        loopMode: points.length >= 2 && d.closed ? d.loopMode : d.loopMode === 'loop' ? 'once' : d.loopMode,
                      };
                    });
                    setPointMenu(null);
                  }}
                >
                  Удалить точку
                </button>
              </div>
            </>,
            document.body,
          )
        : null}
    </div>
  );
}
