import clsx from 'clsx';
import DOMPurify from 'dompurify';
import {
  Check,
  Download,
  FileText,
  Languages,
  MessageSquarePlus,
  Pin,
  PinOff,
  RefreshCw,
  Save,
  Sparkles,
  X,
} from 'lucide-react';
import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';

import { Overlay } from '@/components/Overlay';
import { useEnv } from '@/context/EnvContext';
import { useIsMobileViewport } from '@/hooks/useIsMobileViewport';
import { useTranslation } from '@/hooks/useTranslation';
import { usePanelResize } from '@/hooks/usePanelResize';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useReviewModeStore } from '@/store/reviewModeStore';
import { useThemeStore } from '@/store/themeStore';
import { getPanelTopInset } from '@/utils/insets';
import { openExternalUrl } from '@/utils/open';
import { useReviewPanelDrag } from '../hooks/useReviewPanelDrag';
import { useReviewPanelFloatingResize } from '../hooks/useReviewPanelFloatingResize';
import {
  downloadReviewedEpub,
  exportReviewedEpub,
  generateReviewFeedback,
  isReviewEditDirty,
  loadReviewGlossary,
  retranslateReviewRow,
  saveReviewGlossary,
  saveReviewGptConfig,
  saveReviewRow,
  sidecarApi,
  switchReviewGlossaryPath,
  type ReviewEditPayload,
  type ReviewGlossaryEntry,
  type ReviewGlossaryPayload,
  type ReviewGptConfig,
  type ReviewSessionPayload,
} from '@/services/reviewEditorService';

const MIN_REVIEW_PANEL_WIDTH = 0.24;
const MAX_REVIEW_PANEL_WIDTH = 0.48;
const MIN_FLOATING_PANEL_HEIGHT = 0.35;
const MAX_FLOATING_PANEL_HEIGHT = 0.92;
const FLOATING_PANEL_MARGIN = 12;
const DEFAULT_REVIEW_PANEL_WIDTH = 0.32;

const cnHtmlPurifyOptions = {
  ALLOWED_TAGS: [
    'ruby',
    'rb',
    'rt',
    'rp',
    'span',
    'mark',
    'br',
    'em',
    'strong',
    'b',
    'i',
    'u',
    's',
    'sub',
    'sup',
  ],
  ALLOWED_ATTR: ['class', 'title'],
  ALLOW_DATA_ATTR: false,
};

type GptForm = {
  base_url: string;
  model: string;
  api_key: string;
  glossary_path: string;
  translation_prompt: string;
  format_prompt: string;
  character_prompt: string;
};

const emptyEditState: ReviewEditPayload = {
  current_html: '',
  marked: false,
  issue_type: '',
  severity: '',
  tags: '',
  comment: '',
  learn_note: '',
};

const emptyGptForm: GptForm = {
  base_url: '',
  model: '',
  api_key: '',
  glossary_path: '',
  translation_prompt: '',
  format_prompt: '',
  character_prompt: '',
};

const sanitizeInlineHtml = (html: string) => DOMPurify.sanitize(html || '', cnHtmlPurifyOptions);

const escapeInlineHtml = (value: string) =>
  value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

const rowTitle = (row: { document_title?: string; file_label?: string; file: string }) =>
  row.document_title || row.file_label || row.file;

const toGptForm = (config: ReviewGptConfig | null | undefined): GptForm => ({
  base_url: config?.base_url || '',
  model: config?.model || '',
  api_key: '',
  glossary_path: config?.glossary_path || '',
  translation_prompt:
    config?.translation_prompt || config?.prompt_defaults?.translation_prompt || '',
  format_prompt: config?.format_prompt || config?.prompt_defaults?.format_prompt || '',
  character_prompt: config?.character_prompt || config?.prompt_defaults?.character_prompt || '',
});

const PanelButton = ({
  children,
  icon,
  onClick,
  disabled,
  variant = 'ghost',
}: {
  children: ReactNode;
  icon?: ReactNode;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'ghost';
}) => (
  <button
    type='button'
    onClick={onClick}
    disabled={disabled}
    className={clsx(
      'btn h-auto min-h-9 max-w-full whitespace-normal rounded-md px-3 py-2 text-start text-sm leading-snug break-words',
      '[&>svg]:shrink-0',
      variant === 'primary' ? 'btn-primary' : 'btn-ghost eink-bordered',
      disabled && 'opacity-60',
    )}
  >
    {icon}
    {children}
  </button>
);

function GptConfigPanel({
  baseUrl,
  sessionId,
  form,
  setForm,
  onSaved,
}: {
  baseUrl: string;
  sessionId: string | null | undefined;
  form: GptForm;
  setForm: React.Dispatch<React.SetStateAction<GptForm>>;
  onSaved: (config: ReviewGptConfig) => void;
}) {
  const [open, setOpen] = useState(false);
  const [saving, setSaving] = useState(false);
  const [message, setMessage] = useState('');

  const save = async () => {
    setSaving(true);
    setMessage('');
    try {
      const config = await saveReviewGptConfig(baseUrl, sessionId, {
        base_url: form.base_url || undefined,
        model: form.model || undefined,
        api_key: form.api_key,
        glossary_path: form.glossary_path,
        translation_prompt: form.translation_prompt,
        format_prompt: form.format_prompt,
        character_prompt: form.character_prompt,
      });
      setForm((current) => ({ ...current, api_key: '' }));
      onSaved(config);
      setMessage('已保存 API、提示词与术语表路径。');
    } catch (error) {
      setMessage(error instanceof Error ? error.message : String(error));
    } finally {
      setSaving(false);
    }
  };

  return (
    <section className='eink-bordered border-base-300 bg-base-100 rounded-md border'>
      <button
        type='button'
        className='hover:bg-base-200 flex w-full items-center justify-between rounded-md px-3 py-2 text-start'
        onClick={() => setOpen((value) => !value)}
      >
        <span className='text-sm font-semibold'>API 与提示词</span>
        <span className='text-base-content/60 text-xs'>{open ? '收起' : '展开'}</span>
      </button>
      {open ? (
        <div className='grid gap-3 px-3 pb-3'>
          <div className='grid gap-2'>
            <label className='grid gap-1 text-xs font-medium'>
              Base URL
              <input
                className='input input-bordered eink-bordered h-9 min-w-0 rounded-md text-sm'
                value={form.base_url}
                onChange={(event) =>
                  setForm((current) => ({ ...current, base_url: event.target.value }))
                }
                placeholder='https://api.openai.com/v1'
              />
            </label>
            <label className='grid gap-1 text-xs font-medium'>
              模型
              <input
                className='input input-bordered eink-bordered h-9 min-w-0 rounded-md text-sm'
                value={form.model}
                onChange={(event) =>
                  setForm((current) => ({ ...current, model: event.target.value }))
                }
                placeholder='gpt-4.1'
              />
            </label>
          </div>
          <label className='grid gap-1 text-xs font-medium'>
            API Key
            <input
              className='input input-bordered eink-bordered h-9 min-w-0 rounded-md text-sm'
              value={form.api_key}
              type='password'
              onChange={(event) =>
                setForm((current) => ({ ...current, api_key: event.target.value }))
              }
              placeholder='留空表示沿用已保存密钥'
            />
          </label>
          <label className='grid gap-1 text-xs font-medium'>
            术语表路径
            <input
              className='input input-bordered eink-bordered h-9 min-w-0 rounded-md text-sm'
              value={form.glossary_path}
              onChange={(event) =>
                setForm((current) => ({ ...current, glossary_path: event.target.value }))
              }
              placeholder='mingcibiao.json'
            />
          </label>
          <label className='grid gap-1 text-xs font-medium'>
            翻译内容提示词
            <textarea
              className='textarea textarea-bordered eink-bordered min-h-20 min-w-0 rounded-md text-sm'
              value={form.translation_prompt}
              onChange={(event) =>
                setForm((current) => ({ ...current, translation_prompt: event.target.value }))
              }
            />
          </label>
          <label className='grid gap-1 text-xs font-medium'>
            格式与标点提示词
            <textarea
              className='textarea textarea-bordered eink-bordered min-h-16 min-w-0 rounded-md text-sm'
              value={form.format_prompt}
              onChange={(event) =>
                setForm((current) => ({ ...current, format_prompt: event.target.value }))
              }
            />
          </label>
          <label className='grid gap-1 text-xs font-medium'>
            角色状态与口吻提示词
            <textarea
              className='textarea textarea-bordered eink-bordered min-h-16 min-w-0 rounded-md text-sm'
              value={form.character_prompt}
              onChange={(event) =>
                setForm((current) => ({ ...current, character_prompt: event.target.value }))
              }
            />
          </label>
          <div className='flex flex-wrap items-center gap-2'>
            <PanelButton
              onClick={save}
              disabled={saving || !baseUrl}
              icon={<Save className='h-4 w-4' />}
              variant='primary'
            >
              {saving ? '保存中' : '保存设置'}
            </PanelButton>
            {message ? <span className='text-base-content/70 text-xs'>{message}</span> : null}
          </div>
        </div>
      ) : null}
    </section>
  );
}

function GlossaryPanel({
  baseUrl,
  sessionId,
  path,
  setPath,
}: {
  baseUrl: string;
  sessionId: string | null | undefined;
  path: string;
  setPath: (path: string) => void;
}) {
  const [open, setOpen] = useState(false);
  const [entries, setEntries] = useState<ReviewGlossaryEntry[]>([]);
  const [loaded, setLoaded] = useState(false);
  const [dirty, setDirty] = useState(false);
  const [query, setQuery] = useState('');
  const [message, setMessage] = useState('未读取术语表');
  const [busy, setBusy] = useState(false);

  const filteredEntries = useMemo(() => {
    const needle = query.trim().toLowerCase();
    if (!needle) return entries.map((entry, index) => ({ entry, index }));
    return entries
      .map((entry, index) => ({ entry, index }))
      .filter(
        ({ entry }) =>
          entry.source.toLowerCase().includes(needle) ||
          entry.target.toLowerCase().includes(needle),
      );
  }, [entries, query]);

  const applyPayload = (payload: ReviewGlossaryPayload) => {
    setEntries(payload.entries || []);
    setLoaded(true);
    setDirty(false);
    if (payload.path) setPath(payload.path);
    setMessage(
      `当前术语表：${payload.path || path || '未设置'} · ${payload.count ?? payload.entries?.length ?? 0} 条`,
    );
  };

  const load = async (forcePath = false) => {
    setBusy(true);
    try {
      const payload =
        forcePath && path
          ? await switchReviewGlossaryPath(baseUrl, sessionId, path)
          : await loadReviewGlossary(baseUrl, sessionId);
      applyPayload(payload);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : String(error));
    } finally {
      setBusy(false);
    }
  };

  const save = async () => {
    setBusy(true);
    try {
      const payload = await saveReviewGlossary(baseUrl, sessionId, path, entries);
      applyPayload(payload);
      setMessage('术语表已保存，下一次重翻会实时使用最新术语。');
    } catch (error) {
      setMessage(error instanceof Error ? error.message : String(error));
    } finally {
      setBusy(false);
    }
  };

  const updateEntry = (index: number, patch: Partial<ReviewGlossaryEntry>) => {
    setEntries((current) =>
      current.map((entry, itemIndex) => (itemIndex === index ? { ...entry, ...patch } : entry)),
    );
    setDirty(true);
  };

  const removeEntry = (index: number) => {
    setEntries((current) => current.filter((_entry, itemIndex) => itemIndex !== index));
    setDirty(true);
  };

  const addEntry = () => {
    setOpen(true);
    setLoaded(true);
    setQuery('');
    setEntries((current) => [{ source: '', target: '' }, ...current]);
    setDirty(true);
  };

  return (
    <section className='eink-bordered border-base-300 bg-base-100 rounded-md border'>
      <button
        type='button'
        className='hover:bg-base-200 flex w-full items-center justify-between rounded-md px-3 py-2 text-start'
        onClick={() => {
          const nextOpen = !open;
          setOpen(nextOpen);
          if (nextOpen && !loaded) void load(false);
        }}
      >
        <span className='text-sm font-semibold'>术语表</span>
        <span className='text-base-content/60 text-xs'>{open ? '收起' : '展开'}</span>
      </button>
      {open ? (
        <div className='grid gap-3 px-3 pb-3'>
          <div className='grid gap-2'>
            <input
              className='input input-bordered eink-bordered h-9 min-w-0 rounded-md text-sm'
              value={path}
              onChange={(event) => setPath(event.target.value)}
              placeholder='mingcibiao.json'
            />
            <div className='flex flex-wrap gap-2'>
              <PanelButton
                onClick={() => void load(true)}
                disabled={busy}
                icon={<RefreshCw className='h-4 w-4' />}
              >
                读取
              </PanelButton>
              <PanelButton onClick={addEntry} disabled={busy}>
                新增术语
              </PanelButton>
              <PanelButton
                onClick={() => void save()}
                disabled={busy || !loaded || !dirty}
                icon={<Save className='h-4 w-4' />}
                variant='primary'
              >
                保存术语表
              </PanelButton>
            </div>
            <p className='text-base-content/60 text-xs'>{message}</p>
            <input
              className='input input-bordered eink-bordered h-9 min-w-0 rounded-md text-sm'
              value={query}
              onChange={(event) => setQuery(event.target.value)}
              placeholder='搜索术语或译名'
            />
          </div>
          <div className='grid max-h-72 gap-2 overflow-auto'>
            {!loaded ? (
              <p className='text-base-content/60 p-2 text-sm'>展开后会读取术语表。</p>
            ) : filteredEntries.length === 0 ? (
              <p className='text-base-content/60 p-2 text-sm'>没有匹配的术语。</p>
            ) : (
              filteredEntries.slice(0, 80).map(({ entry, index }) => (
                <div key={`${index}-${entry.source}`} className='grid gap-2'>
                  <input
                    className='input input-bordered eink-bordered h-9 min-w-0 rounded-md text-sm'
                    value={entry.source}
                    onChange={(event) => updateEntry(index, { source: event.target.value })}
                    placeholder='原文'
                  />
                  <input
                    className='input input-bordered eink-bordered h-9 min-w-0 rounded-md text-sm'
                    value={entry.target}
                    onChange={(event) => updateEntry(index, { target: event.target.value })}
                    placeholder='译名'
                  />
                  <button
                    type='button'
                    className='btn btn-ghost eink-bordered h-9 min-h-9 rounded-md px-2 text-xs'
                    onClick={() => removeEntry(index)}
                  >
                    删除
                  </button>
                </div>
              ))
            )}
          </div>
        </div>
      ) : null}
    </section>
  );
}

const ReviewPanel: React.FC = () => {
  const _ = useTranslation();
  const { appService } = useEnv();
  const { safeAreaInsets, systemUIVisible, statusBarHeight } = useThemeStore();
  const activeBookKey = useReviewModeStore((state) => state.activeBookKey);
  const isPanelVisible = useReviewModeStore((state) => state.isPanelVisible);
  const isPanelPinned = useReviewModeStore((state) => state.isPanelPinned);
  const panelWidth = useReviewModeStore((state) => state.panelWidth);
  const panelHeight = useReviewModeStore((state) => state.panelHeight);
  const setPanelVisible = useReviewModeStore((state) => state.setPanelVisible);
  const togglePanelPin = useReviewModeStore((state) => state.togglePanelPin);
  const setPanelWidth = useReviewModeStore((state) => state.setPanelWidth);
  const setPanelHeight = useReviewModeStore((state) => state.setPanelHeight);
  const setBookUnsavedEdit = useReviewModeStore((state) => state.setBookUnsavedEdit);
  const updateRow = useReviewModeStore((state) => state.updateRow);
  const setBookData = useReviewModeStore((state) => state.setBookData);
  const bookState = useReviewModeStore((state) =>
    activeBookKey ? state.books[activeBookKey] : null,
  );
  const getBookData = useBookDataStore((state) => state.getBookData);
  const getProgress = useReaderStore((state) => state.getProgress);
  const getView = useReaderStore((state) => state.getView);
  const viewSettings = useReaderStore((state) =>
    activeBookKey ? state.viewStates[activeBookKey]?.viewSettings : null,
  );

  const [edit, setEdit] = useState<ReviewEditPayload>(emptyEditState);
  const [gptForm, setGptForm] = useState<GptForm>(emptyGptForm);
  const [candidateHtml, setCandidateHtml] = useState('');
  const [retranslateInstruction, setRetranslateInstruction] = useState('');
  const [message, setMessage] = useState('');
  const [exportInfo, setExportInfo] = useState('');
  const [saving, setSaving] = useState(false);
  const [retranslating, setRetranslating] = useState(false);
  const [exporting, setExporting] = useState(false);
  const [previewOpen, setPreviewOpen] = useState(false);
  const isMobile = useIsMobileViewport();
  const panelTopInset = getPanelTopInset({
    isMobile,
    isFullHeightInMobile: true,
    systemUIVisible,
    statusBarHeight,
    safeAreaInsets,
  });
  const isDocked = !isMobile && isPanelPinned;
  const panelRef = useRef<HTMLElement>(null);
  const cnEditorRef = useRef<HTMLTextAreaElement>(null);
  const restoreTimeoutRef = useRef<number | null>(null);

  const selectedRow = useMemo(
    () => bookState?.rows.find((row) => row.id === bookState.selectedRowId) || null,
    [bookState?.rows, bookState?.selectedRowId],
  );
  const canUseRowActions = Boolean(selectedRow && bookState?.baseUrl);
  const hasUnsavedEdit = Boolean(selectedRow && isReviewEditDirty(selectedRow, edit));

  useEffect(() => {
    if (!activeBookKey) return;
    setBookUnsavedEdit(activeBookKey, hasUnsavedEdit);
  }, [activeBookKey, hasUnsavedEdit, setBookUnsavedEdit]);

  useEffect(() => {
    if (!hasUnsavedEdit) return;
    const warnBeforeUnload = (event: BeforeUnloadEvent) => event.preventDefault();
    window.addEventListener('beforeunload', warnBeforeUnload);
    return () => window.removeEventListener('beforeunload', warnBeforeUnload);
  }, [hasUnsavedEdit]);

  const bookTitle = activeBookKey
    ? getBookData(activeBookKey)?.book?.title || selectedRow?.document_title || '审校'
    : '审校';

  useEffect(() => {
    setGptForm(toGptForm(bookState?.gptConfig));
  }, [bookState?.gptConfig]);

  useEffect(() => {
    if (!selectedRow) {
      setEdit(emptyEditState);
      setCandidateHtml('');
      setRetranslateInstruction('');
      return;
    }
    setEdit({
      current_html: selectedRow.current_html ?? selectedRow.cn_html ?? '',
      marked: Boolean(selectedRow.marked),
      issue_type: selectedRow.issue_type || '',
      severity: selectedRow.severity || '',
      tags: selectedRow.tags || '',
      comment: selectedRow.comment || '',
      learn_note: selectedRow.learn_note || '',
    });
    setCandidateHtml('');
    setRetranslateInstruction('');
    setPreviewOpen(false);
  }, [selectedRow]);

  const restoreReadingPositionAfterLayoutChange = useCallback(() => {
    if (!activeBookKey) return;
    const view = getView(activeBookKey);
    const cfi = view?.lastLocation?.cfi || getProgress(activeBookKey)?.location;
    if (!view || !cfi) return;
    if (restoreTimeoutRef.current) window.clearTimeout(restoreTimeoutRef.current);
    restoreTimeoutRef.current = window.setTimeout(() => {
      window.requestAnimationFrame(() => {
        window.requestAnimationFrame(() => view.goTo(cfi));
      });
    }, 160);
  }, [activeBookKey, getProgress, getView]);

  const handleTogglePanelPin = () => {
    togglePanelPin();
  };

  const closePanel = async () => {
    if (
      hasUnsavedEdit &&
      appService &&
      !(await appService.ask('当前段落有未保存的修改，确定关闭审校面板吗？'))
    ) {
      return;
    }
    setPanelVisible(false);
  };

  const floatingPanelEnabled = !isPanelPinned && !isMobile;
  const {
    panelPosition,
    panelPositionRef,
    updatePanelPosition,
    moveToDefaultPosition: moveFloatingPanelToDefault,
    handlePanelDragStart,
  } = useReviewPanelDrag({
    enabled: floatingPanelEnabled,
    panelRef,
    margin: FLOATING_PANEL_MARGIN,
    defaultWidthRatio: DEFAULT_REVIEW_PANEL_WIDTH,
    topInset: panelTopInset,
  });

  const {
    handleWidthPointerDown: handleFloatingWidthPointerDown,
    handleWidthKeyDown: handleFloatingWidthKeyDown,
    handleHeightPointerDown: handleFloatingHeightPointerDown,
    handleHeightKeyDown: handleFloatingHeightKeyDown,
  } = useReviewPanelFloatingResize({
    enabled: floatingPanelEnabled,
    panelRef,
    panelPositionRef,
    panelWidth,
    panelHeight,
    minWidth: MIN_REVIEW_PANEL_WIDTH,
    maxWidth: MAX_REVIEW_PANEL_WIDTH,
    minHeight: MIN_FLOATING_PANEL_HEIGHT,
    maxHeight: MAX_FLOATING_PANEL_HEIGHT,
    setPanelWidth,
    setPanelHeight,
    updatePanelPosition,
  });

  const {
    handleResizeStart: handleDockedResizeStart,
    handleResizeKeyDown: handleDockedResizeKeyDown,
  } = usePanelResize({
    side: 'end',
    minWidth: MIN_REVIEW_PANEL_WIDTH,
    maxWidth: MAX_REVIEW_PANEL_WIDTH,
    getWidth: () => panelWidth,
    onResize: setPanelWidth,
  });

  const handleResizeKeyDown = (event: React.KeyboardEvent) => {
    if (floatingPanelEnabled) {
      handleFloatingWidthKeyDown(event);
    } else {
      handleDockedResizeKeyDown(event);
    }
  };

  useEffect(() => {
    moveFloatingPanelToDefault();
  }, [moveFloatingPanelToDefault]);

  useEffect(() => {
    if (!isPanelVisible || isMobile) return;
    restoreReadingPositionAfterLayoutChange();
  }, [isMobile, isPanelPinned, isPanelVisible, restoreReadingPositionAfterLayoutChange]);

  useEffect(() => {
    if (!isPanelVisible || isMobile || !isPanelPinned) return;
    restoreReadingPositionAfterLayoutChange();
  }, [
    isMobile,
    isPanelPinned,
    isPanelVisible,
    panelWidth,
    restoreReadingPositionAfterLayoutChange,
  ]);

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

  if (!isPanelVisible || !activeBookKey || !bookState) return null;

  const baseUrl = bookState.baseUrl;
  const sessionId = bookState.sessionId;
  const rows = bookState.rows;

  const saveCurrentRow = async () => {
    if (!selectedRow || !baseUrl) return;
    setSaving(true);
    setMessage('');
    try {
      const payload = await saveReviewRow(baseUrl, sessionId, selectedRow.id, edit);
      const savedHtml = payload.current_html ?? sanitizeInlineHtml(edit.current_html);
      const nextRow = {
        ...selectedRow,
        ...edit,
        current_html: savedHtml,
        edited: savedHtml !== selectedRow.cn_html,
        updated_at: payload.updated_at || selectedRow.updated_at,
      };
      updateRow(activeBookKey, nextRow);
      setEdit((current) => ({ ...current, current_html: savedHtml }));
      const session = await sidecarApi<ReviewSessionPayload>(
        baseUrl,
        '/api/session',
        {},
        sessionId,
      );
      setBookData(activeBookKey, { session });
      setMessage(`已保存 ${selectedRow.id}`);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : String(error));
    } finally {
      setSaving(false);
    }
  };

  const retranslateCurrentRow = async () => {
    if (!selectedRow || !baseUrl) return;
    setRetranslating(true);
    setMessage('');
    setCandidateHtml('');
    try {
      const payload = await retranslateReviewRow(
        baseUrl,
        sessionId,
        selectedRow.id,
        retranslateInstruction,
      );
      setCandidateHtml(payload.candidate_html || '');
      setMessage(`重翻完成，命中术语 ${payload.glossary_matches?.length || 0} 条。`);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : String(error));
    } finally {
      setRetranslating(false);
    }
  };

  const generateFeedback = async () => {
    if (!baseUrl) return;
    setMessage('');
    try {
      const payload = await generateReviewFeedback(baseUrl, sessionId);
      setMessage(`反馈已生成：${payload.feedback_md || payload.feedback_jsonl || ''}`);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : String(error));
    }
  };

  const exportEpub = async () => {
    if (!baseUrl) return;
    setExporting(true);
    setExportInfo('');
    setMessage('');
    try {
      const payload = await exportReviewedEpub(baseUrl, sessionId);
      const downloadUrl = payload.download_url
        ? new URL(payload.download_url, baseUrl).toString()
        : '';
      setExportInfo(downloadUrl || payload.output || '');
      if (downloadUrl && !appService?.isDesktopApp) {
        await downloadReviewedEpub(downloadUrl);
      }
      setMessage(`已导出：${payload.output || downloadUrl}`);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : String(error));
    } finally {
      setExporting(false);
    }
  };

  const insertIntoCurrentHtml = (
    buildInsert: (selectedText: string) => {
      html: string;
      selectStart?: number;
      selectEnd?: number;
    } | null,
  ) => {
    const editor = cnEditorRef.current;
    const source = edit.current_html;
    const selectionStart = editor?.selectionStart ?? source.length;
    const selectionEnd = editor?.selectionEnd ?? source.length;
    const selectedText = source.slice(selectionStart, selectionEnd);
    const insert = buildInsert(selectedText);
    if (insert === null) return;
    const nextHtml = `${source.slice(0, selectionStart)}${insert.html}${source.slice(selectionEnd)}`;
    setEdit((current) => ({ ...current, current_html: nextHtml }));
    setPreviewOpen(true);
    requestAnimationFrame(() => {
      editor?.focus();
      const selectStart = selectionStart + (insert.selectStart ?? insert.html.length);
      const selectEnd = selectionStart + (insert.selectEnd ?? insert.html.length);
      editor?.setSelectionRange(selectStart, selectEnd);
    });
  };

  const insertRuby = () => {
    insertIntoCurrentHtml((selectedText) => {
      const baseText = selectedText || '文字';
      const rubyText = '注音';
      const baseHtml = escapeInlineHtml(baseText);
      const prefix = `<ruby><rb>${baseHtml}</rb><rt>`;
      const html = `${prefix}${rubyText}</rt></ruby>`;
      if (selectedText) {
        return {
          html,
          selectStart: prefix.length,
          selectEnd: prefix.length + rubyText.length,
        };
      }
      const baseStart = '<ruby><rb>'.length;
      return {
        html,
        selectStart: baseStart,
        selectEnd: baseStart + baseText.length,
      };
    });
  };

  const insertAnnotation = () => {
    insertIntoCurrentHtml((selectedText) => {
      const noteText = '注释';
      const baseHtml = selectedText ? escapeInlineHtml(selectedText) : '';
      const prefix = `${baseHtml}<span class="review-annotation">（注：`;
      const html = `${prefix}${noteText}）</span>`;
      return {
        html,
        selectStart: prefix.length,
        selectEnd: prefix.length + noteText.length,
      };
    });
  };

  return (
    <>
      {isMobile && (
        <Overlay
          className={clsx('z-[45]', viewSettings?.isEink ? '' : 'bg-black/50 sm:bg-black/20')}
          onDismiss={() => void closePanel()}
        />
      )}
      <aside
        ref={panelRef}
        className={clsx(
          'review-panel-container flex min-w-72 select-none flex-col',
          isDocked && 'shrink-0',
          'font-sans text-base font-normal transition-[padding-top] duration-300 sm:text-sm',
          (isDocked || isMobile) && 'full-height',
          viewSettings?.isEink ? 'bg-base-100' : 'bg-base-200',
          appService?.hasRoundedWindow && 'rounded-window-top-right rounded-window-bottom-right',
          isDocked ? 'z-20' : 'z-[45] shadow-2xl',
          isDocked && 'border-base-300 border-s',
          !isDocked && viewSettings?.isEink && 'border-base-content border-s',
          isMobile && 'bottom-0 max-h-[92vh] rounded-t-2xl',
        )}
        role='group'
        aria-label='审校'
        style={{
          width: isMobile ? '100%' : panelWidth,
          maxWidth: isMobile ? '100%' : `${MAX_REVIEW_PANEL_WIDTH * 100}%`,
          height: !isMobile && !isPanelPinned ? panelHeight : undefined,
          maxHeight:
            !isMobile && !isPanelPinned
              ? `calc(100vh - ${FLOATING_PANEL_MARGIN * 2}px)`
              : undefined,
          position: isMobile ? 'fixed' : isPanelPinned ? 'relative' : 'absolute',
          left: !isMobile && !isPanelPinned ? `${panelPosition.x}px` : undefined,
          top: !isMobile && !isPanelPinned ? `${panelPosition.y}px` : undefined,
          right: isMobile ? 0 : undefined,
          paddingTop: `${panelTopInset}px`,
        }}
      >
        <div
          className={clsx(
            'drag-bar absolute -start-2 top-0 h-full w-0.5 cursor-col-resize bg-transparent p-2',
            isMobile && 'hidden',
          )}
          role='slider'
          tabIndex={0}
          aria-label='调整审校面板宽度'
          aria-orientation='horizontal'
          aria-valuenow={parseFloat(panelWidth)}
          onPointerDown={floatingPanelEnabled ? handleFloatingWidthPointerDown : undefined}
          onMouseDown={floatingPanelEnabled ? undefined : handleDockedResizeStart}
          onTouchStart={floatingPanelEnabled ? undefined : handleDockedResizeStart}
          onKeyDown={handleResizeKeyDown}
        />
        {!isMobile && !isPanelPinned ? (
          <div
            className='drag-bar absolute inset-x-0 -bottom-2 h-0.5 cursor-row-resize bg-transparent p-2'
            role='slider'
            tabIndex={0}
            aria-label='调整审校面板高度'
            aria-orientation='vertical'
            aria-valuenow={parseFloat(panelHeight)}
            onPointerDown={handleFloatingHeightPointerDown}
            onKeyDown={handleFloatingHeightKeyDown}
          />
        ) : null}
        <header
          className={clsx(
            'border-base-300 flex min-h-12 shrink-0 items-center gap-2 border-b px-3 py-2',
            !isPanelPinned && !isMobile && 'cursor-move',
          )}
          onPointerDown={handlePanelDragStart}
        >
          <div className='min-w-0 flex-1'>
            <h2 className='text-sm font-semibold break-words'>审校模式</h2>
            <p className='text-base-content/60 text-xs break-words'>
              {bookTitle} · {rows.length} 段 · 已编辑 {bookState.session?.touched_count || 0} · 标记{' '}
              {bookState.session?.marked_count || 0}
            </p>
          </div>
          <button
            type='button'
            className='btn btn-ghost h-8 min-h-8 w-8 shrink-0 rounded-full p-0'
            title={isPanelPinned ? '取消固定：浮动面板不挤开正文' : '固定面板：挤开正文'}
            onClick={handleTogglePanelPin}
          >
            {isPanelPinned ? <PinOff className='h-4 w-4' /> : <Pin className='h-4 w-4' />}
          </button>
          <button
            type='button'
            className='btn btn-ghost h-8 min-h-8 w-8 shrink-0 rounded-full p-0'
            title={_('Close')}
            onClick={() => void closePanel()}
          >
            <X className='h-4 w-4' />
          </button>
        </header>

        <div className='min-h-0 flex-1 overflow-auto p-3'>
          <div className='grid gap-3'>
            {bookState.loading ? (
              <div className='eink-bordered border-base-300 bg-base-100 rounded-md border p-3 text-sm'>
                正在启动审校器并读取当前 EPUB……
              </div>
            ) : null}
            {bookState.error ? (
              <div
                className='eink-bordered border-error bg-base-100 text-error rounded-md border p-3 text-sm'
                role='alert'
              >
                {bookState.error}
              </div>
            ) : null}

            <div className='flex flex-wrap gap-2'>
              <PanelButton
                onClick={generateFeedback}
                icon={<FileText className='h-4 w-4' />}
                disabled={!baseUrl || hasUnsavedEdit}
              >
                生成反馈
              </PanelButton>
              <PanelButton
                onClick={exportEpub}
                icon={<Download className='h-4 w-4' />}
                disabled={!baseUrl || exporting || hasUnsavedEdit}
                variant='primary'
              >
                {exporting ? '导出中' : '导出 EPUB'}
              </PanelButton>
            </div>
            {hasUnsavedEdit ? (
              <p className='text-base-content/70 text-xs'>请先保存当前段落，再生成反馈或导出。</p>
            ) : null}

            {message ? (
              <div
                className='eink-bordered border-base-300 bg-base-100 rounded-md border p-3 text-sm'
                role='status'
                aria-live='polite'
              >
                {message}
              </div>
            ) : null}
            {exportInfo ? (
              <button
                type='button'
                className='eink-bordered border-base-300 bg-base-100 hover:bg-base-200 rounded-md border p-3 text-start text-sm'
                onClick={() => {
                  if (appService?.isDesktopApp) {
                    openExternalUrl(exportInfo);
                  } else {
                    void downloadReviewedEpub(exportInfo);
                  }
                }}
              >
                <span className='font-semibold'>
                  {appService?.isDesktopApp ? '在浏览器下载导出文件' : '重新下载导出文件'}
                </span>
                <span className='text-base-content/60 mt-1 block break-all text-xs'>
                  {exportInfo}
                </span>
              </button>
            ) : null}

            {!selectedRow ? (
              <div className='eink-bordered border-base-300 bg-base-100 rounded-md border p-5 text-center text-sm'>
                {rows.length
                  ? '请选择一段开始审校。'
                  : '当前 EPUB 没有识别到可审校的中日双语段落。'}
              </div>
            ) : (
              <>
                <section className='eink-bordered border-base-300 bg-base-100 rounded-md border p-3'>
                  <div className='text-base-content/60 mb-2 flex flex-wrap items-center justify-between gap-2 text-xs'>
                    <span className='min-w-0 break-words'>
                      {selectedRow.id} · {rowTitle(selectedRow)}
                    </span>
                    <span className='shrink-0 break-words'>
                      JP P{selectedRow.ja_p_index} / CN P{selectedRow.cn_p_index}
                    </span>
                  </div>
                  <div
                    className='prose prose-sm max-w-none select-text leading-8 break-words'
                    dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(selectedRow.jp_html) }}
                  />
                </section>

                <section className='grid gap-3'>
                  <div className='flex flex-wrap items-center justify-between gap-2'>
                    <h3 className='text-sm font-semibold'>修改中文译文</h3>
                    <div className='flex flex-wrap justify-end gap-2'>
                      <PanelButton
                        onClick={insertRuby}
                        disabled={!canUseRowActions}
                        icon={<Languages className='h-4 w-4' />}
                      >
                        注音
                      </PanelButton>
                      <PanelButton
                        onClick={insertAnnotation}
                        disabled={!canUseRowActions}
                        icon={<MessageSquarePlus className='h-4 w-4' />}
                      >
                        注释
                      </PanelButton>
                      <PanelButton
                        onClick={saveCurrentRow}
                        disabled={!canUseRowActions || saving}
                        icon={<Save className='h-4 w-4' />}
                        variant='primary'
                      >
                        {saving ? '保存中' : '保存'}
                      </PanelButton>
                    </div>
                  </div>
                  <textarea
                    ref={cnEditorRef}
                    className='textarea textarea-bordered eink-bordered min-h-44 min-w-0 rounded-md font-mono text-sm'
                    value={edit.current_html}
                    onChange={(event) =>
                      setEdit((current) => ({ ...current, current_html: event.target.value }))
                    }
                  />
                  <div className='eink-bordered border-base-300 bg-base-100 rounded-md border p-3'>
                    <button
                      type='button'
                      className='flex w-full items-center justify-between gap-2 text-start'
                      onClick={() => setPreviewOpen((value) => !value)}
                    >
                      <span className='text-base-content/60 text-xs'>译文预览</span>
                      <span className='text-base-content/60 text-xs'>
                        {previewOpen ? '收起' : '展开'}
                      </span>
                    </button>
                    {previewOpen ? (
                      <div
                        className='prose prose-sm mt-2 max-w-none select-text leading-7 break-words'
                        dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(edit.current_html) }}
                      />
                    ) : null}
                  </div>
                  <section className='eink-bordered border-base-300 bg-base-100 grid gap-3 rounded-md border p-3'>
                    <div className='flex items-start justify-between gap-3'>
                      <div className='min-w-0'>
                        <h4 className='text-sm font-semibold'>问题记录</h4>
                        <p className='text-base-content/60 mt-1 text-xs'>
                          标签用于归类，备注只写本段，长期规则建议只写可复用口径。
                        </p>
                      </div>
                      <label className='flex shrink-0 items-center gap-2 text-xs'>
                        <input
                          type='checkbox'
                          className='checkbox checkbox-sm'
                          checked={edit.marked}
                          onChange={(event) =>
                            setEdit((current) => ({ ...current, marked: event.target.checked }))
                          }
                        />
                        纳入反馈
                      </label>
                    </div>
                    <div className='grid gap-2 sm:grid-cols-2'>
                      <label className='grid gap-1 text-xs font-medium'>
                        问题类型
                        <select
                          className='select select-bordered eink-bordered h-9 min-h-9 rounded-md text-sm'
                          value={edit.issue_type}
                          onChange={(event) =>
                            setEdit((current) => ({
                              ...current,
                              issue_type: event.target.value,
                            }))
                          }
                        >
                          <option value=''>未选择</option>
                          <option value='误译'>误译</option>
                          <option value='漏译'>漏译</option>
                          <option value='术语不一致'>术语不一致</option>
                          <option value='人名/地名问题'>人名/地名问题</option>
                          <option value='翻译腔'>翻译腔</option>
                          <option value='角色口吻不对'>角色口吻不对</option>
                          <option value='语序不顺'>语序不顺</option>
                          <option value='标点/格式'>标点/格式</option>
                          <option value='其他'>其他</option>
                        </select>
                      </label>
                      <label className='grid gap-1 text-xs font-medium'>
                        严重程度
                        <select
                          className='select select-bordered eink-bordered h-9 min-h-9 rounded-md text-sm'
                          value={edit.severity}
                          onChange={(event) =>
                            setEdit((current) => ({ ...current, severity: event.target.value }))
                          }
                        >
                          <option value=''>未选择</option>
                          <option value='轻微'>轻微</option>
                          <option value='中等'>中等</option>
                          <option value='严重'>严重</option>
                        </select>
                      </label>
                    </div>
                    <label className='grid gap-1 text-xs font-medium'>
                      标签
                      <input
                        className='input input-bordered eink-bordered h-9 min-w-0 rounded-md text-sm'
                        value={edit.tags}
                        onChange={(event) =>
                          setEdit((current) => ({ ...current, tags: event.target.value }))
                        }
                        placeholder='检索归类：术语、口吻、格式，可用逗号分隔'
                      />
                    </label>
                    <label className='grid gap-1 text-xs font-medium'>
                      本段备注
                      <textarea
                        className='textarea textarea-bordered eink-bordered min-h-16 min-w-0 rounded-md text-sm'
                        value={edit.comment}
                        onChange={(event) =>
                          setEdit((current) => ({ ...current, comment: event.target.value }))
                        }
                        placeholder='只记录当前段哪里有问题、为什么这样改'
                      />
                    </label>
                    <label className='grid gap-1 text-xs font-medium'>
                      长期规则建议
                      <textarea
                        className='textarea textarea-bordered eink-bordered min-h-16 min-w-0 rounded-md text-sm'
                        value={edit.learn_note}
                        onChange={(event) =>
                          setEdit((current) => ({ ...current, learn_note: event.target.value }))
                        }
                        placeholder='只写后续段落也应遵守的规则或口吻偏好'
                      />
                    </label>
                  </section>
                </section>

                <section className='border-base-300 grid gap-3 border-t pt-3'>
                  <h3 className='text-sm font-semibold'>API 重翻</h3>
                  <textarea
                    className='textarea textarea-bordered eink-bordered min-h-16 min-w-0 rounded-md text-sm'
                    value={retranslateInstruction}
                    onChange={(event) => setRetranslateInstruction(event.target.value)}
                    placeholder='额外要求，可留空'
                  />
                  <PanelButton
                    onClick={retranslateCurrentRow}
                    disabled={!canUseRowActions || retranslating}
                    icon={<Sparkles className='h-4 w-4' />}
                  >
                    {retranslating ? '重翻中' : '重翻本段'}
                  </PanelButton>
                  {candidateHtml ? (
                    <div className='eink-bordered border-base-300 bg-base-100 rounded-md border p-3'>
                      <div
                        className='prose prose-sm max-w-none select-text leading-7 break-words'
                        dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(candidateHtml) }}
                      />
                      <div className='mt-3'>
                        <PanelButton
                          onClick={() =>
                            setEdit((current) => ({ ...current, current_html: candidateHtml }))
                          }
                          icon={<Check className='h-4 w-4' />}
                          variant='primary'
                        >
                          应用候选到编辑框
                        </PanelButton>
                      </div>
                    </div>
                  ) : null}
                </section>
              </>
            )}

            <GptConfigPanel
              baseUrl={baseUrl}
              sessionId={sessionId}
              form={gptForm}
              setForm={setGptForm}
              onSaved={(config) => setBookData(activeBookKey, { gptConfig: config })}
            />
            <GlossaryPanel
              baseUrl={baseUrl}
              sessionId={sessionId}
              path={gptForm.glossary_path}
              setPath={(path) => setGptForm((current) => ({ ...current, glossary_path: path }))}
            />
          </div>
        </div>
      </aside>
    </>
  );
};

export default ReviewPanel;
