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

import { ROLE_TEMPLATE_CHARACTERS } from '../data/characterPresets';
import {
  AnimationState,
  type Character,
} from '../types';
import {
  buildAnimationClipFromVideoSource,
  normalizeMasterVisualSourceToDataUrl,
  readFileAsDataUrl,
} from './asset-studio/characterAssetWorkflowModel';
import {
  type CharacterAnimationGenerationPayload,
  type CharacterAssetWorkflowCache,
  type CharacterVisualDraft,
  fetchCharacterWorkflowCache,
  generateCharacterAnimationDraft,
  generateCharacterPromptBundle,
  generateCharacterVisualCandidates,
  publishCharacterAnimationAssets,
  publishCharacterVisualAsset,
  saveCharacterWorkflowCache,
} from './asset-studio/characterAssetWorkflowPersistence';
import { buildDefaultRolePromptBundle } from './asset-studio/customWorldRolePromptDefaults';
import { CharacterAnimator } from './CharacterAnimator';

type EditableCustomWorldRole = {
  id: string;
  name: string;
  title: string;
  role: string;
  description?: string;
  backstory?: string;
  personality?: string;
  motivation?: string;
  combatStyle?: string;
  tags?: string[];
  templateCharacterId?: string;
  imageSrc?: string;
  generatedVisualAssetId?: string;
  generatedAnimationSetId?: string;
  animationMap?: Record<string, unknown>;
};

type CustomWorldAiActionConfig = {
  animation: AnimationState;
  label: string;
  templateId: string;
  fps: number;
  frameCount: number;
  durationSeconds: number;
  loop: boolean;
};

const CORE_ACTIONS: CustomWorldAiActionConfig[] = [
  {
    animation: AnimationState.IDLE,
    label: '待机',
    templateId: 'idle',
    fps: 8,
    frameCount: 8,
    durationSeconds: 4,
    loop: true,
  },
  {
    animation: AnimationState.RUN,
    label: '奔跑',
    templateId: 'run',
    fps: 12,
    frameCount: 8,
    durationSeconds: 4,
    loop: true,
  },
  {
    animation: AnimationState.ATTACK,
    label: '攻击',
    templateId: 'attack_slash',
    fps: 12,
    frameCount: 8,
    durationSeconds: 3,
    loop: false,
  },
  {
    animation: AnimationState.HURT,
    label: '受击',
    templateId: 'hurt',
    fps: 10,
    frameCount: 6,
    durationSeconds: 3,
    loop: false,
  },
  {
    animation: AnimationState.DIE,
    label: '死亡',
    templateId: 'die',
    fps: 8,
    frameCount: 8,
    durationSeconds: 4,
    loop: false,
  },
];

function ModalShell({
  title,
  subtitle,
  onClose,
  disableClose = false,
  children,
}: {
  title: string;
  subtitle?: string;
  onClose: () => void;
  disableClose?: boolean;
  children: React.ReactNode;
}) {
  return (
    <div
      className="fixed inset-0 z-[100] flex items-end justify-center bg-black/78 p-0 backdrop-blur-sm sm:items-center sm:p-4"
      onClick={
        disableClose
          ? undefined
          : (event) => {
              if (event.target === event.currentTarget) {
                onClose();
              }
            }
      }
    >
      <div
        className="flex h-[92vh] w-full max-w-6xl flex-col overflow-hidden rounded-t-[1.75rem] border border-white/10 bg-[linear-gradient(180deg,rgba(19,24,39,0.98),rgba(8,10,17,0.96))] 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 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 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 function CustomWorldRoleAssetStudioModal({
  role,
  roleKind,
  onApply,
  onPublishSuccess,
  onClose,
  syncBusy = false,
  visualPointCost = 20,
  animationPointCost = 60,
  priorityTier = roleKind === 'playable' ? 'hero' : 'featured',
}: {
  role: EditableCustomWorldRole;
  roleKind: 'playable' | 'story';
  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;
  priorityTier?: 'hero' | 'featured' | 'supporting';
}) {
  const [workingRole, setWorkingRole] = useState<EditableCustomWorldRole>(role);
  const baseRole = useMemo<EditableCustomWorldRole>(
    () => ({
      id: role.id,
      name: role.name,
      title: role.title,
      role: role.role,
      description: role.description,
      backstory: role.backstory,
      personality: role.personality,
      motivation: role.motivation,
      combatStyle: role.combatStyle,
      tags: role.tags,
      templateCharacterId: role.templateCharacterId,
      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.motivation,
      role.name,
      role.personality,
      role.role,
      role.tags,
      role.templateCharacterId,
      role.title,
    ],
  );
  const initialPromptBundle = useMemo(
    () => buildDefaultRolePromptBundle(baseRole),
    [baseRole],
  );
  const [visualPromptText, setVisualPromptText] = useState(
    initialPromptBundle.visualPromptText,
  );
  const [referenceImageDataUrls, setReferenceImageDataUrls] = useState<
    string[]
  >([]);
  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 [animationPromptText, setAnimationPromptText] = useState(
    initialPromptBundle.animationPromptText,
  );
  const [animationStatus, setAnimationStatus] = useState<string | null>(null);
  const [isGeneratingAnimations, setIsGeneratingAnimations] = useState(false);
  const [saveStatus, setSaveStatus] = useState<string | null>(null);
  const [isSavingToRole, setIsSavingToRole] = useState(false);
  const [isHydratingCache, setIsHydratingCache] = useState(true);

  const selectedTemplate =
    roleKind === 'playable' && workingRole.templateCharacterId
      ? ROLE_TEMPLATE_CHARACTERS.find(
          (character) => character.id === workingRole.templateCharacterId,
        ) ?? null
      : null;
  const characterBriefText = useMemo(
    () =>
      buildRoleCharacterBrief(
        workingRole,
        selectedTemplate
          ? `${selectedTemplate.name} / ${selectedTemplate.title}`
          : undefined,
      ),
    [workingRole, selectedTemplate],
  );
  const promptSeedKey = useMemo(
    () =>
      [
        roleKind,
        workingRole.name,
        workingRole.title,
        workingRole.role,
        workingRole.description ?? '',
        workingRole.backstory ?? '',
        workingRole.personality ?? '',
        workingRole.motivation ?? '',
        workingRole.combatStyle ?? '',
        (workingRole.tags ?? []).join('|'),
        selectedTemplate?.name ?? '',
        selectedTemplate?.title ?? '',
      ].join('||'),
    [
      roleKind,
      selectedTemplate?.name,
      selectedTemplate?.title,
      workingRole.name,
      workingRole.title,
      workingRole.role,
      workingRole.description,
      workingRole.backstory,
      workingRole.personality,
      workingRole.motivation,
      workingRole.combatStyle,
      workingRole.tags,
    ],
  );
  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 =
    selectedVisualDraft?.imageSrc ??
    workingRole.imageSrc ??
    selectedTemplate?.portrait ??
    '';
  const hasGeneratedVisualPreview = Boolean(
    selectedVisualDraft?.imageSrc || workingRole.imageSrc,
  );
  const selectedActionConfig =
    CORE_ACTIONS.find((item) => item.animation === selectedAnimation) ??
    CORE_ACTIONS[0];
  const previewCharacter = useMemo(
    () =>
      buildAnimationPreviewCharacter({
        workingRole,
        selectedTemplate,
      }),
    [selectedTemplate, workingRole],
  );
  const visualSourceMode =
    referenceImageDataUrls.length > 0 ? 'image-to-image' : 'text-to-image';

  useEffect(() => {
    let cancelled = false;
    setWorkingRole(baseRole);
    setVisualPromptText(initialPromptBundle.visualPromptText);
    setAnimationPromptText(initialPromptBundle.animationPromptText);
    setReferenceImageDataUrls([]);
    setVisualDrafts([]);
    setSelectedVisualDraftId('');
    setSelectedAnimation(CORE_ACTIONS[0]?.animation ?? AnimationState.IDLE);
    setVisualStatus(null);
    setAnimationStatus(null);
    setSaveStatus(null);
    setIsHydratingCache(true);

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

        const cache = result.cache;
        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(
          cache.visualPromptText || initialPromptBundle.visualPromptText,
        );
        setAnimationPromptText(
          cache.animationPromptText || initialPromptBundle.animationPromptText,
        );
        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,
    initialPromptBundle,
    roleSnapshotKey,
  ]);

  useEffect(() => {
    if (!characterBriefText.trim()) {
      return;
    }

    let cancelled = false;

    void generateCharacterPromptBundle({
      roleKind,
      characterName: workingRole.name,
      roleTitle: workingRole.title,
      roleLabel: workingRole.role,
      description: workingRole.description,
      backstory: workingRole.backstory,
      personality: workingRole.personality,
      motivation: workingRole.motivation,
      combatStyle: workingRole.combatStyle,
      tags: workingRole.tags,
      characterBriefText,
    })
      .then((result) => {
        if (cancelled) {
          return;
        }

        setVisualPromptText((current) =>
          current.trim() && current !== initialPromptBundle.visualPromptText
            ? current
            : result.visualPromptText,
        );
        setAnimationPromptText((current) =>
          current.trim() && current !== initialPromptBundle.animationPromptText
            ? current
            : result.animationPromptText,
        );
      })
      .catch(() => undefined);

    return () => {
      cancelled = true;
    };
  }, [
    characterBriefText,
    initialPromptBundle.animationPromptText,
    initialPromptBundle.visualPromptText,
    promptSeedKey,
    roleKind,
    workingRole.backstory,
    workingRole.combatStyle,
    workingRole.description,
    workingRole.motivation,
    workingRole.name,
    workingRole.personality,
    workingRole.role,
    workingRole.tags,
    workingRole.title,
  ]);

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

    const timer = window.setTimeout(() => {
      const payload: CharacterAssetWorkflowCache = {
        characterId: workingRole.id,
        visualPromptText,
        animationPromptText,
        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,
    isHydratingCache,
    selectedAnimation,
    selectedVisualDraftId,
    visualDrafts,
    visualPromptText,
    workingRole.animationMap,
    workingRole.generatedAnimationSetId,
    workingRole.generatedVisualAssetId,
    workingRole.id,
    workingRole.imageSrc,
  ]);

  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,
    draftList = visualDrafts,
  ) => {
    const normalizedVisual = await normalizeMasterVisualSourceToDataUrl(
      draft.imageSrc,
      {
        applyChromaKey: true,
      },
    );
    const result = await publishCharacterVisualAsset({
      characterId: workingRole.id,
      sourceMode: visualSourceMode,
      promptText: visualPromptText,
      selectedPreviewSource: normalizedVisual.dataUrl,
      previewSources: [normalizedVisual.dataUrl],
      width: normalizedVisual.width,
      height: normalizedVisual.height,
      updateCharacterOverride: false,
    });

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

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

    setIsGeneratingVisuals(true);
    setVisualStatus(null);

    try {
      const result = await generateCharacterVisualCandidates({
        characterId: workingRole.id,
        sourceMode: visualSourceMode,
        promptText: visualPromptText,
        characterBriefText,
        referenceImageDataUrls: referenceImageDataUrls,
        candidateCount: 1,
        imageModel: 'wan2.7-image-pro',
        size: '1024*1024',
      });
      setVisualDrafts(result.drafts);
      if (result.drafts[0]) {
        await applyVisualDraftToWorkflow(result.drafts[0], result.drafts);
        setVisualStatus('角色形象已生成，如不满意可直接重新生成。');
      } else {
        setSelectedVisualDraftId('');
        setVisualStatus('这次没有生成可用角色形象。');
      }
    } catch (error) {
      setVisualStatus(
        error instanceof Error ? error.message : '生成角色形象失败。',
      );
    } finally {
      setIsGeneratingVisuals(false);
    }
  };

  const generateActionClip = async (config: CustomWorldAiActionConfig) => {
    if (!workingRole.imageSrc || !workingRole.generatedVisualAssetId) {
      throw new Error('请先生成角色形象，再生成动作。');
    }

    const result = await generateCharacterAnimationDraft({
      characterId: workingRole.id,
      strategy: 'image-to-video',
      animation: config.animation,
      promptText: animationPromptText,
      characterBriefText,
      actionTemplateId: config.templateId,
      visualSource: workingRole.imageSrc,
      referenceImageDataUrls: [],
      referenceVideoDataUrls: [],
      frameCount: config.frameCount,
      fps: config.fps,
      durationSeconds: config.durationSeconds,
      loop: config.loop,
      useChromaKey: true,
      resolution: '720P',
      imageSequenceModel: 'wan2.7-image-pro',
      videoModel: 'wan2.7-i2v',
      referenceVideoModel: 'wan2.7-r2v',
      motionTransferModel: 'wan2.2-animate-move',
    } satisfies CharacterAnimationGenerationPayload);

    if (result.strategy !== 'image-to-video') {
      throw new Error('当前自定义世界动作工坊只支持图生视频方案。');
    }

    return buildAnimationClipFromVideoSource(result.previewVideoPath, {
      animation: config.animation,
      fps: config.fps,
      loop: config.loop,
      frameCount: config.frameCount,
      applyChromaKey: true,
    });
  };

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

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

    setIsGeneratingAnimations(true);
    setAnimationStatus(null);

    try {
      const clip = await generateActionClip(selectedActionConfig);
      const result = await publishCharacterAnimationAssets({
        characterId: workingRole.id,
        visualAssetId: workingRole.generatedVisualAssetId!,
        animations: {
          [selectedActionConfig.animation]: {
            framesDataUrls: clip.frames,
            fps: clip.fps,
            loop: clip.loop,
            frameWidth: clip.frameWidth,
            frameHeight: clip.frameHeight,
            previewVideoPath: clip.previewVideoPath,
          },
        },
        updateCharacterOverride: false,
      });

      const nextRole = mergeRole(workingRole, {
        generatedAnimationSetId: result.animationSetId,
        animationMap: {
          ...((workingRole.animationMap ?? {}) as Record<string, unknown>),
          ...(result.animationMap as NonNullable<
            EditableCustomWorldRole['animationMap']
          >),
        },
      });
      setWorkingRole(nextRole);
      setSaveStatus(null);
      setAnimationStatus(`${selectedActionConfig.label} 动作已更新。`);
    } catch (error) {
      setAnimationStatus(
        error instanceof Error ? error.message : '生成角色动作失败。',
      );
    } finally {
      setIsGeneratingAnimations(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 ||
        isGeneratingAnimations ||
        isSavingToRole ||
        syncBusy
      }
    >
      <div className="space-y-5">
        <Section title="角色形象">
          <div className="space-y-4">
            <div className="overflow-hidden rounded-3xl border border-white/10 bg-[radial-gradient(circle_at_top,rgba(56,189,248,0.16),transparent_48%),linear-gradient(180deg,rgba(19,24,39,0.96),rgba(8,10,17,0.92))]">
              <div className="flex min-h-[18rem] items-center justify-center p-4 sm:min-h-[22rem]">
                {previewImageSrc ? (
                  <img
                    src={previewImageSrc}
                    alt={workingRole.name}
                    className="max-h-[28rem] w-full object-contain"
                  />
                ) : selectedTemplate ? (
                  <img
                    src={selectedTemplate.portrait}
                    alt={selectedTemplate.name}
                    className="max-h-[20rem] w-full object-contain"
                  />
                ) : (
                  <div className="px-6 text-center text-sm text-zinc-500">
                    暂无角色形象预览
                  </div>
                )}
              </div>
            </div>

            <Field label="形象提示词">
              <TextArea
                value={visualPromptText}
                onChange={setVisualPromptText}
                rows={6}
                placeholder="角色形象提示词会先按设定自动生成，也可以继续手动细化。"
              />
            </Field>

            <Field label="参考图">
              <label className="block rounded-2xl border border-dashed border-white/12 bg-black/20 px-4 py-4">
                <input
                  type="file"
                  accept="image/png,image/jpeg,image/webp"
                  multiple
                  onChange={handleReferenceImageUpload}
                  className="w-full text-xs text-zinc-300 file:mr-3 file:rounded-lg file:border-0 file:bg-sky-500 file:px-3 file:py-1.5 file:text-xs file:font-medium file:text-black"
                />
              </label>
              {referenceImageDataUrls.length > 0 ? (
                <div className="mt-3 space-y-3">
                  <div className="flex flex-wrap gap-2">
                    {referenceImageDataUrls.map((imageSrc, index) => (
                      <div
                        key={`${imageSrc}-${index}`}
                        className="h-16 w-16 overflow-hidden rounded-xl border border-white/10 bg-black/25"
                      >
                        <img
                          src={imageSrc}
                          alt={`reference-${index + 1}`}
                          className="h-full w-full object-cover"
                        />
                      </div>
                    ))}
                  </div>
                  <div>
                    <ActionButton
                      label="清空参考图"
                      onClick={() => setReferenceImageDataUrls([])}
                      disabled={isGeneratingVisuals || isApplyingVisual || syncBusy}
                    />
                  </div>
                </div>
              ) : null}
            </Field>

            <div className="flex flex-wrap gap-3">
              <ActionButton
                icon={
                  hasGeneratedVisualPreview ? (
                    <RefreshCcw className="h-4 w-4" />
                  ) : (
                    <ImagePlus className="h-4 w-4" />
                  )
                }
                label={
                  isGeneratingVisuals
                    ? '生成中...'
                    : hasGeneratedVisualPreview
                      ? '重新生成角色形象'
                      : '生成角色形象'
                }
                subLabel={`消耗${visualPointCost}叙世币`}
                onClick={() => void handleGenerateVisuals()}
                disabled={isGeneratingVisuals || isApplyingVisual || syncBusy}
                tone="sky"
              />
            </div>

            {visualStatus ? (
              <div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-zinc-300">
                {visualStatus}
              </div>
            ) : null}
          </div>
        </Section>

        <Section title="动作">
          <div className="space-y-4">
            <div className="rounded-3xl border border-white/10 bg-[linear-gradient(180deg,rgba(19,24,39,0.96),rgba(8,10,17,0.92))] p-4">
              <div className="flex min-h-[16rem] items-center justify-center rounded-2xl border border-white/10 bg-black/20 p-4">
                {previewCharacter && hasGeneratedAnimation(workingRole, selectedAnimation) ? (
                  <div className="h-[220px] w-[220px]">
                    <CharacterAnimator
                      state={selectedAnimation}
                      character={previewCharacter}
                      className="h-full w-full"
                    />
                  </div>
                ) : previewImageSrc ? (
                  <img
                    src={previewImageSrc}
                    alt={workingRole.name}
                    className="max-h-[16rem] w-full object-contain"
                  />
                ) : (
                  <div className="px-4 text-sm text-zinc-500">暂无动作预览</div>
                )}
              </div>
            </div>

            <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
              {CORE_ACTIONS.map((item) => {
                const isSelected = item.animation === selectedAnimation;
                const isReady = hasGeneratedAnimation(workingRole, item.animation);
                return (
                  <button
                    key={item.animation}
                    type="button"
                    onClick={() => setSelectedAnimation(item.animation)}
                    className={`rounded-2xl border px-3 py-3 text-left transition-colors ${
                      isSelected
                        ? 'border-sky-300/30 bg-sky-500/10'
                        : 'border-white/10 bg-black/20 hover:border-white/20'
                    }`}
                  >
                    <div className="flex items-start justify-between gap-2">
                      <div className="min-w-0">
                        <div className="text-sm font-semibold text-white">
                          {item.label}
                        </div>
                        <div className="mt-1 text-[11px] text-zinc-400">
                          {isSelected ? '当前预览' : '点击切换'}
                        </div>
                      </div>
                      <StatusBadge tone={isReady ? 'green' : 'zinc'}>
                        {isReady ? '已生成' : '待生成'}
                      </StatusBadge>
                    </div>
                  </button>
                );
              })}
            </div>

            <Field label="动作提示词">
              <TextArea
                value={animationPromptText}
                onChange={setAnimationPromptText}
                rows={5}
                placeholder="角色动作提示词会先按设定自动生成，也可以继续手动细化。"
              />
            </Field>

            <div className="flex flex-wrap gap-3">
              <ActionButton
                icon={<RefreshCcw className="h-4 w-4" />}
                label={isGeneratingAnimations ? '生成中...' : '生成动作'}
                subLabel={`消耗${animationPointCost}叙世币`}
                onClick={() => void handleGenerateAnimation()}
                disabled={
                  isGeneratingAnimations ||
                  !workingRole.imageSrc ||
                  !workingRole.generatedVisualAssetId ||
                  syncBusy
                }
                tone="sky"
              />
            </div>

            {animationStatus ? (
              <div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-zinc-300">
                {animationStatus}
              </div>
            ) : null}
          </div>
        </Section>

        <div className="sticky bottom-0 z-10 -mx-4 border-t border-white/10 bg-[linear-gradient(180deg,rgba(8,10,17,0.2)_0%,rgba(8,10,17,0.96)_28%)] px-4 pb-[calc(env(safe-area-inset-bottom,0px)+0.35rem)] pt-3 backdrop-blur sm:mx-0 sm:rounded-3xl sm:border sm:px-4">
          <div className="space-y-3">
            {saveStatus ? (
              <div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-zinc-300">
                {saveStatus}
              </div>
            ) : null}
            <div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
              <button
                type="button"
                onClick={() => void handleSaveToRole()}
                disabled={
                  isSavingToRole ||
                  syncBusy ||
                  !workingRole.imageSrc ||
                  !workingRole.generatedVisualAssetId
                }
                className={`rounded-full border border-emerald-400/30 bg-emerald-500/10 px-5 py-2 text-sm font-semibold text-emerald-100 transition-colors hover:bg-emerald-500/20 ${
                  isSavingToRole ||
                  syncBusy ||
                  !workingRole.imageSrc ||
                  !workingRole.generatedVisualAssetId
                    ? 'cursor-not-allowed opacity-45'
                    : ''
                }`}
              >
                {isSavingToRole || syncBusy ? '保存中...' : '保存到当前角色'}
              </button>
            </div>
          </div>
        </div>
      </div>
    </PortalModalShell>
  );
}
