import {
  CopyOutlined,
  CodeOutlined,
  CheckCircleOutlined,
  CloseOutlined,
  DownOutlined,
  EyeOutlined,
  ExpandOutlined,
  LeftOutlined,
  LinkOutlined,
  LoadingOutlined,
  MessageOutlined,
  PaperClipOutlined,
  PlusOutlined,
  RightOutlined,
  ShareAltOutlined,
  UpOutlined,
} from '@ant-design/icons';
import { App, Button, Input, Spin, Typography } from 'antd';
import { useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent } from 'react';
import { StepperUI } from '../../../components/stepper';
import { InlineImage } from '../../../components/common/InlineImage';
import { MarkdownPreviewContent } from '../../../components/markdownPreview/MarkdownPreviewContent';
import { FullscreenPreviewModal, ZoomablePreviewSurface } from '../../../components/previewer';
import { ChatPreviewBody } from './ChatPreviewBody';
import { isMarkdownContentType, isMarkdownResourceUrl, normalizeChatResourceUrl } from './chatResourceUrl';
import { sharePreviewLink } from './chatUtils';
import { openChatExternalLink } from './linkNavigation';
import { classifyPreviewKind } from './previewKind';
import { isPromptResolved } from './promptState';
import type { ChatComposerAttachment, ChatMessagePart, ChatPromptContextRef } from './types';

const { Paragraph, Text } = Typography;

type PromptTarget = Extract<ChatMessagePart, { type: 'prompt' }>;
type PromptOption = PromptTarget['options'][number];
type PromptPreview = NonNullable<PromptOption['preview']>;
type PromptStep = {
  key: string;
  title: string;
  description?: string | null;
  submitLabel?: string | null;
  mode?: 'queue' | 'direct' | null;
  multiple?: boolean;
  optional?: boolean;
  responseTemplate?: string | null;
  freeTextLabel?: string | null;
  freeTextPlaceholder?: string | null;
  selectedValues?: string[];
  options: PromptOption[];
};

export type PromptStepDraftSelection = {
  stepKey: string;
  stepTitle: string;
  selectedValues: string[];
  freeText: string;
  skipped?: boolean;
};

export type PromptDraftSelection = {
  selectedValues: string[];
  freeText: string;
  stepSelections?: PromptStepDraftSelection[];
  summaryText?: string | null;
  attachments?: ChatComposerAttachment[];
};

export type PromptSubmitPayload = {
  text: string;
  mode: 'queue' | 'direct';
  selection: PromptDraftSelection;
  promptTitle: string;
  promptSignature: string;
  contextRef?: ChatPromptContextRef | null;
  sourceMessageId?: number;
  attachments?: ChatComposerAttachment[];
};

const PROMPT_FREE_TEXT_ONLY_LABEL = '선택 없이 기타 요청';
const PROMPT_PARENT_QUESTION_MAX_LENGTH = 400;
const PROMPT_PARENT_QUESTION_PREVIEW_MAX_LENGTH = 140;
const PROMPT_PREVIEW_CONTENT_CACHE = new Map<string, { content: string | null; contentType: string | null }>();

function buildOptionSelectionText(options: PromptOption[], selectedValues: string[]) {
  const selectedOptions = options.filter((option) => selectedValues.includes(option.value));

  return selectedOptions
    .map((option) => (option.label === option.value ? option.label : `${option.label} (${option.value})`))
    .join(', ');
}

function buildSelectionText(target: PromptTarget, selectedValues: string[]) {
  return buildOptionSelectionText(target.options, selectedValues);
}

export function buildPromptTargetSignature(target: PromptTarget) {
  const stepsSignature = (target.steps ?? [])
    .map((step) =>
      [
        step.key,
        step.title,
        step.description ?? '',
        step.multiple ? 'multi' : 'single',
        step.optional ? 'optional' : 'required',
        step.mode ?? '',
        step.freeTextLabel ?? '',
        step.freeTextPlaceholder ?? '',
        ...step.options.map((option) => [option.value, option.label, option.description ?? ''].join('~')),
      ].join('|'),
    )
    .join('||');
  const optionsSignature = target.options
    .map((option) => [option.value, option.label, option.description ?? ''].join('~'))
    .join('|');

  return [
    target.title,
    target.description ?? '',
    target.mode ?? '',
    target.multiple ? 'multi' : 'single',
    target.submitLabel ?? '',
    target.freeTextLabel ?? '',
    target.freeTextPlaceholder ?? '',
    target.responseTemplate ?? '',
    target.currentStepKey ?? '',
    optionsSignature,
    stepsSignature,
  ].join('::');
}

function buildFreeTextOnlySummaryText(freeText: string) {
  return freeText.trim() ? PROMPT_FREE_TEXT_ONLY_LABEL : '';
}

function buildAttachmentMessageBlock(attachments: ChatComposerAttachment[]) {
  if (attachments.length === 0) {
    return '';
  }

  return ['첨부 파일:', ...attachments.map((attachment) => `- ${attachment.name}: ${attachment.publicUrl || attachment.path}`)].join('\n');
}

function mergePromptComposerAttachments(previous: ChatComposerAttachment[], next: ChatComposerAttachment[]) {
  const seen = new Set(previous.map((item) => item.path));

  return [
    ...previous,
    ...next.filter((item) => {
      if (seen.has(item.path)) {
        return false;
      }

      seen.add(item.path);
      return true;
    }),
  ];
}

function resolvePromptPasteFiles(clipboardData: DataTransfer) {
  const clipboardItemFiles = Array.from(clipboardData.items ?? [])
    .filter((item) => item.kind === 'file')
    .map((item) => item.getAsFile())
    .filter((file): file is File => Boolean(file) && file.size > 0);
  const files = Array.from(clipboardData.files ?? []).filter((file) => file.size > 0);
  const candidateFiles = clipboardItemFiles.length > 0 ? clipboardItemFiles : files;

  if (candidateFiles.length > 0) {
    return candidateFiles;
  }

  return [];
}

function normalizePromptContextText(value: string | null | undefined, maxLength?: number) {
  const normalized = String(value ?? '').replace(/\s+/g, ' ').trim();

  if (!normalized) {
    return '';
  }

  if (!maxLength || normalized.length <= maxLength) {
    return normalized;
  }

  return `${normalized.slice(0, maxLength).trimEnd()}...`;
}

export function buildPromptContextRef(target: PromptTarget, parentQuestionText?: string | null): ChatPromptContextRef | null {
  const promptTitle = target.title.trim();
  const promptDescription = target.description?.trim() || '';
  const normalizedParentQuestion = normalizePromptContextText(parentQuestionText, PROMPT_PARENT_QUESTION_MAX_LENGTH);

  if (!promptTitle) {
    return null;
  }

  return {
    key: 'prompt_parent_question',
    promptTitle,
    promptDescription: promptDescription || null,
    parentQuestionText: normalizedParentQuestion || null,
  };
}

function normalizePromptSteps(target: PromptTarget): PromptStep[] {
  if (Array.isArray(target.steps) && target.steps.length > 0) {
    return target.steps.map((step, index) => ({
      key: step.key || `step-${index + 1}`,
      title: step.title,
      description: step.description ?? null,
      submitLabel: step.submitLabel ?? null,
      mode: step.mode ?? target.mode ?? null,
      multiple: step.multiple === true,
      optional: step.optional === true,
      responseTemplate: step.responseTemplate ?? null,
      freeTextLabel: step.freeTextLabel ?? null,
      freeTextPlaceholder: step.freeTextPlaceholder ?? null,
      selectedValues: step.selectedValues ?? [],
      options: step.options,
    }));
  }

  return [
    {
      key: 'default',
      title: target.title,
      description: target.description ?? null,
      submitLabel: target.submitLabel ?? null,
      mode: target.mode ?? null,
      multiple: target.multiple === true,
      optional: false,
      responseTemplate: target.responseTemplate ?? null,
      freeTextLabel: target.freeTextLabel ?? null,
      freeTextPlaceholder: target.freeTextPlaceholder ?? null,
      selectedValues: target.selectedValues ?? [],
      options: target.options,
    },
  ];
}

function buildStepSelectionText(step: PromptStep, selectedValues: string[]) {
  return buildOptionSelectionText(step.options, selectedValues);
}

function replacePromptTemplate(
  template: string,
  replacements: Record<string, string>,
) {
  return Object.entries(replacements).reduce(
    (resolved, [key, value]) => resolved.replaceAll(`{{${key}}}`, value),
    template,
  );
}

function normalizePromptTemplateKey(value: string) {
  const normalized = value.trim().replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
  return normalized || 'step';
}

function buildStepTemplateReplacements(steps: PromptStep[], stepSelections: PromptStepDraftSelection[]) {
  return stepSelections.reduce<Record<string, string>>((replacements, selection, index) => {
    const step = steps.find((candidate) => candidate.key === selection.stepKey) ?? null;
    const stepKey = normalizePromptTemplateKey(selection.stepKey || step?.key || `step_${index + 1}`);
    const stepTitle = selection.stepTitle || step?.title || `단계 ${index + 1}`;
    const selectedOptions = step?.options.filter((option) => selection.selectedValues.includes(option.value)) ?? [];
    const selectedValues = selection.selectedValues.join(', ');
    const selectedLabels = selectedOptions.map((option) => option.label).join(', ');
    const selectionText = step ? buildStepSelectionText(step, selection.selectedValues) : selectedValues;
    const trimmedFreeText = selection.freeText.trim();
    const summaryText = selection.skipped
      ? `${stepTitle}: 건너뜀`
      : `${stepTitle}: ${selectionText || '선택 없음'}${trimmedFreeText ? ` / 추가 요청: ${trimmedFreeText.replace(/\n+/g, ' ')}` : ''}`;

    replacements[`${stepKey}_title`] = stepTitle;
    replacements[`${stepKey}_value`] = selection.selectedValues[0] ?? '';
    replacements[`${stepKey}_values`] = selectedValues;
    replacements[`${stepKey}_label`] = selectedOptions[0]?.label ?? '';
    replacements[`${stepKey}_labels`] = selectedLabels;
    replacements[`${stepKey}_text`] = selectionText;
    replacements[`${stepKey}_summary`] = summaryText;
    replacements[`${stepKey}_free_text`] = trimmedFreeText;
    replacements[`${stepKey}_free_text_block`] = trimmedFreeText ? `추가 요청:\n${trimmedFreeText}` : '';
    return replacements;
  }, {});
}

function normalizePromptDraftSelection(
  target: PromptTarget,
  selectionOrSelectedValues: PromptDraftSelection | string[],
  freeText = '',
) {
  if (Array.isArray(selectionOrSelectedValues)) {
    return {
      selectedValues: selectionOrSelectedValues,
      freeText,
      stepSelections: undefined,
      summaryText: null,
      attachments: [],
    } satisfies PromptDraftSelection;
  }

  if (selectionOrSelectedValues.stepSelections && selectionOrSelectedValues.stepSelections.length > 0) {
    return selectionOrSelectedValues;
  }

  const steps = normalizePromptSteps(target);

  if (steps.length <= 1) {
    return selectionOrSelectedValues;
  }

  return {
    ...selectionOrSelectedValues,
    stepSelections: [
      {
        stepKey: steps[0]?.key ?? 'default',
        stepTitle: steps[0]?.title ?? target.title,
        selectedValues: selectionOrSelectedValues.selectedValues,
        freeText: selectionOrSelectedValues.freeText,
      },
    ],
  } satisfies PromptDraftSelection;
}

function buildPromptDraftSummaryText(target: PromptTarget, draft: PromptDraftSelection) {
  const steps = normalizePromptSteps(target);

  if (steps.length <= 1) {
    const selectionText = buildSelectionText(target, draft.selectedValues);
    if (selectionText) {
      return selectionText;
    }

    return buildFreeTextOnlySummaryText(draft.freeText);
  }

  const stepSelections = draft.stepSelections ?? [];
  const completedCount = stepSelections.filter(
    (selection) => selection.skipped || selection.selectedValues.length > 0 || selection.freeText.trim().length > 0,
  ).length;
  const totalCount = steps.length;
  const latestSelection = [...stepSelections].reverse().find(
    (selection) => selection.selectedValues.length > 0 || selection.freeText.trim().length > 0,
  );
  const latestLabel = latestSelection
    ? (() => {
        const selectionText = buildStepSelectionText(
          steps.find((step) => step.key === latestSelection.stepKey) ?? steps[0],
          latestSelection.selectedValues,
        );
        if (selectionText) {
          return selectionText;
        }

        return buildFreeTextOnlySummaryText(latestSelection.freeText);
      })()
    : '';

  return latestLabel ? `${completedCount}/${totalCount} 단계 · ${latestLabel}` : `${completedCount}/${totalCount} 단계 선택`;
}

export function buildPromptResponseText(
  target: PromptTarget,
  selectionOrSelectedValues: PromptDraftSelection | string[],
  freeText = '',
) {
  const steps = normalizePromptSteps(target);
  const draft = normalizePromptDraftSelection(target, selectionOrSelectedValues, freeText);

  if (steps.length <= 1) {
    const selectedOptions = target.options.filter((option) => draft.selectedValues.includes(option.value));
    const selectedLabels = selectedOptions.map((option) => option.label);
    const selectedValue = draft.selectedValues[0] ?? '';
    const selectedLabel = selectedLabels[0] ?? '';
    const selectionText = buildSelectionText(target, draft.selectedValues);
    const trimmedFreeText = draft.freeText.trim();
    const hasSelectedOption = selectionText.length > 0;
    const normalizedSelectionText = selectionText || buildFreeTextOnlySummaryText(trimmedFreeText);
    const template =
      target.responseTemplate?.trim() ||
      (hasSelectedOption
        ? `프롬프트 "${target.title}"에서 "${selectionText}" 항목을 선택했습니다. 이 선택을 기준으로 다음 단계를 이어서 진행해 주세요.`
        : `프롬프트 "${target.title}"에서 선택 없이 기타 요청을 남겼습니다. 이 요청을 기준으로 다음 단계를 이어서 진행해 주세요.`);

    const resolvedText = replacePromptTemplate(template, {
      prompt_title: target.title,
      selection_value: selectedValue,
      selection_values: draft.selectedValues.join(', '),
      selection_label: selectedLabel || normalizedSelectionText,
      selection_labels: selectedLabels.join(', ') || normalizedSelectionText,
      selection_text: normalizedSelectionText,
      custom_text: trimmedFreeText,
      custom_text_block: trimmedFreeText ? `추가 요청:\n${trimmedFreeText}` : '',
      step_summaries: normalizedSelectionText,
      step_count: normalizedSelectionText ? '1' : '0',
    });

    if (!trimmedFreeText || resolvedText.includes(trimmedFreeText)) {
      return resolvedText.trim();
    }

    return `${resolvedText.trim()}\n\n추가 요청:\n${trimmedFreeText}`;
  }

  const stepSelections = (draft.stepSelections ?? []).filter((selection) => {
    if (selection.skipped) {
      return true;
    }

    return selection.selectedValues.length > 0 || selection.freeText.trim().length > 0;
  });
  const aggregateSelectedValues = stepSelections.flatMap((selection) => selection.selectedValues);
  const aggregateSelectedLabels = stepSelections.flatMap((selection) => {
    const step = steps.find((candidate) => candidate.key === selection.stepKey);
    if (!step) {
      return [];
    }

    return step.options.filter((option) => selection.selectedValues.includes(option.value)).map((option) => option.label);
  });
  const stepSummaryLines = stepSelections.map((selection, index) => {
    const step = steps.find((candidate) => candidate.key === selection.stepKey);
    const stepTitle = selection.stepTitle || step?.title || `단계 ${index + 1}`;

    if (selection.skipped) {
      return `${index + 1}. ${stepTitle}: 건너뜀`;
    }

    const selectionText = step ? buildStepSelectionText(step, selection.selectedValues) : selection.selectedValues.join(', ');
    const freeTextSummary = selection.freeText.trim()
      ? `\n   추가 요청: ${selection.freeText.trim().replace(/\n+/g, ' ')}`
      : '';

    return `${index + 1}. ${stepTitle}: ${selectionText || '선택 없음'}${freeTextSummary}`;
  });
  const trimmedFreeText = draft.freeText.trim();
  const lastMeaningfulSelection = [...stepSelections].reverse().find(
    (selection) => selection.skipped || selection.selectedValues.length > 0 || selection.freeText.trim().length > 0,
  );
  const lastMeaningfulStep = lastMeaningfulSelection
    ? steps.find((candidate) => candidate.key === lastMeaningfulSelection.stepKey) ?? null
    : null;
  const template =
    lastMeaningfulStep?.responseTemplate?.trim() ||
    target.responseTemplate?.trim() ||
    `프롬프트 "${target.title}" 단계 선택을 정리했습니다.\n{{step_summaries}}\n{{custom_text_block}}`;
  const resolvedText = replacePromptTemplate(template, {
    prompt_title: target.title,
    selection_value: aggregateSelectedValues[0] ?? '',
    selection_values: aggregateSelectedValues.join(', '),
    selection_label: aggregateSelectedLabels[0] ?? '',
    selection_labels: aggregateSelectedLabels.join(', '),
    selection_text: stepSummaryLines.join('\n'),
    custom_text: trimmedFreeText,
    custom_text_block: trimmedFreeText ? `최종 요청:\n${trimmedFreeText}` : '',
    step_summaries: stepSummaryLines.join('\n'),
    step_count: String(stepSelections.length),
    ...buildStepTemplateReplacements(steps, stepSelections),
  }).trim();

  if (!trimmedFreeText || resolvedText.includes(trimmedFreeText)) {
    return resolvedText;
  }

  return `${resolvedText}\n\n최종 요청:\n${trimmedFreeText}`;
}

function buildHtmlFrameDocument(html: string, sourceUrl?: string | null) {
  const trimmed = html.trim();

  if (!trimmed) {
    return '<!doctype html><html><body></body></html>';
  }

  const baseHref = (() => {
    try {
      return sourceUrl ? new URL('.', sourceUrl).toString() : window.location.href;
    } catch {
      return typeof window !== 'undefined' ? window.location.href : 'about:blank';
    }
  })();
  const baseTag = `<base href="${baseHref}">`;

  if (/<head(\s|>)/i.test(trimmed)) {
    return trimmed.replace(/<head(\s*[^>]*)>/i, (match) => `${match}${baseTag}`);
  }

  if (/<html(\s|>)/i.test(trimmed)) {
    return trimmed.replace(/<html(\s*[^>]*)>/i, (match) => `${match}<head>${baseTag}</head>`);
  }

  return `<!doctype html><html><head>${baseTag}</head><body>${trimmed}</body></html>`;
}

function isHtmlLikeUrl(url: string) {
  try {
    const parsed = new URL(url, typeof window !== 'undefined' ? window.location.href : 'http://localhost');
    return /\.(html?)$/i.test(parsed.pathname);
  } catch {
    return false;
  }
}

function canShowHtmlPreviewActions(preview: PromptPreview | null | undefined) {
  if (!preview) {
    return false;
  }

  if (preview.type === 'editable' || preview.type === 'markdown' || preview.type === 'html') {
    return true;
  }

  if (preview.type !== 'resource') {
    return false;
  }

  if (preview.content?.trim()) {
    return (
      /<(?:!doctype\s+html|html|head|body|main|section|article|div)\b/i.test(preview.content) ||
      isMarkdownPromptPreview(preview)
    );
  }

  return Boolean(preview.url && isHtmlLikeUrl(preview.url));
}

function canRenderFramePreview(url: string) {
  if (typeof window === 'undefined') {
    return false;
  }

  try {
    const parsed = new URL(url, window.location.href);
    return parsed.origin === window.location.origin;
  } catch {
    return false;
  }
}

function shouldRenderAsHtmlDocument(preview: PromptPreview, remoteContentType: string | null, remoteContent: string | null) {
  if (preview.type === 'html') {
    return true;
  }

  const normalizedContentType = remoteContentType?.toLowerCase() ?? '';

  if (normalizedContentType.includes('text/html')) {
    return true;
  }

  if (preview.url && isHtmlLikeUrl(preview.url)) {
    return true;
  }

  return Boolean(remoteContent && /<(?:!doctype\s+html|html|head|body|main|section|article|div)\b/i.test(remoteContent));
}

function isAppRouteUrl(url: string) {
  if (typeof window === 'undefined') {
    return false;
  }

  try {
    const parsed = new URL(url, window.location.href);
    const pathname = parsed.pathname.toLowerCase();
    const hasKnownFileExtension = /\.[a-z0-9]+$/i.test(pathname);
    const isInternalResource =
      pathname.startsWith('/api/chat/resources/') || pathname.startsWith('/public/.codex_chat/') || pathname.startsWith('/.codex_chat/');
    return parsed.origin === window.location.origin && !hasKnownFileExtension && !isInternalResource;
  } catch {
    return false;
  }
}

function isHtmlFallbackPreview(preview: PromptPreview, htmlContent: string) {
  const normalizedPreview = htmlContent.trimStart().toLowerCase();

  if (!normalizedPreview) {
    return false;
  }

  if (preview.url && isAppRouteUrl(preview.url)) {
    return true;
  }

  return (
    (normalizedPreview.startsWith('<!doctype html') ||
      normalizedPreview.startsWith('<html') ||
      normalizedPreview.includes('<head') ||
      normalizedPreview.includes('<body')) &&
    normalizedPreview.includes('<div id="root"')
  );
}

function isTextLikeContentType(contentType: string | null) {
  if (!contentType) {
    return false;
  }

  const normalized = contentType.toLowerCase();
  return (
    normalized.startsWith('text/') ||
    normalized.includes('json') ||
    normalized.includes('xml') ||
    normalized.includes('javascript') ||
    normalized.includes('svg')
  );
}

function resolvePromptPreviewUrl(url?: string | null) {
  const normalized = String(url ?? '').trim();

  if (!normalized) {
    return '';
  }

  return normalizeChatResourceUrl(normalized);
}

function isMarkdownPromptPreview(preview: PromptPreview | null | undefined) {
  if (!preview) {
    return false;
  }

  if (preview.type === 'markdown') {
    return true;
  }

  if (preview.type !== 'resource') {
    return false;
  }

  if (preview.content?.trim()) {
    return /^#{1,3}\s|\n#\s|^\s*-\s|\[[^\]]+\]\([^)]+\)/m.test(preview.content);
  }

  return isMarkdownResourceUrl(resolvePromptPreviewUrl(preview.url) || preview.url);
}

function usePromptPreviewContent(preview: PromptPreview | null | undefined) {
  const [remoteContent, setRemoteContent] = useState<string | null>(preview?.content ?? null);
  const [remoteContentType, setRemoteContentType] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [loadError, setLoadError] = useState('');
  const normalizedPreviewUrl = resolvePromptPreviewUrl(preview?.url);
  const previewContent = preview?.content ?? null;
  const previewType = preview?.type ?? null;
  const hasPreview = Boolean(preview);

  useEffect(() => {
    setRemoteContent(previewContent);
    setRemoteContentType(null);
    setLoadError('');

    const shouldFetchTextPreview =
      previewType === 'markdown' || previewType === 'html' || previewType === 'editable';
    const shouldInspectResourcePreview =
      previewType === 'resource' && normalizedPreviewUrl && canRenderFramePreview(normalizedPreviewUrl);

    if (
      !hasPreview ||
      previewContent ||
      !normalizedPreviewUrl ||
      (!shouldFetchTextPreview && !shouldInspectResourcePreview)
    ) {
      setIsLoading(false);
      return;
    }

    const cachedPreview = PROMPT_PREVIEW_CONTENT_CACHE.get(`${previewType ?? 'unknown'}::${normalizedPreviewUrl}`);

    if (cachedPreview) {
      setRemoteContent(cachedPreview.content);
      setRemoteContentType(cachedPreview.contentType);
      setIsLoading(false);
      return;
    }

    const controller = new AbortController();
    let resolvedContentType: string | null = null;
    setIsLoading(true);

    void fetch(normalizedPreviewUrl, {
      credentials: 'include',
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          throw new Error(`${response.status} ${response.statusText}`.trim());
        }

        const contentType = response.headers.get('content-type');
        resolvedContentType = contentType;

        if (!controller.signal.aborted) {
          setRemoteContentType(contentType);
        }

        if (shouldInspectResourcePreview && !isTextLikeContentType(contentType)) {
          return null;
        }

        return response.text();
      })
      .then((text) => {
        if (!controller.signal.aborted) {
          PROMPT_PREVIEW_CONTENT_CACHE.set(`${previewType ?? 'unknown'}::${normalizedPreviewUrl}`, {
            content: text,
            contentType: resolvedContentType,
          });
          setRemoteContent(text);
        }
      })
      .catch((error: unknown) => {
        if (controller.signal.aborted) {
          return;
        }

        setLoadError(error instanceof Error ? error.message : 'preview를 불러오지 못했습니다.');
      })
      .finally(() => {
        if (!controller.signal.aborted) {
          setIsLoading(false);
        }
      });

    return () => controller.abort();
  }, [hasPreview, normalizedPreviewUrl, previewContent, previewType]);

  return { remoteContent, remoteContentType, isLoading, loadError };
}

export function PromptPreviewSurface({
  preview,
  compact = false,
  htmlMode = 'preview',
  sourceContent,
  onSourceContentChange,
}: {
  preview: PromptPreview;
  compact?: boolean;
  htmlMode?: 'preview' | 'source';
  sourceContent?: string;
  onSourceContentChange?: (content: string) => void;
}) {
  const { remoteContent, remoteContentType, isLoading, loadError } = usePromptPreviewContent(preview);
  const normalizedPreviewUrl = resolvePromptPreviewUrl(preview.url);
  const shouldRenderAsHtml = shouldRenderAsHtmlDocument(preview, remoteContentType, remoteContent);
  const htmlDocument = shouldRenderAsHtml ? buildHtmlFrameDocument(remoteContent || '', normalizedPreviewUrl || preview.url) : null;
  const defaultSourceContent = remoteContent ?? preview.content ?? '';
  const sourceValue = sourceContent ?? defaultSourceContent;
  const markdownValue = remoteContent ?? '표시할 markdown preview가 없습니다.';
  const [editableSourceContent, setEditableSourceContent] = useState(defaultSourceContent);

  useEffect(() => {
    if (!onSourceContentChange || htmlMode !== 'source') {
      return;
    }

    if (sourceContent === undefined || sourceContent.length === 0) {
      onSourceContentChange(defaultSourceContent);
    }
  }, [defaultSourceContent, htmlMode, onSourceContentChange, sourceContent]);

  useEffect(() => {
    setEditableSourceContent(defaultSourceContent);
  }, [defaultSourceContent]);

  if (preview.type === 'image' && normalizedPreviewUrl) {
    const imageNode = (
      <InlineImage
        src={normalizedPreviewUrl}
        alt={preview.alt?.trim() || preview.title?.trim() || 'prompt preview image'}
        className={`app-chat-prompt-card__preview-image${compact ? ' app-chat-prompt-card__preview-image--compact' : ''}`}
        fallbackText="이미지 preview를 불러오지 못했습니다."
      />
    );

    if (compact) {
      return imageNode;
    }

    return (
      <ZoomablePreviewSurface stageClassName="app-chat-prompt-card__preview-zoom-stage" contentClassName="app-chat-prompt-card__preview-zoom-content">
        {imageNode}
      </ZoomablePreviewSurface>
    );
  }

  if (isLoading) {
    return (
      <div className="app-chat-prompt-card__preview-placeholder app-chat-prompt-card__preview-placeholder--loading">
        <Spin indicator={<LoadingOutlined spin />} size="small" />
      </div>
    );
  }

  if (loadError) {
    return <div className="app-chat-prompt-card__preview-placeholder">{loadError}</div>;
  }

  if (preview.type === 'markdown') {
    if (htmlMode === 'source') {
      return (
        <div className="app-chat-prompt-card__preview-source">
          <Input.TextArea
            value={sourceValue}
            onChange={(event) => onSourceContentChange?.(event.currentTarget.value)}
            autoSize={false}
            className="app-chat-prompt-card__preview-source-editor"
            placeholder="표시할 markdown 소스가 없습니다."
          />
        </div>
      );
    }

    return (
      <div className="app-chat-prompt-card__preview-markdown">
        <MarkdownPreviewContent content={markdownValue} maxBlocks={compact ? 5 : undefined} />
      </div>
    );
  }

  if (preview.type === 'editable') {
    return (
      <div className="app-chat-prompt-card__preview-source">
        <Input.TextArea
          value={editableSourceContent}
          onChange={(event) => {
            const nextValue = event.currentTarget.value;
            setEditableSourceContent(nextValue);
            onSourceContentChange?.(nextValue);
          }}
          autoSize={false}
          className="app-chat-prompt-card__preview-source-editor"
          placeholder="편집 가능한 소스 블록입니다."
        />
      </div>
    );
  }

  if (preview.type === 'html') {
    const htmlContent = remoteContent || '';

    if (normalizedPreviewUrl && isAppRouteUrl(normalizedPreviewUrl)) {
      return (
        <div className="app-chat-prompt-card__preview-placeholder">
          앱 화면 경로는 prompt iframe으로 열지 않습니다. `/api/chat/resources/...html`, `/api/resource-manager/preview/...html`, `resource/...html` 같은 실제 HTML 리소스 URL을 사용해 주세요.
        </div>
      );
    }

    if (isHtmlFallbackPreview(preview, htmlContent)) {
      return (
        <div className="app-chat-prompt-card__preview-placeholder">
          현재 앱 fallback HTML이 반환되었습니다. `type:"html"`에는 실제 HTML 본문을 넣거나, `type:"resource"`에 `/api/chat/resources/...html`, `/api/resource-manager/preview/...html`, `resource/...html` 같은 정적 HTML 리소스를 연결해 주세요.
        </div>
      );
    }

    if (htmlMode === 'source') {
      return (
        <div className="app-chat-prompt-card__preview-source">
          <Input.TextArea
            value={sourceValue}
            onChange={(event) => onSourceContentChange?.(event.currentTarget.value)}
            autoSize={false}
            className="app-chat-prompt-card__preview-source-editor"
            placeholder="표시할 HTML 소스가 없습니다."
          />
        </div>
      );
    }

    const frameNode = (
      <iframe
        title={preview.title?.trim() || 'prompt html preview'}
        srcDoc={htmlDocument ?? buildHtmlFrameDocument(htmlContent, normalizedPreviewUrl || preview.url)}
        className="app-chat-prompt-card__preview-frame"
        sandbox="allow-same-origin"
      />
    );

    if (compact) {
      return frameNode;
    }

    return (
      <ZoomablePreviewSurface stageClassName="app-chat-prompt-card__preview-zoom-stage" contentClassName="app-chat-prompt-card__preview-zoom-content">
        {frameNode}
      </ZoomablePreviewSurface>
    );
  }

  if (preview.type === 'resource' && normalizedPreviewUrl) {
    const resourceKind =
      isMarkdownResourceUrl(normalizedPreviewUrl || preview.url) || isMarkdownContentType(remoteContentType)
        ? 'markdown'
        : classifyPreviewKind(normalizedPreviewUrl);

    if (isAppRouteUrl(normalizedPreviewUrl)) {
      return (
        <div className="app-chat-prompt-card__preview-placeholder">
          앱 화면 URL은 resource preview로 열지 않습니다. `/api/chat/resources/...html`, `/api/resource-manager/preview/...html`, `resource/...html` 같은 정적 리소스 경로만 사용해 주세요.
        </div>
      );
    }

    if (remoteContentType?.toLowerCase().includes('text/html') && isHtmlFallbackPreview(preview, remoteContent || '')) {
      return (
        <div className="app-chat-prompt-card__preview-placeholder">
          현재 앱 fallback HTML이 반환되었습니다. `type:"resource"`에는 실제 리소스(`/api/chat/resources/...html`, `/api/resource-manager/preview/...html`, `resource/...html`)만 연결해 주세요.
        </div>
      );
    }

    return (
      <ChatPreviewBody
        target={{
          label: preview.title?.trim() || preview.alt?.trim() || 'prompt resource preview',
          url: normalizedPreviewUrl,
          kind: resourceKind,
        }}
        previewText={remoteContent || ''}
        isPreviewLoading={isLoading}
        previewError={loadError}
        previewContentType={remoteContentType ?? undefined}
        maxMarkdownBlocks={compact ? 5 : undefined}
        renderHtmlAsFrame={htmlMode !== 'source'}
      />
    );
  }

  return <div className="app-chat-prompt-card__preview-placeholder">표시할 preview가 없습니다.</div>;
}

function PromptPreviewViewport({ children }: { children: ReactNode }) {
  return (
    <div className="app-chat-prompt-card__preview-scroll">
      <div className="app-chat-prompt-card__preview-scroll-inner">
        {children}
      </div>
    </div>
  );
}

function PromptPreviewCard({
  option,
  onOpenPreview,
  onPreviewViewed,
}: {
  option: PromptOption;
  onOpenPreview: (option: PromptOption) => void;
  onPreviewViewed?: (option: PromptOption) => void;
}) {
  const preview = option.preview;
  const { message } = App.useApp();
  const normalizedPreviewUrl = resolvePromptPreviewUrl(preview?.url);

  if (!preview) {
    return null;
  }

  useEffect(() => {
    onPreviewViewed?.(option);
  }, [normalizedPreviewUrl, onPreviewViewed, option.label, option.value]);

  const handleSharePreview = (event: ReactKeyboardEvent | ReactMouseEvent<HTMLElement>) => {
    event.stopPropagation();

    if (!normalizedPreviewUrl) {
      message.info('이 preview는 공유할 링크가 없습니다.');
      return;
    }

    onPreviewViewed?.(option);
    void sharePreviewLink({
      url: normalizedPreviewUrl,
      title: preview.title?.trim() || option.label,
    })
      .then((result) => {
        message.success(result === 'shared' ? 'preview 링크를 공유했습니다.' : 'preview 링크를 복사했습니다.');
      })
      .catch((error: unknown) => {
        if (error instanceof DOMException && error.name === 'AbortError') {
          return;
        }

        message.error(error instanceof Error ? error.message : 'preview 링크를 공유하지 못했습니다.');
      });
  };

  return (
    <div
      className="app-chat-prompt-card__preview-shell"
      onClick={(event) => event.stopPropagation()}
      onKeyDown={(event) => event.stopPropagation()}
    >
      <div className="app-chat-prompt-card__preview-toolbar">
        <Text className="app-chat-prompt-card__preview-title" ellipsis={{ tooltip: preview.title?.trim() || option.label }}>
          {preview.title?.trim() || option.label}
        </Text>
        <Button
          type="text"
          size="small"
          className="app-chat-prompt-card__preview-tool"
          icon={<ExpandOutlined />}
          aria-label={`${option.label} preview 확대`}
          onClick={(event) => {
            event.stopPropagation();
            onPreviewViewed?.(option);
            onOpenPreview(option);
          }}
        />
        <Button
          type="text"
          size="small"
          className="app-chat-prompt-card__preview-tool"
          icon={<ShareAltOutlined />}
          aria-label={`${option.label} preview 공유`}
          onClick={handleSharePreview}
        />
        {preview.url ? (
          <Button
            type="text"
            size="small"
            className="app-chat-prompt-card__preview-tool"
            icon={<LinkOutlined />}
            aria-label={`${option.label} preview 열기`}
            onClick={(event) => {
              event.stopPropagation();
              onPreviewViewed?.(option);
              openChatExternalLink(normalizedPreviewUrl || preview.url || '', event);
            }}
          />
        ) : (
          <Button
            type="text"
            size="small"
            className="app-chat-prompt-card__preview-tool"
            icon={<LinkOutlined />}
            aria-label={`${option.label} preview 링크 없음`}
            onClick={(event) => {
              event.stopPropagation();
              message.info('이 preview는 내부 본문만 있어 별도 링크가 없습니다.');
            }}
          />
        )}
      </div>
      <div className="app-chat-prompt-card__preview-body">
        <PromptPreviewViewport>
          <PromptPreviewSurface preview={preview} compact />
        </PromptPreviewViewport>
      </div>
    </div>
  );
}

function buildInitialStepSelectionMap(
  target: PromptTarget,
  steps: PromptStep[],
  draftSelection?: PromptDraftSelection | null,
) {
  const normalizedDraft = draftSelection ? normalizePromptDraftSelection(target, draftSelection) : null;
  const draftStepSelections = new Map((normalizedDraft?.stepSelections ?? []).map((selection) => [selection.stepKey, selection]));
  const topLevelSelectedValues = normalizedDraft?.selectedValues ?? [];
  const topLevelFreeText = normalizedDraft?.freeText ?? '';

  return Object.fromEntries(
    steps.map((step) => {
      const draftStepSelection = draftStepSelections.get(step.key);
      const selectedValues =
        draftStepSelection?.selectedValues ??
        (steps.length === 1 && step.key === 'default'
          ? topLevelSelectedValues.length > 0
            ? topLevelSelectedValues
            : target.selectedValues ?? step.selectedValues ?? []
          : step.selectedValues ?? []);
      const freeText =
        draftStepSelection?.freeText ??
        (steps.length === 1 && step.key === 'default'
          ? topLevelFreeText
          : '');
      const skipped = draftStepSelection?.skipped === true;

      return [
        step.key,
        {
          stepKey: step.key,
          stepTitle: step.title,
          selectedValues,
          freeText,
          skipped,
        } satisfies PromptStepDraftSelection,
      ];
    }),
  );
}

function resolveCurrentStepIndex(
  steps: PromptStep[],
  stepSelections: Record<string, PromptStepDraftSelection>,
  preferredStepKey?: string | null,
) {
  const preferredIndex = preferredStepKey ? steps.findIndex((step) => step.key === preferredStepKey) : -1;

  if (preferredIndex >= 0) {
    return preferredIndex;
  }

  const firstIncompleteIndex = steps.findIndex((step) => {
    const selection = stepSelections[step.key];
    return !selection || (!selection.skipped && selection.selectedValues.length === 0 && selection.freeText.trim().length === 0);
  });

  if (firstIncompleteIndex >= 0) {
    return firstIncompleteIndex;
  }

  return Math.max(steps.length - 1, 0);
}

function canProceedStep(step: PromptStep, selection: PromptStepDraftSelection | undefined) {
  if (!step) {
    return false;
  }

  if (step.optional && selection?.skipped) {
    return true;
  }

  return (selection?.selectedValues.length ?? 0) > 0 || (selection?.freeText.trim().length ?? 0) > 0;
}

function shouldExpandAllPromptPreviews(step: PromptStep | undefined, selection: PromptStepDraftSelection | undefined, isLocked: boolean) {
  if (!step || !isLocked) {
    return false;
  }

  const hasPreviewableOption = step.options.some((option) => option.preview);
  if (!hasPreviewableOption) {
    return false;
  }

  return (selection?.selectedValues.length ?? 0) === 0;
}

function buildPromptSelectionPayload(target: PromptTarget, stepSelections: Record<string, PromptStepDraftSelection>) {
  return buildPromptSelectionPayloadWithAttachments(target, stepSelections, []);
}

function buildPromptSelectionPayloadWithAttachments(
  target: PromptTarget,
  stepSelections: Record<string, PromptStepDraftSelection>,
  attachments: ChatComposerAttachment[],
) {
  const steps = normalizePromptSteps(target);
  const draftSelections = steps
    .map((step) => stepSelections[step.key])
    .filter(Boolean) as PromptStepDraftSelection[];
  const meaningfulSelections = draftSelections.filter((selection) => selection.skipped || selection.selectedValues.length > 0 || selection.freeText.trim());

  if (meaningfulSelections.length === 0) {
    return null;
  }

  const selectedValues = meaningfulSelections.flatMap((selection) => selection.selectedValues);
  const aggregatedFreeText = meaningfulSelections
    .map((selection) => selection.freeText.trim())
    .filter(Boolean)
    .join('\n\n');

  return {
    selectedValues,
    freeText: aggregatedFreeText,
    stepSelections: meaningfulSelections,
    summaryText: buildPromptDraftSummaryText(target, {
      selectedValues,
      freeText: aggregatedFreeText,
      stepSelections: meaningfulSelections,
    }),
    attachments,
  } satisfies PromptDraftSelection;
}

function buildPromptSelectionSeedSignature(target: PromptTarget, steps: PromptStep[]) {
  const topLevelSelectedValues = target.selectedValues ?? [];
  const stepSelectedValues = steps.map((step) => `${step.key}:${(step.selectedValues ?? []).join(',')}`).join('|');

  return [
    topLevelSelectedValues.join(','),
    stepSelectedValues,
    target.currentStepKey ?? '',
    target.readOnly === true ? 'readonly' : '',
    target.resolvedBy ?? '',
    target.resultText ?? '',
  ].join('::');
}

export function ChatPromptCard({
  target,
  onSubmit,
  readOnly = false,
  parentQuestionText,
  onSelectionChange,
  onSubmitted,
  submittedSelection,
  draftSelection,
  onPreviewViewed,
  defaultCollapsed = false,
  onSharePrompt,
  allowAttachments = false,
  attachmentAccept,
  onUploadAttachment,
  anchorRef,
}: {
  target: PromptTarget;
  onSubmit: (payload: PromptSubmitPayload) => Promise<boolean>;
  readOnly?: boolean;
  parentQuestionText?: string | null;
  onSelectionChange?: (selection: PromptDraftSelection | null) => void;
  onSubmitted?: (selection: PromptDraftSelection) => void;
  submittedSelection?: PromptDraftSelection | null;
  draftSelection?: PromptDraftSelection | null;
  onPreviewViewed?: (previewUrl: string | null) => void;
  defaultCollapsed?: boolean;
  onSharePrompt?: (() => void) | null;
  allowAttachments?: boolean;
  attachmentAccept?: string;
  onUploadAttachment?: ((file: File) => Promise<ChatComposerAttachment>) | null;
  anchorRef?: ((element: HTMLElement | null) => void) | null;
}) {
  const { message } = App.useApp();
  const steps = useMemo(() => normalizePromptSteps(target), [target]);
  const promptTargetSignature = useMemo(() => buildPromptTargetSignature(target), [target]);
  const promptSelectionSeedSignature = useMemo(
    () => buildPromptSelectionSeedSignature(target, steps),
    [steps, target],
  );
  const draftSelectionSeedSignature = useMemo(
    () =>
      draftSelection
        ? JSON.stringify({
            selectedValues: draftSelection.selectedValues,
            freeText: draftSelection.freeText,
            stepSelections: draftSelection.stepSelections,
            summaryText: draftSelection.summaryText ?? null,
          })
        : '',
    [draftSelection],
  );
  const promptResetKey = `${promptTargetSignature}::${promptSelectionSeedSignature}`;
  const hasStepper = steps.length > 1;
  const initialStepSelections = useMemo(
    () => buildInitialStepSelectionMap(target, steps, draftSelection),
    [draftSelectionSeedSignature, promptResetKey, steps, target, draftSelection],
  );
  const [stepSelections, setStepSelections] = useState<Record<string, PromptStepDraftSelection>>(initialStepSelections);
  const [activeStepIndex, setActiveStepIndex] = useState(() => resolveCurrentStepIndex(steps, initialStepSelections, target.currentStepKey));
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submittedSummary, setSubmittedSummary] = useState('');
  const [submittedFreeText, setSubmittedFreeText] = useState('');
  const [expandedOptionValue, setExpandedOptionValue] = useState<string | null>(null);
  const [expandedHtmlMode, setExpandedHtmlMode] = useState<'preview' | 'source'>('preview');
  const [expandedPreviewSource, setExpandedPreviewSource] = useState('');
  const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
  const [attachments, setAttachments] = useState<ChatComposerAttachment[]>([]);
  const [isUploadingAttachment, setIsUploadingAttachment] = useState(false);
  const attachmentInputRef = useRef<HTMLInputElement | null>(null);
  const resolvedSelectedValues = target.selectedValues ?? [];
  const resolvedSelectionSummary = buildSelectionText(target, resolvedSelectedValues);
  const isResolved = isPromptResolved(target);
  const externallySubmittedSummary =
    submittedSelection?.summaryText ||
    (submittedSelection ? buildPromptDraftSummaryText(target, submittedSelection) : '');
  const externalDraftSummary =
    draftSelection?.summaryText ||
    (draftSelection ? buildPromptDraftSummaryText(target, draftSelection) : '');
  const submittedFreeTextValue = submittedFreeText || submittedSelection?.freeText.trim() || '';
  const submittedAttachments = submittedSelection?.attachments ?? target.attachments ?? [];
  const displayedSubmittedSummary = submittedSummary || externallySubmittedSummary;
  const displayedParentQuestionText = useMemo(
    () => normalizePromptContextText(parentQuestionText, PROMPT_PARENT_QUESTION_PREVIEW_MAX_LENGTH),
    [parentQuestionText],
  );
  const isLocked =
    readOnly || target.readOnly === true || isResolved || Boolean(submittedSummary) || Boolean(externallySubmittedSummary);
  const resolvedStateLabel =
    target.resolvedBy === 'timeout'
      ? '시간 초과 자동선택'
      : target.resolvedBy === 'system'
        ? '임의 선택 결과'
        : target.resolvedBy === 'user'
          ? resolvedSelectedValues.length > 0
            ? '선택 완료'
            : '전달 완료'
          : isResolved && resolvedSelectedValues.length > 0
            ? '선택 완료'
            : null;
  const expandedOption = steps
    .flatMap((step) => step.options)
    .find((option) => option.value === expandedOptionValue) ?? null;
  const isExpandedMarkdownPreview = isMarkdownPromptPreview(expandedOption?.preview);
  const lastHydratedPromptResetKeyRef = useRef(promptResetKey);

  useEffect(() => {
    setExpandedHtmlMode('preview');
    setExpandedPreviewSource(expandedOption?.preview?.content ?? '');
  }, [expandedOptionValue]);
  const activeStep = steps[Math.min(activeStepIndex, Math.max(steps.length - 1, 0))] ?? steps[0];
  const activeSelection = activeStep ? stepSelections[activeStep.key] : undefined;
  const markPromptPreviewViewed = (option: PromptOption) => {
    const previewUrl = resolvePromptPreviewUrl(option.preview?.url);
    onPreviewViewed?.(previewUrl || null);
  };
  const selectionSummary =
    activeStep && activeSelection
      ? buildStepSelectionText(activeStep, activeSelection.selectedValues) || buildFreeTextOnlySummaryText(activeSelection.freeText)
      : '';
  const displayedSelectionSummary =
    displayedSubmittedSummary ||
    externalDraftSummary ||
    (hasStepper
      ? buildPromptDraftSummaryText(
          target,
          buildPromptSelectionPayload(target, stepSelections) ?? {
            selectedValues: [],
            freeText: '',
            attachments: [],
          },
        )
      : resolvedSelectionSummary);
  const isFinalStep = activeStepIndex >= steps.length - 1;
  const canProceed = activeStep ? canProceedStep(activeStep, activeSelection) : false;
  const canAdvance = !isLocked && !isSubmitting && !isUploadingAttachment && canProceed;
  const canSubmit = !isLocked && !isSubmitting && !isUploadingAttachment && isFinalStep && canProceed;
  const submitLabel = activeStep?.submitLabel?.trim() || target.submitLabel?.trim() || '선택 전달';
  const progressPayload = buildPromptSelectionPayloadWithAttachments(target, stepSelections, attachments);
  const expandAllOptionPreviews = shouldExpandAllPromptPreviews(activeStep, activeSelection, isLocked);
  const expandedOptionPreviewUrl = expandedOption?.preview ? resolvePromptPreviewUrl(expandedOption.preview.url) : '';
  const isExpandedEditablePreview = expandedOption?.preview?.type === 'editable';
  const onSelectionChangeRef = useRef(onSelectionChange);

  useEffect(() => {
    onSelectionChangeRef.current = onSelectionChange;
  }, [onSelectionChange]);

  const emitSelectionChange = (nextSelections: Record<string, PromptStepDraftSelection>) => {
    const selectionChangeHandler = onSelectionChangeRef.current;

    if (!selectionChangeHandler) {
      return;
    }

    if (isLocked) {
      selectionChangeHandler(null);
      return;
    }

    selectionChangeHandler(buildPromptSelectionPayloadWithAttachments(target, nextSelections, attachments));
  };

  useEffect(() => {
    if (lastHydratedPromptResetKeyRef.current === promptResetKey) {
      return;
    }

    lastHydratedPromptResetKeyRef.current = promptResetKey;
    setStepSelections(initialStepSelections);
    setActiveStepIndex(resolveCurrentStepIndex(steps, initialStepSelections, target.currentStepKey));
  }, [initialStepSelections, promptResetKey, steps, target.currentStepKey]);

  useEffect(() => {
    if (isLocked) {
      emitSelectionChange({});
    }
  }, [attachments, isLocked]);

  useEffect(() => {
    if (defaultCollapsed) {
      setIsCollapsed(true);
    }
  }, [defaultCollapsed]);

  useEffect(() => {
    setAttachments([]);
    setIsUploadingAttachment(false);
  }, [promptResetKey]);

  useEffect(() => {
    if (isLocked || !onSelectionChange) {
      return;
    }

    emitSelectionChange(stepSelections);
  }, [attachments, isLocked, stepSelections]);

  useEffect(() => {
    if (!expandedOption?.preview) {
      return;
    }

    onPreviewViewed?.(expandedOptionPreviewUrl || null);
  }, [expandedOption?.value, expandedOptionPreviewUrl, onPreviewViewed]);

  const handleShareExpandedPreview = () => {
    if (!expandedOption?.preview) {
      return;
    }

    if (!expandedOptionPreviewUrl) {
      message.info('이 preview는 공유할 링크가 없습니다.');
      return;
    }

    void sharePreviewLink({
      url: expandedOptionPreviewUrl,
      title: expandedOption.preview.title?.trim() || expandedOption.label,
    })
      .then((result) => {
        message.success(result === 'shared' ? 'preview 링크를 공유했습니다.' : 'preview 링크를 복사했습니다.');
      })
      .catch((error: unknown) => {
        if (error instanceof DOMException && error.name === 'AbortError') {
          return;
        }

        message.error(error instanceof Error ? error.message : 'preview 링크를 공유하지 못했습니다.');
      });
  };

  const handleCopyExpandedEditablePreview = async () => {
    if (!expandedOption?.preview || !isExpandedEditablePreview) {
      return;
    }

    try {
      await navigator.clipboard.writeText(expandedPreviewSource);
      message.success('편집 내용을 복사했습니다.');
    } catch {
      message.error('편집 내용을 복사할 수 없습니다.');
    }
  };

  const uploadAttachments = async (files: File[]) => {
    if (!allowAttachments || !onUploadAttachment || isLocked || isSubmitting || isUploadingAttachment || files.length === 0) {
      return;
    }

    setIsUploadingAttachment(true);

    try {
      const uploadResults = await Promise.allSettled(files.map((file) => onUploadAttachment(file)));
      const uploadedItems: ChatComposerAttachment[] = [];
      const failedMessages: string[] = [];

      uploadResults.forEach((result, index) => {
        if (result.status === 'fulfilled') {
          uploadedItems.push(result.value);
          return;
        }

        const fallbackName = files[index]?.name?.trim() || `파일 ${index + 1}`;
        failedMessages.push(
          result.reason instanceof Error && result.reason.message.trim()
            ? result.reason.message.trim()
            : `${fallbackName} 업로드에 실패했습니다.`,
        );
      });

      if (uploadedItems.length > 0) {
        setAttachments((current) => mergePromptComposerAttachments(current, uploadedItems));
        message.success(`첨부 파일 ${uploadedItems.length}건을 추가했습니다.`);
      }

      if (failedMessages.length > 0) {
        message.error(failedMessages[0] ?? '첨부 파일 업로드에 실패했습니다.');
      }
    } finally {
      setIsUploadingAttachment(false);
    }
  };

  const updateActiveSelection = (updater: (current: PromptStepDraftSelection) => PromptStepDraftSelection) => {
    if (!activeStep) {
      return;
    }

    setStepSelections((current) => {
      const currentSelection = current[activeStep.key] ?? {
        stepKey: activeStep.key,
        stepTitle: activeStep.title,
        selectedValues: [],
        freeText: '',
        skipped: false,
      };
      const nextSelection = updater(currentSelection);
      const nextSelections = {
        ...current,
        [activeStep.key]: nextSelection,
      };
      return nextSelections;
    });
  };

  const handleStepAdvance = async () => {
    if (!activeStep || !activeSelection) {
      return;
    }

    if (!isFinalStep) {
      if (!canAdvance) {
        return;
      }

      setActiveStepIndex((current) => Math.min(current + 1, steps.length - 1));
      return;
    }

    if (!canSubmit) {
      return;
    }

    const payload = buildPromptSelectionPayloadWithAttachments(target, stepSelections, attachments);

    if (!payload) {
      return;
    }

    const attachmentBlock = buildAttachmentMessageBlock(attachments);
    const outgoingText = buildPromptResponseText(target, payload, '');
    const resolvedOutgoingText = attachmentBlock ? `${outgoingText.trim()}\n\n${attachmentBlock}`.trim() : outgoingText.trim();

    setIsSubmitting(true);
    const isSent = await onSubmit({
      text: resolvedOutgoingText,
      mode: activeStep.mode === 'direct' ? 'direct' : target.mode === 'direct' ? 'direct' : 'queue',
      selection: payload,
      promptTitle: target.title,
      promptSignature: buildPromptTargetSignature(target),
      contextRef: buildPromptContextRef(target, parentQuestionText),
      attachments,
    });
    setIsSubmitting(false);

    if (!isSent) {
      return;
    }

    setSubmittedSummary(payload.summaryText || buildPromptDraftSummaryText(target, payload));
    setSubmittedFreeText(payload.freeText);
    setAttachments([]);
    onSubmitted?.(payload);
  };

  return (
    <>
      <section ref={anchorRef} className="app-chat-preview-card app-chat-preview-card--prompt">
        <div className="app-chat-preview-card__header">
          <div className="app-chat-preview-card__meta">
            <span className="app-chat-preview-card__glyph app-chat-preview-card__glyph--prompt" aria-hidden="true">
              <MessageOutlined />
            </span>
            <div className="app-chat-preview-card__titles">
              <span className="app-chat-preview-card__label">{target.title}</span>
              <span className="app-chat-preview-card__kind">{hasStepper ? 'prompt stepper' : 'prompt'}</span>
            </div>
          </div>
          <div className="app-chat-preview-card__actions">
            {submittedSummary ? (
              <span className="app-chat-prompt-card__submitted">
                <CheckCircleOutlined />
                <span>전달됨</span>
              </span>
            ) : externallySubmittedSummary ? (
              <span className="app-chat-prompt-card__submitted">
                <CheckCircleOutlined />
                <span>전달됨</span>
              </span>
            ) : resolvedStateLabel ? (
              <span className="app-chat-prompt-card__submitted">
                <CheckCircleOutlined />
                <span>{resolvedStateLabel}</span>
              </span>
            ) : null}
            {displayedSelectionSummary ? (
              <span className="app-chat-prompt-card__selection-pill" title={displayedSelectionSummary}>
                {displayedSelectionSummary}
              </span>
            ) : null}
            {onSharePrompt ? (
              <Button
                type="text"
                size="small"
                className="app-chat-prompt-card__collapse-toggle"
                icon={<ShareAltOutlined />}
                aria-label={`${target.title} 공유`}
                onClick={(event) => {
                  event.preventDefault();
                  event.stopPropagation();
                  onSharePrompt();
                }}
              />
            ) : null}
            <Button
              type="text"
              size="small"
              className="app-chat-prompt-card__collapse-toggle"
              icon={isCollapsed ? <DownOutlined /> : <UpOutlined />}
              aria-label={isCollapsed ? `${target.title} 펼치기` : `${target.title} 접기`}
              onClick={(event) => {
                event.preventDefault();
                event.stopPropagation();
                setIsCollapsed((current) => !current);
              }}
            />
          </div>
        </div>
        {!isCollapsed ? (
          <div className="app-chat-preview-card__body app-chat-preview-card__body--prompt">
            {displayedParentQuestionText ? (
              <div className="app-chat-prompt-card__context" title={String(parentQuestionText ?? '').trim() || undefined}>
                <span className="app-chat-prompt-card__context-label">대상 질의</span>
                <span className="app-chat-prompt-card__context-text">{displayedParentQuestionText}</span>
              </div>
            ) : null}
            {target.description ? (
              <Paragraph className="app-chat-prompt-card__description">{target.description}</Paragraph>
            ) : null}
            {target.resultText ? <Paragraph className="app-chat-prompt-card__result">{target.resultText}</Paragraph> : null}
            {hasStepper ? (
              <div className="app-chat-prompt-card__stepper">
                <StepperUI
                  steps={steps.map((step, index) => {
                    const selection = stepSelections[step.key];
                    const isComplete =
                      selection?.skipped ||
                      (selection?.selectedValues.length ?? 0) > 0 ||
                      (selection?.freeText.trim().length ?? 0) > 0;

                    return {
                      key: step.key,
                      label: step.title,
                      description:
                        selection?.skipped
                          ? '건너뜀'
                          : isComplete
                            ? buildStepSelectionText(step, selection?.selectedValues ?? []) || step.description
                            : step.description,
                      status: index < activeStepIndex ? 'complete' : index === activeStepIndex ? 'current' : 'pending',
                    };
                  })}
                  currentStepKey={activeStep?.key}
                  direction="vertical"
                  compact
                  onStepClick={
                    isLocked
                      ? undefined
                      : (_, index) => {
                          if (index > activeStepIndex + 1) {
                            return;
                          }

                          setActiveStepIndex(index);
                        }
                  }
                />
              </div>
            ) : null}
            {activeStep ? (
              <div className="app-chat-prompt-card__step-panel">
                {hasStepper ? (
                  <div className="app-chat-prompt-card__step-header">
                    <div>
                      <Text className="app-chat-prompt-card__step-kicker">
                        {activeStepIndex + 1}/{steps.length} 단계
                      </Text>
                      <Text className="app-chat-prompt-card__step-title">{activeStep.title}</Text>
                    </div>
                    {activeStep.optional ? <span className="app-chat-prompt-card__optional-pill">선택 단계</span> : null}
                  </div>
                ) : null}
                {hasStepper && activeStep.description ? (
                  <Paragraph className="app-chat-prompt-card__description">{activeStep.description}</Paragraph>
                ) : null}
                <div className="app-chat-prompt-card__options" role={activeStep.multiple ? 'group' : 'radiogroup'} aria-label={activeStep.title}>
                  {activeStep.options.map((option) => {
                    const isSelected = activeSelection?.selectedValues.includes(option.value) ?? false;
                    const shouldRenderPreview = Boolean(option.preview) && (isSelected || expandAllOptionPreviews);
                    const optionInteractionProps = isLocked
                      ? {}
                      : {
                          role: 'button' as const,
                          tabIndex: 0,
                          onClick: () => {
                            updateActiveSelection((current) => {
                              const nextSelectedValues = activeStep.multiple
                                ? current.selectedValues.includes(option.value)
                                  ? current.selectedValues.filter((value) => value !== option.value)
                                  : [...current.selectedValues, option.value]
                                : current.selectedValues.includes(option.value)
                                  ? []
                                  : [option.value];

                              return {
                                ...current,
                                selectedValues: nextSelectedValues,
                                skipped: false,
                              };
                            });
                          },
                          onKeyDown: (event: ReactKeyboardEvent<HTMLDivElement>) => {
                            if (event.key !== 'Enter' && event.key !== ' ') {
                              return;
                            }

                            event.preventDefault();
                            updateActiveSelection((current) => {
                              const nextSelectedValues = activeStep.multiple
                                ? current.selectedValues.includes(option.value)
                                  ? current.selectedValues.filter((value) => value !== option.value)
                                  : [...current.selectedValues, option.value]
                                : current.selectedValues.includes(option.value)
                                  ? []
                                  : [option.value];

                              return {
                                ...current,
                                selectedValues: nextSelectedValues,
                                skipped: false,
                              };
                            });
                          },
                        };

                    return (
                      <div
                        key={`${activeStep.key}-${option.value}`}
                        className={`app-chat-prompt-card__option${isSelected ? ' app-chat-prompt-card__option--selected' : ''}${
                          isLocked ? ' app-chat-prompt-card__option--readonly' : ''
                        }`}
                        {...optionInteractionProps}
                      >
                        <div className="app-chat-prompt-card__option-head">
                          <span className="app-chat-prompt-card__option-control" aria-hidden="true">
                            {activeStep.multiple ? (
                              <span className="app-chat-prompt-card__checkbox">
                                <span />
                              </span>
                            ) : (
                              <span className="app-chat-prompt-card__radio">
                                <span />
                              </span>
                            )}
                          </span>
                          <div className="app-chat-prompt-card__option-body">
                            <span className="app-chat-prompt-card__option-headline">
                              <Text className="app-chat-prompt-card__option-label">{option.label}</Text>
                              <span className="app-chat-prompt-card__option-meta">
                                {option.preview ? (
                                  <span className="app-chat-prompt-card__option-preview-chip">
                                    <EyeOutlined />
                                    <span>preview</span>
                                  </span>
                                ) : null}
                                {isSelected && (isLocked || Boolean(submittedSummary) || isResolved) ? (
                                  <span className="app-chat-prompt-card__option-state">선택됨</span>
                                ) : null}
                              </span>
                            </span>
                            {option.description ? (
                              <span className="app-chat-prompt-card__option-description">{option.description}</span>
                            ) : null}
                            {option.preview ? (
                              <span className="app-chat-prompt-card__option-preview-hint">
                                {expandAllOptionPreviews
                                  ? '선택값이 없어 preview 전체를 표시합니다'
                                  : isSelected
                                    ? '아래 preview에서 바로 확인'
                                    : '선택하면 아래에 preview가 펼쳐집니다'}
                              </span>
                            ) : null}
                          </div>
                        </div>
                        {shouldRenderPreview ? (
                          <div className="app-chat-prompt-card__option-preview-inline">
                            <PromptPreviewCard
                              option={option}
                              onOpenPreview={(nextOption) => setExpandedOptionValue(nextOption.value)}
                              onPreviewViewed={markPromptPreviewViewed}
                            />
                          </div>
                        ) : null}
                      </div>
                    );
                  })}
                </div>
                {!isLocked ? (
                  <div className="app-chat-prompt-card__free-text">
                    <Text className="app-chat-prompt-card__free-text-label">
                      {activeStep.freeTextLabel?.trim() || target.freeTextLabel?.trim() || '기타 요청'}
                    </Text>
                    <Input.TextArea
                      value={activeSelection?.freeText ?? ''}
                      onChange={(event) => {
                        const nextFreeText = event.target.value;
                        updateActiveSelection((current) => ({
                          ...current,
                          freeText: nextFreeText,
                        }));
                      }}
                      onKeyDown={(event) => {
                        if (event.key !== 'Enter' || event.nativeEvent.isComposing) {
                          return;
                        }

                        const hasSubmitModifier = event.ctrlKey || event.metaKey;
                        if (!hasSubmitModifier) {
                          event.stopPropagation();
                          return;
                        }

                        event.preventDefault();
                        event.stopPropagation();
                        void handleStepAdvance();
                      }}
                      onPaste={(event) => {
                        if (!allowAttachments || !onUploadAttachment || isLocked || isSubmitting || isUploadingAttachment) {
                          return;
                        }

                        const files = resolvePromptPasteFiles(event.clipboardData);

                        if (files.length === 0) {
                          return;
                        }

                        event.preventDefault();
                        event.stopPropagation();
                        void uploadAttachments(files);
                      }}
                      placeholder={
                        activeStep.freeTextPlaceholder?.trim() ||
                        target.freeTextPlaceholder?.trim() ||
                        '선택과 함께 전달할 추가 요청을 입력하세요.'
                      }
                      autoSize={{ minRows: 2, maxRows: 5 }}
                      maxLength={1000}
                      disabled={isSubmitting || isUploadingAttachment}
                    />
                    <Text className="app-chat-prompt-card__free-text-hint">Ctrl+Enter / Cmd+Enter 전송, Enter 줄바꿈</Text>
                    {!isLocked && allowAttachments && onUploadAttachment ? (
                      <>
                        <input
                          ref={attachmentInputRef}
                          type="file"
                          multiple
                          accept={attachmentAccept}
                          className="app-chat-panel__composer-file-input"
                          onChange={(event) => {
                            const files = Array.from(event.target.files ?? []);
                            event.target.value = '';
                            void uploadAttachments(files);
                          }}
                        />
                        <div className="app-chat-prompt-card__attachment-actions">
                          <Button
                            size="small"
                            className="app-chat-prompt-card__attachment-trigger"
                            icon={<PlusOutlined />}
                            onClick={() => attachmentInputRef.current?.click()}
                            loading={isUploadingAttachment}
                            disabled={isSubmitting || isUploadingAttachment}
                            aria-label="첨부 파일 추가"
                            title="첨부 파일 추가"
                          />
                          <Text className="app-chat-prompt-card__free-text-hint">파일 선택 또는 Ctrl+V/Cmd+V 붙여넣기로 첨부할 수 있습니다.</Text>
                        </div>
                        {attachments.length > 0 ? (
                          <div className="app-chat-panel__composer-attachment-strip" aria-live="polite">
                            {attachments.map((attachment) => (
                              <div key={attachment.id} className="app-chat-panel__composer-attachment-chip">
                                <span className="app-chat-panel__composer-attachment-name">{attachment.name}</span>
                                <Button
                                  type="text"
                                  size="small"
                                  className="app-chat-panel__composer-attachment-remove"
                                  icon={<CloseOutlined />}
                                  aria-label={`${attachment.name} 첨부 제거`}
                                  onClick={() => {
                                    setAttachments((current) => current.filter((item) => item.id !== attachment.id));
                                  }}
                                />
                              </div>
                            ))}
                          </div>
                        ) : null}
                      </>
                    ) : null}
                  </div>
                ) : null}
                {isLocked && submittedAttachments.length > 0 ? (
                  <div className="app-chat-prompt-card__free-text">
                    <Text className="app-chat-prompt-card__free-text-label">첨부 파일</Text>
                    <div className="app-chat-panel__composer-attachment-strip" aria-live="polite">
                      {submittedAttachments.map((attachment) => (
                        <div key={attachment.id} className="app-chat-panel__composer-attachment-chip">
                          <a
                            className="app-chat-panel__composer-attachment-name"
                            href={normalizeChatResourceUrl(attachment.publicUrl)}
                            target="_blank"
                            rel="noreferrer"
                          >
                            {attachment.name}
                          </a>
                        </div>
                      ))}
                    </div>
                  </div>
                ) : null}
                <div className="app-chat-prompt-card__footer">
                  <div className="app-chat-prompt-card__summary" aria-live="polite">
                    {submittedSummary
                      ? `마지막 전달: ${submittedSummary}`
                      : externallySubmittedSummary
                        ? `마지막 전달: ${externallySubmittedSummary}`
                        : resolvedSelectionSummary
                          ? target.resolvedBy === 'timeout'
                            ? `시간 초과로 자동 선택: ${resolvedSelectionSummary}`
                            : `선택 결과: ${resolvedSelectionSummary}`
                          : isResolved && target.resolvedBy === 'user'
                            ? target.resultText?.trim()
                              ? `전달 결과: ${target.resultText.trim()}`
                              : '선택 없이 전달 완료됨.'
                            : readOnly || target.readOnly === true
                              ? '지난 prompt는 읽기 전용입니다.'
                              : hasStepper
                                ? progressPayload?.summaryText || '단계를 순서대로 선택하세요.'
                                : selectionSummary
                                  ? `선택: ${selectionSummary}`
                                  : '항목을 선택하세요.'}
                    {submittedFreeTextValue ? (
                      <span className="app-chat-prompt-card__summary-detail">추가 요청: {submittedFreeTextValue}</span>
                    ) : null}
                  </div>
                  <div className="app-chat-prompt-card__footer-actions">
                    {hasStepper ? (
                      <Button
                        size="small"
                        icon={<LeftOutlined />}
                        onClick={() => setActiveStepIndex((current) => Math.max(0, current - 1))}
                        disabled={isSubmitting || activeStepIndex === 0}
                      >
                        이전
                      </Button>
                    ) : null}
                    {hasStepper && activeStep?.optional ? (
                      <Button
                        size="small"
                        onClick={() => {
                          updateActiveSelection((current) => ({
                            ...current,
                            selectedValues: [],
                            skipped: true,
                          }));
                          if (!isFinalStep) {
                            setActiveStepIndex((current) => Math.min(current + 1, steps.length - 1));
                          }
                        }}
                        disabled={isLocked || isSubmitting}
                      >
                        건너뛰기
                      </Button>
                    ) : null}
                    <Button
                      type="primary"
                      size="small"
                      icon={hasStepper && !isFinalStep ? <RightOutlined /> : undefined}
                      onClick={() => void handleStepAdvance()}
                      disabled={hasStepper && !isFinalStep ? !canAdvance : !canSubmit}
                      loading={isSubmitting}
                    >
                      {hasStepper && !isFinalStep ? '다음 단계' : submitLabel}
                    </Button>
                  </div>
                </div>
              </div>
            ) : null}
          </div>
        ) : null}
      </section>
      <FullscreenPreviewModal
        open={Boolean(expandedOption?.preview)}
        onClose={() => setExpandedOptionValue(null)}
      title={expandedOption?.preview?.title?.trim() || expandedOption?.label || 'preview'}
        actions={
          canShowHtmlPreviewActions(expandedOption?.preview) ? (
            <>
              <Button
                type="text"
                className="fullscreen-preview-modal__icon-button"
                aria-label="preview 공유"
                icon={<ShareAltOutlined />}
                onClick={handleShareExpandedPreview}
              />
              {isExpandedEditablePreview ? (
                <Button
                  type="text"
                  className="fullscreen-preview-modal__icon-button"
                  aria-label="편집 내용 복사"
                  icon={<CopyOutlined />}
                  onClick={handleCopyExpandedEditablePreview}
                />
              ) : (
                <>
                  <Button
                    type="text"
                    className="fullscreen-preview-modal__icon-button"
                    aria-label="미리보기 화면"
                    icon={<EyeOutlined />}
                    onClick={() => setExpandedHtmlMode('preview')}
                  />
                  <Button
                    type="text"
                    className="fullscreen-preview-modal__icon-button"
                    aria-label="소스 보기"
                    icon={<CodeOutlined />}
                    onClick={() => setExpandedHtmlMode('source')}
                  />
                </>
              )}
            </>
          ) : (
            <Button
              type="text"
              className="fullscreen-preview-modal__icon-button"
              aria-label="preview 공유"
              icon={<ShareAltOutlined />}
              onClick={handleShareExpandedPreview}
            />
          )
        }
        className={`app-chat-prompt-card__preview-modal${
          isExpandedMarkdownPreview ? ' app-chat-prompt-card__preview-modal--light-surface' : ''
        }`}
      >
        <div className="app-chat-prompt-card__preview-modal-surface">
          {expandedOption?.preview ? (
            <PromptPreviewSurface
              preview={expandedOption.preview}
              htmlMode={expandedHtmlMode}
              sourceContent={expandedPreviewSource}
              onSourceContentChange={setExpandedPreviewSource}
            />
          ) : null}
        </div>
      </FullscreenPreviewModal>
    </>
  );
}
