import {
  type ChangeEvent,
  type CSSProperties,
  type ReactNode,
  useEffect,
  useMemo,
  useState,
} from 'react';
import { createPortal } from 'react-dom';

import { ROLE_TEMPLATE_CHARACTERS } from '../../data/characterPresets';
import {
  AnimationState,
  type Character,
  type CharacterAnimationConfig,
} from '../../types';
import { readFileAsDataUrl } from '../asset-studio/characterAssetWorkflowModel';
import {
  type CharacterAssetWorkflowCache,
  type CharacterVisualDraft,
  fetchCharacterWorkflowCache,
  saveCharacterWorkflowCache,
} from '../asset-studio/characterAssetWorkflowPersistence';
import { buildDefaultRolePromptBundle } from '../asset-studio/customWorldRolePromptDefaults';
import { buildProjectPixelStyleReferenceBoard } from '../asset-studio/projectPixelStyleReference';
import { useAuthUi } from '../auth/AuthUiContext';
import { CharacterAnimator } from '../CharacterAnimator';
import {
  CORE_ACTIONS,
  type CustomWorldAiActionConfig,
  type EditableCustomWorldRole,
} from './roleAssetStudioModel';
import { RpgCreationRoleAnimationSection } from './RpgCreationRoleAnimationSection';
import { RpgCreationRoleAssetStudioFooter } from './RpgCreationRoleAssetStudioFooter';
import { RpgCreationRoleVisualSection } from './RpgCreationRoleVisualSection';
import { useRoleAnimationWorkflow } from './useRoleAnimationWorkflow';
import { useRoleVisualCandidateWorkflow } from './useRoleVisualCandidateWorkflow';

const DEFAULT_ANIMATION_PLAYBACK_RATE = 0.75;
const MIN_ANIMATION_PLAYBACK_RATE = 0.25;
const MAX_ANIMATION_PLAYBACK_RATE = 1.5;

function clampAnimationPlaybackRate(value: number) {
  if (!Number.isFinite(value)) {
    return DEFAULT_ANIMATION_PLAYBACK_RATE;
  }

  return Math.min(
    MAX_ANIMATION_PLAYBACK_RATE,
    Math.max(MIN_ANIMATION_PLAYBACK_RATE, value),
  );
}

function buildDefaultAnimationPromptTextByKey(defaultText: string) {
  return CORE_ACTIONS.reduce<Partial<Record<AnimationState, string>>>(
    (result, action) => ({
      ...result,
      [action.animation]: defaultText,
    }),
    {},
  );
}

function pickCachedAnimationPromptTextByKey(
  cache: CharacterAssetWorkflowCache,
  fallbackText: string,
  preferFreshRoleText: boolean,
) {
  const fromCache = cache.animationPromptTextByKey ?? {};

  return CORE_ACTIONS.reduce<Partial<Record<AnimationState, string>>>(
    (result, action) => {
      const cachedText = fromCache[action.animation]?.trim();
      const legacyText = cache.animationPromptText?.trim();
      return {
        ...result,
        [action.animation]: preferFreshRoleText
          ? fallbackText
          : cachedText && !isLegacyGeneratedActionDescription(cachedText)
            ? cachedText
            : legacyText && !isLegacyGeneratedActionDescription(legacyText)
              ? legacyText
              : fallbackText,
      };
    },
    {},
  );
}

function roundAnimationFps(value: number) {
  return Math.round(value * 100) / 100;
}

function ModalShell({
  title,
  subtitle,
  onClose,
  disableClose = false,
  children,
}: {
  title: string;
  subtitle?: string;
  onClose: () => void;
  disableClose?: boolean;
  children: React.ReactNode;
}) {
  const authUi = useAuthUi();
  const platformThemeClass =
    authUi?.platformTheme === 'dark'
      ? 'platform-theme--dark'
      : 'platform-theme--light';

  return (
    <div
      className={`platform-overlay platform-theme ${platformThemeClass} fixed inset-0 z-[100] flex items-end justify-center p-0 backdrop-blur-sm sm:items-center sm:p-4`}
      onClick={
        disableClose
          ? undefined
          : (event) => {
              if (event.target === event.currentTarget) {
                onClose();
              }
            }
      }
    >
      <div
        className={`platform-modal-shell platform-role-studio platform-ui-shell platform-theme ${platformThemeClass} flex h-[92vh] w-full max-w-6xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.6)] sm:h-auto sm:max-h-[min(92vh,58rem)] sm:rounded-[1.75rem]`}
        onClick={(event) => event.stopPropagation()}
      >
        <div className="flex items-start justify-between gap-4 border-b border-white/10 px-4 py-4 sm:px-5">
          <div className="min-w-0">
            <div className="truncate text-sm font-semibold text-white">
              {title}
            </div>
            {subtitle ? (
              <div className="mt-1 text-xs leading-6 text-zinc-400">
                {subtitle}
              </div>
            ) : null}
          </div>
          <button
            type="button"
            onClick={onClose}
            disabled={disableClose}
            className={`rounded-full border border-white/10 bg-black/20 px-3 py-2 text-xs text-zinc-300 transition-colors hover:text-white ${disableClose ? 'cursor-not-allowed opacity-45' : ''}`}
          >
            关闭
          </button>
        </div>
        <div className="min-h-0 flex-1 overflow-y-auto p-4 sm:p-5">
          {children}
        </div>
      </div>
    </div>
  );
}

function PortalModalShell(props: {
  title: string;
  subtitle?: string;
  onClose: () => void;
  disableClose?: boolean;
  children: React.ReactNode;
}) {
  if (typeof document === 'undefined') {
    return null;
  }

  return createPortal(<ModalShell {...props} />, document.body);
}

function Section({
  title,
  children,
}: {
  title: string;
  children: React.ReactNode;
}) {
  return (
    <section className="space-y-4 rounded-3xl border border-white/10 bg-black/20 p-4">
      <div className="text-[11px] font-bold tracking-[0.18em] text-zinc-200">
        {title}
      </div>
      {children}
    </section>
  );
}

function Field({
  label,
  children,
}: {
  label: string;
  children: React.ReactNode;
}) {
  return (
    <label className="block">
      <div className="mb-2 text-[11px] font-bold tracking-[0.14em] text-zinc-300">
        {label}
      </div>
      {children}
    </label>
  );
}

function TextArea({
  value,
  onChange,
  rows = 4,
  placeholder,
  readOnly = false,
}: {
  value: string;
  onChange: (value: string) => void;
  rows?: number;
  placeholder?: string;
  readOnly?: boolean;
}) {
  return (
    <textarea
      rows={rows}
      value={value}
      placeholder={placeholder}
      readOnly={readOnly}
      onChange={(event) => onChange(event.target.value)}
      className="w-full rounded-2xl border border-white/10 bg-black/25 px-4 py-3 text-sm leading-7 text-zinc-100 outline-none transition-colors placeholder:text-zinc-500 focus:border-sky-300/35"
    />
  );
}

function StatusBadge({
  tone,
  children,
}: {
  tone: 'green' | 'amber' | 'zinc';
  children: ReactNode;
}) {
  const toneClassName = {
    green: 'border-emerald-400/30 bg-emerald-500/10 text-emerald-100',
    amber: 'border-amber-400/30 bg-amber-500/10 text-amber-100',
    zinc: 'border-white/10 bg-black/20 text-zinc-300',
  }[tone];

  return (
    <span
      className={`inline-flex rounded-full border px-2.5 py-1 text-[11px] ${toneClassName}`}
    >
      {children}
    </span>
  );
}

function ActionButton({
  icon,
  label,
  subLabel,
  onClick,
  disabled = false,
  tone = 'default',
}: {
  icon?: React.ReactNode;
  label: string;
  subLabel?: string;
  onClick: () => void;
  disabled?: boolean;
  tone?: 'default' | 'sky' | 'green';
}) {
  const toneClassName =
    tone === 'green'
      ? 'border-emerald-400/30 bg-emerald-500/10 text-emerald-100 hover:bg-emerald-500/20'
      : tone === 'sky'
        ? 'border-sky-300/22 bg-sky-500/12 text-sky-50 hover:border-sky-200/40 hover:text-white'
        : 'border-white/12 bg-black/20 text-zinc-200 hover:border-white/22 hover:text-white';

  return (
    <button
      type="button"
      onPointerDown={(event) => {
        event.stopPropagation();
      }}
      onMouseDown={(event) => {
        event.stopPropagation();
      }}
      onClick={(event) => {
        event.stopPropagation();
        onClick();
      }}
      disabled={disabled}
      className={`inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition-colors ${toneClassName} ${disabled ? 'cursor-not-allowed opacity-45' : ''}`}
    >
      {icon ?? null}
      <span className="flex flex-col items-start leading-tight">
        <span>{label}</span>
        {subLabel ? (
          <span className="text-[11px] font-medium opacity-70">{subLabel}</span>
        ) : null}
      </span>
    </button>
  );
}

function buildRoleCharacterBrief(
  role: EditableCustomWorldRole,
  templateLabel?: string,
) {
  return [
    `角色名称：${role.name}`,
    `角色头衔：${role.title}`,
    `世界身份：${role.role}`,
    role.description ? `角色描述：${role.description}` : '',
    role.backstory ? `角色背景：${role.backstory}` : '',
    role.personality ? `角色性格：${role.personality}` : '',
    role.motivation ? `角色动机：${role.motivation}` : '',
    role.combatStyle ? `战斗风格：${role.combatStyle}` : '',
    role.tags && role.tags.length > 0
      ? `角色标签：${role.tags.join('、')}`
      : '',
    templateLabel ? `参考模板：${templateLabel}` : '',
  ]
    .filter(Boolean)
    .join('\n');
}

function isLegacyGeneratedVisualDescription(value: string) {
  const normalized = value.trim();
  if (!normalized) {
    return false;
  }

  return [
    '2D 横版 RPG',
    '纯绿色绿幕',
    '2 到 2.5 头身',
    '深色粗轮廓',
    '身体整体朝右',
    '脚底完整可见',
  ].some((marker) => normalized.includes(marker));
}

function isLegacyGeneratedActionDescription(value: string) {
  const normalized = value.trim();
  if (!normalized) {
    return false;
  }

  return [
    '动作气质参考：',
    '发力起手明确',
    '收招利落',
    '动作表现偏向',
    '起手克制',
  ].some((marker) => normalized.includes(marker));
}

function mergeRole<T extends EditableCustomWorldRole>(
  role: T,
  patch: Partial<T>,
) {
  return {
    ...role,
    ...patch,
  };
}

function hasGeneratedAnimation(
  role: EditableCustomWorldRole,
  animation: AnimationState,
) {
  const entry = role.animationMap?.[animation] as
    | { basePath?: string; spriteSheetPath?: string }
    | undefined;

  return Boolean(entry?.basePath || entry?.spriteSheetPath);
}

function getAnimationPreviewFrameStyle(
  _config: CharacterAnimationConfig | null | undefined,
  targetSize: number,
) {
  return {
    width: `${targetSize}px`,
    height: `${targetSize}px`,
    maxWidth: '100%',
    maxHeight: '100%',
  } satisfies CSSProperties;
}

function getAnimationPreviewViewportStyle(size: number) {
  return {
    width: `${size}px`,
    height: `${size}px`,
    maxWidth: '100%',
  } satisfies CSSProperties;
}

function resolveAnimationPlaybackRate(
  actionConfig: CustomWorldAiActionConfig | undefined,
  animationConfig: CharacterAnimationConfig | null | undefined,
) {
  if (!actionConfig) {
    return DEFAULT_ANIMATION_PLAYBACK_RATE;
  }

  const configuredFps =
    typeof animationConfig?.fps === 'number' &&
    Number.isFinite(animationConfig.fps)
      ? animationConfig.fps
      : null;

  if (!configuredFps) {
    return DEFAULT_ANIMATION_PLAYBACK_RATE;
  }

  return clampAnimationPlaybackRate(configuredFps / actionConfig.fps);
}

function applyPlaybackRateToAnimationMap(params: {
  animationMap: Record<string, unknown> | undefined;
  animation: AnimationState;
  actionConfig: CustomWorldAiActionConfig;
  playbackRate: number;
}) {
  const { animationMap, animation, actionConfig, playbackRate } = params;
  if (!animationMap) {
    return animationMap;
  }

  const currentConfig = animationMap[animation];
  if (!currentConfig || typeof currentConfig !== 'object') {
    return animationMap;
  }

  const currentAnimationConfig = currentConfig as CharacterAnimationConfig;
  const nextFps = roundAnimationFps(
    Math.max(1, actionConfig.fps * clampAnimationPlaybackRate(playbackRate)),
  );
  const currentFps =
    typeof currentAnimationConfig.fps === 'number' &&
    Number.isFinite(currentAnimationConfig.fps)
      ? roundAnimationFps(currentAnimationConfig.fps)
      : null;

  if (currentFps === nextFps) {
    return animationMap;
  }

  return {
    ...animationMap,
    [animation]: {
      ...currentAnimationConfig,
      fps: nextFps,
    },
  };
}

function buildAnimationPreviewCharacter(params: {
  workingRole: EditableCustomWorldRole;
  selectedTemplate: (typeof ROLE_TEMPLATE_CHARACTERS)[number] | null;
}): Character | null {
  const { workingRole, selectedTemplate } = params;
  const portrait = workingRole.imageSrc || selectedTemplate?.portrait;
  if (!portrait) {
    return null;
  }

  return {
    id: workingRole.id,
    name: workingRole.name,
    title: workingRole.title,
    description: workingRole.description ?? '',
    backstory: workingRole.backstory ?? '',
    avatar: portrait,
    portrait,
    assetFolder: selectedTemplate?.assetFolder ?? 'custom-world',
    assetVariant: selectedTemplate?.assetVariant ?? 'generated',
    generatedVisualAssetId: workingRole.generatedVisualAssetId,
    generatedAnimationSetId: workingRole.generatedAnimationSetId,
    animationMap: {
      ...(selectedTemplate?.animationMap ?? {}),
      ...((workingRole.animationMap ?? {}) as Record<string, unknown>),
    },
    attributes: selectedTemplate?.attributes ?? {},
    personality: workingRole.personality ?? '',
    skills: selectedTemplate?.skills ?? [],
    adventureOpenings: selectedTemplate?.adventureOpenings ?? {},
  } as Character;
}

export interface RpgCreationRoleAssetStudioModalProps {
  role: EditableCustomWorldRole;
  roleKind: 'playable' | 'story';
  cacheScopeId?: string;
  onApply?: (nextRole: EditableCustomWorldRole) => void;
  onPublishSuccess?: (
    payload: {
      roleId: string;
      portraitPath: string;
      generatedVisualAssetId: string;
      generatedAnimationSetId?: string | null;
      animationMap?: Record<string, unknown> | null;
    },
    options?: {
      closeAfterSync?: boolean;
    },
  ) => void;
  onClose: () => void;
  syncBusy?: boolean;
  visualPointCost?: number;
  animationPointCost?: number;
}

export function RpgCreationRoleAssetStudioModal({
  role,
  roleKind,
  cacheScopeId,
  onApply,
  onPublishSuccess,
  onClose,
  syncBusy = false,
  visualPointCost = 20,
  animationPointCost = 60,
}: RpgCreationRoleAssetStudioModalProps) {
  const [workingRole, setWorkingRole] = useState<EditableCustomWorldRole>(role);
  const baseRole = useMemo<EditableCustomWorldRole>(
    () => ({
      id: role.id,
      name: role.name,
      title: role.title,
      role: role.role,
      visualDescription: role.visualDescription,
      actionDescription: role.actionDescription,
      sceneVisualDescription: role.sceneVisualDescription,
      description: role.description,
      backstory: role.backstory,
      personality: role.personality,
      motivation: role.motivation,
      combatStyle: role.combatStyle,
      tags: role.tags,
      imageSrc: role.imageSrc,
      generatedVisualAssetId: role.generatedVisualAssetId,
      generatedAnimationSetId: role.generatedAnimationSetId,
      animationMap: role.animationMap,
    }),
    [
      role.animationMap,
      role.backstory,
      role.combatStyle,
      role.description,
      role.generatedAnimationSetId,
      role.generatedVisualAssetId,
      role.id,
      role.imageSrc,
      role.actionDescription,
      role.motivation,
      role.name,
      role.personality,
      role.role,
      role.sceneVisualDescription,
      role.tags,
      role.title,
      role.visualDescription,
    ],
  );
  const initialPromptBundle = useMemo(
    () => buildDefaultRolePromptBundle(baseRole),
    [baseRole],
  );
  const [visualPromptText, setVisualPromptText] = useState(
    initialPromptBundle.visualPromptText,
  );
  const [referenceImageDataUrls, setReferenceImageDataUrls] = useState<
    string[]
  >([]);
  const [
    projectStyleReferenceBoardSource,
    setProjectStyleReferenceBoardSource,
  ] = useState('');
  const [visualDrafts, setVisualDrafts] = useState<CharacterVisualDraft[]>([]);
  const [selectedVisualDraftId, setSelectedVisualDraftId] = useState('');
  const [visualStatus, setVisualStatus] = useState<string | null>(null);
  const [isGeneratingVisuals, setIsGeneratingVisuals] = useState(false);
  const [isApplyingVisual, setIsApplyingVisual] = useState(false);

  const [selectedAnimation, setSelectedAnimation] = useState<AnimationState>(
    CORE_ACTIONS[0]?.animation ?? AnimationState.IDLE,
  );
  const [animationPromptTextByKey, setAnimationPromptTextByKey] = useState<
    Partial<Record<AnimationState, string>>
  >(() =>
    buildDefaultAnimationPromptTextByKey(
      initialPromptBundle.animationPromptText,
    ),
  );
  const [animationStatusByKey, setAnimationStatusByKey] = useState<
    Partial<Record<AnimationState, string | null>>
  >({});
  const [generatingAnimationMap, setGeneratingAnimationMap] = useState<
    Partial<Record<AnimationState, boolean>>
  >({});
  const [animationPreviewPlaybackRate, setAnimationPreviewPlaybackRate] =
    useState(DEFAULT_ANIMATION_PLAYBACK_RATE);
  const [saveStatus, setSaveStatus] = useState<string | null>(null);
  const [isSavingToRole, setIsSavingToRole] = useState(false);
  const [isHydratingCache, setIsHydratingCache] = useState(true);
  const { applyVisualDraftToRole, generateVisualCandidatesForRole } =
    useRoleVisualCandidateWorkflow();
  const { generateAnimationClipForRole, publishAnimationClipForRole } =
    useRoleAnimationWorkflow();

  const selectedTemplate =
    roleKind === 'playable' ? (ROLE_TEMPLATE_CHARACTERS[0] ?? null) : null;
  const characterBriefText = useMemo(
    () =>
      buildRoleCharacterBrief(
        workingRole,
        selectedTemplate
          ? `${selectedTemplate.name} / ${selectedTemplate.title}`
          : undefined,
      ),
    [workingRole, selectedTemplate],
  );
  const roleSnapshotKey = useMemo(
    () =>
      [
        role.id,
        role.name,
        role.title,
        role.role,
        role.imageSrc ?? '',
        role.generatedVisualAssetId ?? '',
        role.generatedAnimationSetId ?? '',
      ].join('||'),
    [
      role.generatedAnimationSetId,
      role.generatedVisualAssetId,
      role.id,
      role.imageSrc,
      role.name,
      role.role,
      role.title,
    ],
  );
  const selectedVisualDraft =
    visualDrafts.find((draft) => draft.id === selectedVisualDraftId) ?? null;
  const previewImageSrc =
    workingRole.imageSrc ??
    selectedVisualDraft?.imageSrc ??
    selectedTemplate?.portrait ??
    '';
  const hasGeneratedVisualPreview = Boolean(
    selectedVisualDraft?.imageSrc || workingRole.imageSrc,
  );
  const selectedActionConfig =
    CORE_ACTIONS.find((item) => item.animation === selectedAnimation) ??
    CORE_ACTIONS[0]!;
  const animationPromptText =
    animationPromptTextByKey[selectedAnimation] ??
    initialPromptBundle.animationPromptText;
  const previewCharacter = useMemo(
    () =>
      buildAnimationPreviewCharacter({
        workingRole,
        selectedTemplate,
      }),
    [selectedTemplate, workingRole],
  );
  const selectedAnimationConfig = previewCharacter?.animationMap?.[
    selectedAnimation
  ] as CharacterAnimationConfig | undefined;
  const selectedAnimationStatus =
    animationStatusByKey[selectedAnimation] ?? null;
  const isSelectedAnimationGenerating =
    generatingAnimationMap[selectedAnimation] === true;
  const hasAnyGeneratingAnimations = Object.values(generatingAnimationMap).some(
    (value) => value === true,
  );
  const isSelectedAnimationGenerated = hasGeneratedAnimation(
    workingRole,
    selectedAnimation,
  );
  const shouldUseSelectedAnimationPreview =
    Boolean(previewCharacter) &&
    (isSelectedAnimationGenerated ||
      selectedAnimation === AnimationState.IDLE ||
      selectedAnimation === AnimationState.DIE);
  const animationPreviewFrameStyle = useMemo(
    () => getAnimationPreviewFrameStyle(selectedAnimationConfig, 440),
    [selectedAnimationConfig],
  );
  const animationPreviewViewportStyle = useMemo(
    () => getAnimationPreviewViewportStyle(440),
    [],
  );
  const effectiveVisualReferenceImageDataUrls = useMemo(() => {
    if (!projectStyleReferenceBoardSource) {
      return referenceImageDataUrls;
    }

    if (referenceImageDataUrls.length >= 4) {
      return referenceImageDataUrls;
    }

    return [projectStyleReferenceBoardSource, ...referenceImageDataUrls].slice(
      0,
      4,
    );
  }, [projectStyleReferenceBoardSource, referenceImageDataUrls]);
  const visualSourceMode =
    referenceImageDataUrls.length > 0 ? 'image-to-image' : 'text-to-image';

  useEffect(() => {
    let cancelled = false;

    void buildProjectPixelStyleReferenceBoard()
      .then((nextBoardSource) => {
        if (!cancelled) {
          setProjectStyleReferenceBoardSource(nextBoardSource);
        }
      })
      .catch(() => undefined);

    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    let cancelled = false;
    setWorkingRole(baseRole);
    setVisualPromptText(initialPromptBundle.visualPromptText);
    setAnimationPromptTextByKey(
      buildDefaultAnimationPromptTextByKey(
        initialPromptBundle.animationPromptText,
      ),
    );
    setReferenceImageDataUrls([]);
    setVisualDrafts([]);
    setSelectedVisualDraftId('');
    setSelectedAnimation(CORE_ACTIONS[0]?.animation ?? AnimationState.IDLE);
    setVisualStatus(null);
    setAnimationStatusByKey({});
    setGeneratingAnimationMap({});
    setAnimationPreviewPlaybackRate(DEFAULT_ANIMATION_PLAYBACK_RATE);
    setSaveStatus(null);
    setIsHydratingCache(true);

    void fetchCharacterWorkflowCache(baseRole.id, cacheScopeId)
      .then((result) => {
        if (cancelled || !result.cache) {
          return;
        }

        const cache = result.cache;
        if (cacheScopeId && cache.cacheScopeId !== cacheScopeId) {
          return;
        }
        const nextRole = mergeRole(baseRole, {
          imageSrc: cache.imageSrc ?? baseRole.imageSrc,
          generatedVisualAssetId:
            cache.generatedVisualAssetId ?? baseRole.generatedVisualAssetId,
          generatedAnimationSetId:
            cache.generatedAnimationSetId ?? baseRole.generatedAnimationSetId,
          animationMap:
            (cache.animationMap as EditableCustomWorldRole['animationMap']) ??
            baseRole.animationMap,
        });
        setWorkingRole(nextRole);
        setVisualPromptText(
          !baseRole.visualDescription?.trim() &&
            cache.visualPromptText &&
            !isLegacyGeneratedVisualDescription(cache.visualPromptText)
            ? cache.visualPromptText
            : initialPromptBundle.visualPromptText,
        );
        setAnimationPromptTextByKey(
          pickCachedAnimationPromptTextByKey(
            cache,
            initialPromptBundle.animationPromptText,
            Boolean(baseRole.actionDescription?.trim()),
          ),
        );
        setVisualDrafts(cache.visualDrafts ?? []);
        setSelectedVisualDraftId(
          cache.selectedVisualDraftId || cache.visualDrafts?.[0]?.id || '',
        );
        setSelectedAnimation(
          CORE_ACTIONS.some(
            (item) => item.animation === cache.selectedAnimation,
          )
            ? (cache.selectedAnimation as AnimationState)
            : (CORE_ACTIONS[0]?.animation ?? AnimationState.IDLE),
        );
      })
      .catch(() => undefined)
      .finally(() => {
        if (!cancelled) {
          setIsHydratingCache(false);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [baseRole, cacheScopeId, initialPromptBundle, roleSnapshotKey]);

  useEffect(() => {
    if (isHydratingCache) {
      return;
    }

    const timer = window.setTimeout(() => {
      const payload: CharacterAssetWorkflowCache = {
        characterId: workingRole.id,
        cacheScopeId,
        visualPromptText,
        animationPromptText,
        animationPromptTextByKey,
        visualDrafts,
        selectedVisualDraftId,
        selectedAnimation,
        imageSrc: workingRole.imageSrc,
        generatedVisualAssetId: workingRole.generatedVisualAssetId,
        generatedAnimationSetId: workingRole.generatedAnimationSetId,
        animationMap: (workingRole.animationMap ?? null) as Record<
          string,
          unknown
        > | null,
      };
      void saveCharacterWorkflowCache(payload).catch(() => undefined);
    }, 350);

    return () => {
      window.clearTimeout(timer);
    };
  }, [
    animationPromptText,
    animationPromptTextByKey,
    isHydratingCache,
    selectedAnimation,
    selectedVisualDraftId,
    cacheScopeId,
    visualDrafts,
    visualPromptText,
    workingRole.animationMap,
    workingRole.generatedAnimationSetId,
    workingRole.generatedVisualAssetId,
    workingRole.id,
    workingRole.imageSrc,
  ]);

  useEffect(() => {
    setAnimationPreviewPlaybackRate(
      resolveAnimationPlaybackRate(
        selectedActionConfig,
        selectedAnimationConfig,
      ),
    );
  }, [selectedActionConfig, selectedAnimationConfig]);

  const confirmPointSpend = (params: {
    kindLabel: string;
    points: number;
    description: string;
  }) => {
    if (typeof window === 'undefined' || typeof window.confirm !== 'function') {
      return true;
    }

    return window.confirm(
      `${params.kindLabel}预计消耗 ${params.points} 叙世币。\n${params.description}`,
    );
  };

  const handleReferenceImageUpload = async (
    event: ChangeEvent<HTMLInputElement>,
  ) => {
    const fileList = event.target.files;
    if (!fileList || fileList.length === 0) {
      return;
    }

    const uploadedDataUrls = await Promise.all(
      Array.from(fileList)
        .slice(0, 4)
        .map((file) => readFileAsDataUrl(file)),
    );
    setReferenceImageDataUrls(uploadedDataUrls);
    setVisualStatus(`已载入 ${uploadedDataUrls.length} 张参考图。`);
    event.target.value = '';
  };

  const applyVisualDraftToWorkflow = async (draft: CharacterVisualDraft) => {
    setIsApplyingVisual(true);
    try {
      const result = await applyVisualDraftToRole({
        draft,
        promptText: visualPromptText,
        role: workingRole,
        sourceMode: visualSourceMode,
      });

      const nextRole = mergeRole(workingRole, {
        imageSrc: result.portraitPath,
        generatedVisualAssetId: result.assetId,
        generatedAnimationSetId: undefined,
        animationMap: undefined,
      });
      setWorkingRole(nextRole);
      setSelectedVisualDraftId(draft.id);
      setAnimationStatusByKey({});
      setGeneratingAnimationMap({});
      setSaveStatus(null);
      setVisualStatus('角色形象已更新，可继续生成动作。');
    } finally {
      setIsApplyingVisual(false);
    }
  };

  const handleGenerateVisuals = async () => {
    if (
      !confirmPointSpend({
        kindLabel: '角色形象生成',
        points: visualPointCost,
        description: '这次是角色形象草稿生成，不是最终发布。',
      })
    ) {
      return;
    }

    setIsGeneratingVisuals(true);
    setVisualStatus(null);

    try {
      const result = await generateVisualCandidatesForRole({
        promptText: visualPromptText,
        referenceImageDataUrls: effectiveVisualReferenceImageDataUrls,
        role: workingRole,
        sourceMode: visualSourceMode,
      });
      setVisualDrafts(result.drafts);
      if (result.drafts[0]) {
        await applyVisualDraftToWorkflow(result.drafts[0]);
        setVisualStatus('角色形象已生成，如不满意可直接重新生成。');
      } else {
        setSelectedVisualDraftId('');
        setVisualStatus('这次没有生成可用角色形象。');
      }
    } catch (error) {
      setVisualStatus(
        error instanceof Error ? error.message : '生成角色形象失败。',
      );
    } finally {
      setIsGeneratingVisuals(false);
    }
  };

  const handleGenerateAnimation = async () => {
    if (!selectedActionConfig) {
      return;
    }

    const actionConfig = selectedActionConfig;
    const requestedPlaybackRate = animationPreviewPlaybackRate;
    if (generatingAnimationMap[actionConfig.animation]) {
      return;
    }

    if (
      !confirmPointSpend({
        kindLabel: '动作草稿生成',
        points: animationPointCost,
        description: '这次是动作草稿试片，不是最终发布。',
      })
    ) {
      return;
    }

    setGeneratingAnimationMap((current) => ({
      ...current,
      [actionConfig.animation]: true,
    }));
    setAnimationStatusByKey((current) => ({
      ...current,
      [actionConfig.animation]: null,
    }));

    try {
      const clip = await generateAnimationClipForRole({
        actionConfig,
        animationPromptText,
        characterBriefText,
        role: workingRole,
      });
      const result = await publishAnimationClipForRole({
        actionConfig,
        clip,
        role: workingRole,
      });

      setWorkingRole((current) =>
        mergeRole(current, {
          generatedAnimationSetId: result.animationSetId,
          animationMap: applyPlaybackRateToAnimationMap({
            animationMap: {
              ...((current.animationMap ?? {}) as Record<string, unknown>),
              ...(result.animationMap as NonNullable<
                EditableCustomWorldRole['animationMap']
              >),
            },
            animation: actionConfig.animation,
            actionConfig,
            playbackRate: requestedPlaybackRate,
          }),
        }),
      );
      setSaveStatus(null);
      setAnimationStatusByKey((current) => ({
        ...current,
        [actionConfig.animation]: `${actionConfig.label} 动作已更新。`,
      }));
    } catch (error) {
      setAnimationStatusByKey((current) => ({
        ...current,
        [actionConfig.animation]:
          error instanceof Error ? error.message : '生成角色动作失败。',
      }));
    } finally {
      setGeneratingAnimationMap((current) => ({
        ...current,
        [actionConfig.animation]: false,
      }));
    }
  };

  const handleSaveToRole = async () => {
    if (!workingRole.imageSrc || !workingRole.generatedVisualAssetId) {
      setSaveStatus('请先生成角色形象后再保存。');
      return;
    }

    setIsSavingToRole(true);
    setSaveStatus(null);

    try {
      const nextRole = mergeRole(role, {
        imageSrc: workingRole.imageSrc,
        generatedVisualAssetId: workingRole.generatedVisualAssetId,
        generatedAnimationSetId: workingRole.generatedAnimationSetId,
        animationMap: workingRole.animationMap,
      });

      onApply?.(nextRole);
      if (onPublishSuccess) {
        onPublishSuccess(
          {
            roleId: role.id,
            portraitPath: nextRole.imageSrc ?? previewImageSrc,
            generatedVisualAssetId: nextRole.generatedVisualAssetId ?? '',
            generatedAnimationSetId: nextRole.generatedAnimationSetId ?? null,
            animationMap: (nextRole.animationMap ?? null) as Record<
              string,
              unknown
            > | null,
          },
          {
            closeAfterSync: true,
          },
        );
      } else {
        onClose();
      }
    } catch (error) {
      setSaveStatus(
        error instanceof Error ? error.message : '保存角色形象失败。',
      );
    } finally {
      setIsSavingToRole(false);
    }
  };

  return (
    <PortalModalShell
      title="AI角色形象生成"
      onClose={onClose}
      disableClose={
        isHydratingCache ||
        isGeneratingVisuals ||
        isApplyingVisual ||
        hasAnyGeneratingAnimations ||
        isSavingToRole ||
        syncBusy
      }
    >
      <div className="space-y-5">
        <RpgCreationRoleVisualSection
          ActionButton={ActionButton}
          Field={Field}
          Section={Section}
          TextArea={TextArea}
          handleReferenceImageUpload={handleReferenceImageUpload}
          hasGeneratedVisualPreview={hasGeneratedVisualPreview}
          isApplyingVisual={isApplyingVisual}
          isGeneratingVisuals={isGeneratingVisuals}
          previewImageSrc={previewImageSrc}
          referenceImageDataUrls={referenceImageDataUrls}
          selectedTemplatePortrait={selectedTemplate?.portrait}
          selectedTemplateName={selectedTemplate?.name}
          syncBusy={syncBusy}
          visualPointCost={visualPointCost}
          visualPromptText={visualPromptText}
          visualStatus={visualStatus}
          workingRoleName={workingRole.name}
          onClearReferenceImages={() => setReferenceImageDataUrls([])}
          onGenerateVisuals={() => {
            void handleGenerateVisuals();
          }}
          onVisualPromptChange={setVisualPromptText}
        />

        <RpgCreationRoleAnimationSection
          ActionButton={ActionButton}
          CharacterAnimator={CharacterAnimator}
          Field={Field}
          Section={Section}
          StatusBadge={StatusBadge}
          TextArea={TextArea}
          animationPreviewFrameStyle={animationPreviewFrameStyle}
          animationPreviewPlaybackRate={animationPreviewPlaybackRate}
          animationPreviewViewportStyle={animationPreviewViewportStyle}
          animationPromptText={animationPromptText}
          generatingAnimationMap={generatingAnimationMap}
          hasGeneratedAnimation={(animation) =>
            hasGeneratedAnimation(workingRole, animation)
          }
          isSelectedAnimationGenerating={isSelectedAnimationGenerating}
          previewCharacter={previewCharacter}
          previewImageSrc={previewImageSrc}
          selectedAnimation={selectedAnimation}
          selectedAnimationStatus={selectedAnimationStatus}
          shouldUseSelectedAnimationPreview={shouldUseSelectedAnimationPreview}
          syncBusy={syncBusy}
          animationPointCost={animationPointCost}
          workingRoleGeneratedVisualAssetId={workingRole.generatedVisualAssetId}
          workingRoleImageSrc={workingRole.imageSrc}
          workingRoleName={workingRole.name}
          onAnimationPromptChange={(value) => {
            setAnimationPromptTextByKey((current) => ({
              ...current,
              [selectedAnimation]: value,
            }));
          }}
          onGenerateAnimation={() => {
            void handleGenerateAnimation();
          }}
          onPlaybackRateChange={(value) => {
            const nextPlaybackRate = clampAnimationPlaybackRate(value);
            setAnimationPreviewPlaybackRate(nextPlaybackRate);
            setSaveStatus(null);
            setWorkingRole((current) => {
              const nextAnimationMap = applyPlaybackRateToAnimationMap({
                animationMap: current.animationMap as
                  | Record<string, unknown>
                  | undefined,
                animation: selectedAnimation,
                actionConfig: selectedActionConfig,
                playbackRate: nextPlaybackRate,
              });

              return nextAnimationMap === current.animationMap
                ? current
                : mergeRole(current, {
                    animationMap: nextAnimationMap,
                  });
            });
          }}
          onSelectAnimation={setSelectedAnimation}
        />

        <RpgCreationRoleAssetStudioFooter
          isSavingToRole={isSavingToRole}
          saveStatus={saveStatus}
          syncBusy={syncBusy}
          workingRoleGeneratedVisualAssetId={workingRole.generatedVisualAssetId}
          workingRoleImageSrc={workingRole.imageSrc}
          onSaveToRole={() => {
            void handleSaveToRole();
          }}
        />
      </div>
    </PortalModalShell>
  );
}
