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

import { ipcChannels } from '../../shared/ipc/contracts';
import type { AppPlayer, AppPlayerTeam, PlayerId, PlayerTeamId } from '../../shared/types';
import {
  DEFAULT_PLAYER_IMAGE_OFFSET,
  DEFAULT_PLAYER_IMAGE_SCALE,
  DEFAULT_PLAYER_RING_COLOR,
} from '../../shared/types/appPlayers';
import { getDndApi } from '../shared/dndApi';
import { PlayerTokenView } from '../shared/playerToken/PlayerTokenView';
import { useAppPlayers } from '../shared/playerToken/useAppPlayers';
import { usePlayerImageUrl } from '../shared/playerToken/usePlayerImageUrl';
import { Button, Input } from '../shared/ui/controls';

import styles from './EditorApp.module.css';
import {
  filterMaterialImagePaths,
  getDroppedFileEntries,
  pickFirstMaterialImagePath,
  useFileDropZone,
} from './fileDrop';
import { useEditorI18n } from './i18n/EditorI18nContext';
import playerStyles from './PlayersModals.module.css';

const PLAYER_DND_MIME = 'application/x-dnd-player-id';

function PlayerRow({
  player,
  selected,
  onSelect,
}: {
  player: AppPlayer;
  selected: boolean;
  onSelect: () => void;
}) {
  const url = usePlayerImageUrl(player.id);
  return (
    <button
      type="button"
      data-testid={`player-row-${player.id}`}
      className={[playerStyles.playerRow, selected ? playerStyles.playerRowSelected : '']
        .filter(Boolean)
        .join(' ')}
      draggable
      onDragStart={(e) => {
        e.dataTransfer.setData(PLAYER_DND_MIME, player.id);
        e.dataTransfer.effectAllowed = 'move';
      }}
      onClick={onSelect}
    >
      <PlayerTokenView
        name={player.name}
        imageUrl={url}
        ringColor={player.ringColor}
        imageOffset={player.imageOffset}
        imageScale={player.imageScale}
        sizePx={48}
        hideName
      />
      <span>{player.name}</span>
    </button>
  );
}

function TeamSection({
  team,
  players,
  selectedId,
  onSelect,
  onAssign,
  onEdit,
  onDelete,
}: {
  team: AppPlayerTeam | null;
  players: AppPlayer[];
  selectedId: PlayerId | null;
  onSelect: (id: PlayerId) => void;
  onAssign: (id: PlayerId, teamId: PlayerTeamId | null) => void;
  onEdit?: () => void;
  onDelete?: () => void;
}) {
  const { t } = useEditorI18n();
  return (
    <section
      className={playerStyles.team}
      onDragOver={(e) => {
        if (e.dataTransfer.types.includes(PLAYER_DND_MIME)) e.preventDefault();
      }}
      onDrop={(e) => {
        e.preventDefault();
        const id = e.dataTransfer.getData(PLAYER_DND_MIME);
        if (id) onAssign(id as PlayerId, team?.id ?? null);
      }}
    >
      <div className={playerStyles.teamHeader}>
        {team ? <span className={playerStyles.teamDot} style={{ background: team.color }} /> : null}
        <span className={playerStyles.teamName}>{team?.name ?? t('players.ungrouped')}</span>
        {team ? (
          <details className={playerStyles.teamMenu}>
            <summary aria-label={t('players.teamMenu')}>⋮</summary>
            <div className={playerStyles.teamMenuPopup}>
              <button type="button" onClick={onEdit}>
                {t('common.edit')}
              </button>
              <button type="button" onClick={onDelete}>
                {t('common.delete')}
              </button>
            </div>
          </details>
        ) : null}
      </div>
      <div className={playerStyles.teamBody}>
        {players.map((player) => (
          <PlayerRow
            key={player.id}
            player={player}
            selected={player.id === selectedId}
            onSelect={() => onSelect(player.id)}
          />
        ))}
        {players.length === 0 ? <div className={playerStyles.dropTarget}>{t('players.dropHere')}</div> : null}
      </div>
    </section>
  );
}

export function PlayersManagerModal({ open, onClose }: { open: boolean; onClose: () => void }) {
  const { t } = useEditorI18n();
  const api = getDndApi();
  const { players, teams } = useAppPlayers();
  const [query, setQuery] = useState('');
  const [selectedId, setSelectedId] = useState<PlayerId | null>(null);
  const [name, setName] = useState('');
  const [ringColor, setRingColor] = useState(DEFAULT_PLAYER_RING_COLOR);
  const [imageOffset, setImageOffset] = useState(DEFAULT_PLAYER_IMAGE_OFFSET);
  const [imageScale, setImageScale] = useState(DEFAULT_PLAYER_IMAGE_SCALE);
  const [filePath, setFilePath] = useState<string | null>(null);
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  const [adding, setAdding] = useState(false);
  const [saving, setSaving] = useState(false);
  const [progress, setProgress] = useState({ percent: 0, detail: '' });
  /** Electron не поддерживает window.prompt — форма команды в сайдбаре. */
  const [teamForm, setTeamForm] = useState<{
    id: PlayerTeamId | null;
    name: string;
    color: string;
  } | null>(null);
  const [pendingDeletePlayer, setPendingDeletePlayer] = useState<AppPlayer | null>(null);
  const [pendingDeleteTeam, setPendingDeleteTeam] = useState<AppPlayerTeam | null>(null);
  const appearanceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const selected = players.find((p) => p.id === selectedId) ?? null;
  const selectedUrl = usePlayerImageUrl(selected?.id);

  const resetModalState = useCallback(() => {
    if (appearanceTimerRef.current) {
      clearTimeout(appearanceTimerRef.current);
      appearanceTimerRef.current = null;
    }
    setQuery('');
    setSelectedId(null);
    setName('');
    setRingColor(DEFAULT_PLAYER_RING_COLOR);
    setImageOffset({ ...DEFAULT_PLAYER_IMAGE_OFFSET });
    setImageScale(DEFAULT_PLAYER_IMAGE_SCALE);
    setFilePath(null);
    setPreviewUrl((prev) => {
      if (prev?.startsWith('blob:')) URL.revokeObjectURL(prev);
      return null;
    });
    setAdding(false);
    setSaving(false);
    setProgress({ percent: 0, detail: '' });
    setTeamForm(null);
    setPendingDeletePlayer(null);
    setPendingDeleteTeam(null);
  }, []);

  const handleClose = useCallback(() => {
    if (saving) return;
    resetModalState();
    onClose();
  }, [onClose, resetModalState, saving]);

  useEffect(() => {
    if (!open) return;
    setSelectedId((current) =>
      current && players.some((p) => p.id === current) ? current : (players[0]?.id ?? null),
    );
  }, [open, players]);

  // Синхронизировать форму только при смене выбранного игрока — не при каждом
  // players.stateChanged (иначе сбрасывается несохранённый zoom/pan).
  useEffect(() => {
    if (adding) return;
    if (!selectedId) return;
    const p = players.find((item) => item.id === selectedId);
    if (!p) return;
    setName(p.name);
    setRingColor(p.ringColor);
    setImageOffset(p.imageOffset);
    setImageScale(p.imageScale ?? DEFAULT_PLAYER_IMAGE_SCALE);
    setFilePath(null);
    setPreviewUrl(null);
    // eslint-disable-next-line react-hooks/exhaustive-deps -- только selectedId / adding
  }, [adding, selectedId]);

  useEffect(() => {
    if (!open) return;
    return api.on(ipcChannels.players.upsertProgress, (event) => {
      setProgress({
        percent: Math.max(0, Math.min(100, Math.round(event.percent))),
        detail: event.detail?.trim() ?? t('players.savingWait'),
      });
    });
  }, [api, open, t]);

  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key !== 'Escape' || saving) return;
      if (pendingDeletePlayer) {
        setPendingDeletePlayer(null);
        return;
      }
      if (pendingDeleteTeam) {
        setPendingDeleteTeam(null);
        return;
      }
      if (teamForm) {
        setTeamForm(null);
        return;
      }
      handleClose();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [handleClose, open, pendingDeletePlayer, pendingDeleteTeam, saving, teamForm]);

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    return q ? players.filter((p) => p.name.toLowerCase().includes(q)) : players;
  }, [players, query]);

  const beginAdd = () => {
    setAdding(true);
    setSelectedId(null);
    setName('');
    setRingColor(DEFAULT_PLAYER_RING_COLOR);
    setImageOffset({ ...DEFAULT_PLAYER_IMAGE_OFFSET });
    setImageScale(DEFAULT_PLAYER_IMAGE_SCALE);
    setFilePath(null);
    setPreviewUrl(null);
  };

  const setPreviewFromPathAndUrl = (path: string, url: string) => {
    setFilePath(path);
    setPreviewUrl((prev) => {
      if (prev?.startsWith('blob:')) URL.revokeObjectURL(prev);
      return url || null;
    });
  };

  const pickImage = async () => {
    const result = await api.invoke(ipcChannels.players.pickImage, {});
    if (result.canceled) return;
    setPreviewFromPathAndUrl(result.filePath, result.previewDataUrl);
  };

  const applyDroppedImage = (path: string, previewFromFile: string) => {
    if (!adding && !selected) beginAdd();
    setPreviewFromPathAndUrl(path, previewFromFile);
  };

  const drop = useFileDropZone({
    disabled: saving,
    filterPaths: filterMaterialImagePaths,
    onDropPaths: (paths) => {
      const picked = pickFirstMaterialImagePath(paths);
      if (!picked) return;
      applyDroppedImage(picked, '');
    },
  });

  const appearanceLatestRef = useRef({
    selected,
    adding,
    saving,
    ringColor,
    imageOffset,
    imageScale,
  });
  appearanceLatestRef.current = { selected, adding, saving, ringColor, imageOffset, imageScale };

  const persistAppearance = (patch: {
    ringColor?: string;
    imageOffset?: typeof imageOffset;
    imageScale?: number;
  }) => {
    const snap = appearanceLatestRef.current;
    if (!snap.selected || snap.adding || snap.saving) return;
    const payload = {
      id: snap.selected.id,
      teamId: snap.selected.teamId,
      name: snap.selected.name,
      ringColor: patch.ringColor ?? snap.ringColor,
      imageOffset: patch.imageOffset ?? snap.imageOffset,
      imageScale: patch.imageScale ?? snap.imageScale,
    };
    if (appearanceTimerRef.current) clearTimeout(appearanceTimerRef.current);
    appearanceTimerRef.current = setTimeout(() => {
      appearanceTimerRef.current = null;
      void api.invoke(ipcChannels.players.upsert, payload);
    }, 180);
  };

  useEffect(() => {
    return () => {
      if (appearanceTimerRef.current) clearTimeout(appearanceTimerRef.current);
    };
  }, []);

  const save = async () => {
    const trimmed = name.trim();
    if (!trimmed || (selected === null && filePath === null)) return;
    if (appearanceTimerRef.current) {
      clearTimeout(appearanceTimerRef.current);
      appearanceTimerRef.current = null;
    }
    flushSync(() => {
      setSaving(true);
      setProgress({ percent: 0, detail: t('players.savingWait') });
    });
    try {
      const result = await api.invoke(ipcChannels.players.upsert, {
        ...(selected ? { id: selected.id, teamId: selected.teamId } : {}),
        name: trimmed,
        ...(filePath ? { filePath } : {}),
        ringColor,
        imageOffset,
        imageScale,
      });
      setAdding(false);
      setSelectedId(result.player.id);
    } finally {
      setSaving(false);
    }
  };

  if (!open) return null;
  const activeUrl = previewUrl ?? selectedUrl;

  return createPortal(
    <>
      <div className={styles.modalBackdrop} aria-hidden />
      <div
        role="dialog"
        aria-modal="true"
        data-testid="players-modal"
        className={[styles.modalDialog, playerStyles.dialog].join(' ')}
      >
        <div className={styles.modalHeader}>
          <div className={styles.modalTitle}>{t('players.managerTitle')}</div>
          <button
            type="button"
            aria-label={t('common.close')}
            onClick={handleClose}
            className={styles.modalClose}
            disabled={saving}
          >
            ×
          </button>
        </div>
        <div className={playerStyles.body}>
          <aside className={playerStyles.sidebar}>
            <Input value={query} onChange={setQuery} placeholder={t('players.search')} />
            <div className={playerStyles.sidebarActions}>
              <Button variant="primary" data-testid="players-add" onClick={beginAdd}>
                {t('players.add')}
              </Button>
              <Button
                data-testid="players-add-team"
                onClick={() =>
                  setTeamForm({
                    id: null,
                    name: '',
                    color: DEFAULT_PLAYER_RING_COLOR,
                  })
                }
              >
                {t('players.addTeam')}
              </Button>
            </div>
            {teamForm ? (
              <div className={playerStyles.teamForm} data-testid="players-team-form">
                <label className={playerStyles.teamFormField}>
                  <span>{t('players.teamName')}</span>
                  <Input
                    value={teamForm.name}
                    onChange={(v) => setTeamForm((prev) => (prev ? { ...prev, name: v } : prev))}
                    placeholder={t('players.teamName')}
                    autoFocus
                  />
                </label>
                <label className={playerStyles.teamFormField}>
                  <span>{t('players.teamColor')}</span>
                  <input
                    type="color"
                    value={teamForm.color}
                    onChange={(e) =>
                      setTeamForm((prev) =>
                        prev ? { ...prev, color: e.currentTarget.value } : prev,
                      )
                    }
                  />
                </label>
                <div className={playerStyles.teamFormActions}>
                  <Button
                    variant="primary"
                    disabled={!teamForm.name.trim()}
                    onClick={() => {
                      const trimmed = teamForm.name.trim();
                      if (!trimmed) return;
                      void api
                        .invoke(ipcChannels.players.upsertTeam, {
                          ...(teamForm.id ? { id: teamForm.id } : {}),
                          name: trimmed,
                          color: teamForm.color,
                        })
                        .then(() => setTeamForm(null));
                    }}
                  >
                    {t('common.save')}
                  </Button>
                  <Button onClick={() => setTeamForm(null)}>{t('common.cancel')}</Button>
                </div>
              </div>
            ) : null}
            <div className={playerStyles.list}>
              {teams.map((team) => (
                <TeamSection
                  key={team.id}
                  team={team}
                  players={filtered.filter((p) => p.teamId === team.id)}
                  selectedId={selectedId}
                  onSelect={(id) => {
                    setAdding(false);
                    setSelectedId(id);
                  }}
                  onAssign={(playerId, teamId) =>
                    void api.invoke(ipcChannels.players.assignTeam, { playerId, teamId })
                  }
                  onEdit={() =>
                    setTeamForm({
                      id: team.id,
                      name: team.name,
                      color: team.color,
                    })
                  }
                  onDelete={() => setPendingDeleteTeam(team)}
                />
              ))}
              <TeamSection
                team={null}
                players={filtered.filter((p) => p.teamId === null)}
                selectedId={selectedId}
                onSelect={(id) => {
                  setAdding(false);
                  setSelectedId(id);
                }}
                onAssign={(playerId, teamId) =>
                  void api.invoke(ipcChannels.players.assignTeam, { playerId, teamId })
                }
              />
            </div>
          </aside>
          <main
            className={[playerStyles.editor, drop.dragOver ? playerStyles.editorDropOver : '']
              .filter(Boolean)
              .join(' ')}
            onDragEnter={drop.onDragEnter}
            onDragLeave={drop.onDragLeave}
            onDragOver={drop.onDragOver}
            onDrop={(e) => {
              drop.onDrop(e);
              const entries = getDroppedFileEntries(e);
              const files = e.dataTransfer?.files;
              for (let i = 0; i < entries.length; i += 1) {
                const entry = entries[i]!;
                if (!pickFirstMaterialImagePath([entry.path])) continue;
                const file = files?.[i];
                applyDroppedImage(entry.path, file ? URL.createObjectURL(file) : '');
                return;
              }
            }}
          >
            {drop.dragOver ? <div className={styles.dropHintOverlay}>{t('players.dropHint')}</div> : null}
            {adding || selected ? (
              <>
                <PlayerTokenView
                  data-testid="player-token-preview"
                  name={name}
                  imageUrl={activeUrl}
                  ringColor={ringColor}
                  imageOffset={imageOffset}
                  imageScale={imageScale}
                  panEnabled
                  onImageOffsetChange={(next) => {
                    setImageOffset(next);
                    persistAppearance({ imageOffset: next });
                  }}
                  onImageScaleChange={(next) => {
                    setImageScale(next);
                    persistAppearance({ imageScale: next });
                  }}
                  sizePx={220}
                />
                <label className={playerStyles.field}>
                  <span>{t('players.name')}</span>
                  <Input value={name} onChange={setName} placeholder={t('players.namePlaceholder')} />
                </label>
                <label className={playerStyles.field}>
                  <span>{t('players.ringColor')}</span>
                  <input
                    type="color"
                    value={ringColor}
                    onChange={(e) => {
                      const next = e.currentTarget.value;
                      setRingColor(next);
                      persistAppearance({ ringColor: next });
                    }}
                  />
                </label>
                <div className={playerStyles.actions}>
                  <Button data-testid="players-choose-image" onClick={() => void pickImage()}>
                    {t('players.chooseImage')}
                  </Button>
                  <Button
                    variant="primary"
                    data-testid="players-save"
                    disabled={!name.trim() || (!selected && !filePath) || saving}
                    onClick={() => void save()}
                  >
                    {saving ? t('common.saving') : t('common.save')}
                  </Button>
                  {selected ? (
                    <Button onClick={() => setPendingDeletePlayer(selected)}>{t('common.delete')}</Button>
                  ) : null}
                </div>
              </>
            ) : (
              <div className={styles.muted}>{t('players.selectPrompt')}</div>
            )}
          </main>
        </div>
      </div>
      {pendingDeletePlayer
        ? createPortal(
            <>
              <button
                type="button"
                aria-label={t('common.close')}
                className={styles.modalBackdrop}
                onClick={() => setPendingDeletePlayer(null)}
              />
              <div
                role="dialog"
                aria-modal="true"
                data-testid="players-delete-confirm"
                className={styles.modalDialog}
              >
                <div className={styles.modalHeader}>
                  <div className={styles.modalTitle}>{t('players.deleteTitle')}</div>
                  <button
                    type="button"
                    aria-label={t('common.close')}
                    className={styles.modalClose}
                    onClick={() => setPendingDeletePlayer(null)}
                  >
                    ×
                  </button>
                </div>
                <div className={styles.muted}>
                  {t('players.deleteConfirm', { name: pendingDeletePlayer.name })}
                </div>
                <div className={styles.modalFooter}>
                  <Button onClick={() => setPendingDeletePlayer(null)}>{t('common.cancel')}</Button>
                  <Button
                    variant="primary"
                    onClick={() => {
                      const id = pendingDeletePlayer.id;
                      setPendingDeletePlayer(null);
                      void api.invoke(ipcChannels.players.delete, { id });
                    }}
                  >
                    {t('common.delete')}
                  </Button>
                </div>
              </div>
            </>,
            document.body,
          )
        : null}
      {pendingDeleteTeam
        ? createPortal(
            <>
              <button
                type="button"
                aria-label={t('common.close')}
                className={styles.modalBackdrop}
                onClick={() => setPendingDeleteTeam(null)}
              />
              <div role="dialog" aria-modal="true" className={styles.modalDialog}>
                <div className={styles.modalHeader}>
                  <div className={styles.modalTitle}>{t('players.deleteTeamTitle')}</div>
                  <button
                    type="button"
                    aria-label={t('common.close')}
                    className={styles.modalClose}
                    onClick={() => setPendingDeleteTeam(null)}
                  >
                    ×
                  </button>
                </div>
                <div className={styles.muted}>
                  {t('players.deleteTeamConfirm', { name: pendingDeleteTeam.name })}
                </div>
                <div className={styles.modalFooter}>
                  <Button onClick={() => setPendingDeleteTeam(null)}>{t('common.cancel')}</Button>
                  <Button
                    variant="primary"
                    onClick={() => {
                      const id = pendingDeleteTeam.id;
                      setPendingDeleteTeam(null);
                      void api.invoke(ipcChannels.players.deleteTeam, { id });
                    }}
                  >
                    {t('common.delete')}
                  </Button>
                </div>
              </div>
            </>,
            document.body,
          )
        : null}
      {saving ? (
        <div className={styles.progressOverlay} role="dialog" aria-busy data-testid="players-save-progress">
          <div className={styles.progressModal}>
            <div className={styles.progressTitle}>{t('players.savingTitle')}</div>
            <div className={styles.previewSpinner} aria-hidden />
            <div className={styles.progressBar}>
              <div className={styles.progressFill} style={{ width: `${String(progress.percent)}%` }} />
            </div>
            <div className={styles.progressMeta}>
              <div>{progress.detail}</div>
              <div>{progress.percent}%</div>
            </div>
          </div>
        </div>
      ) : null}
    </>,
    document.body,
  );
}
