import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
import type { CanvasState, ProjectEditableState } from "@dada/shared-contracts";

import { ConflictExportGuard, ProjectAutoSaveQueue, type ProjectSaveStatus } from "./project-autosave.js";
import {
  CanvasEditHistory,
  createEditorCanvasState,
  defaultBackgroundAdjustments,
  resetBackgroundAdjustments,
  switchBackground,
  updateBackgroundAdjustments,
} from "./editor-canvas.js";
import { CanvasElementController, createStaticStickerElement, type CanvasElementIdentity, type CanvasLayerCommand, type CanvasPoint, type CanvasRect } from "./editor-elements.js";
import { EditorStage } from "./editor-stage.js";
import { useDialogFocus } from "./dialog-focus.js";
import { ExportDialog } from "./export-dialog.js";
import {
  completePendingTextForExport,
  composeCanvasExport,
  downloadExportBlob,
  exportFilename,
  resolveExportSettings,
  saveLatestExport,
  type ExportFormat,
} from "./export-compositor.js";
import { runExportFlow, type ExportFlowStatus } from "./export-flow.js";
import { TextInspector } from "./text-inspector.js";
import { createBrowserArchivedFontLoader, type ArchivedFontLoader, type ArchivedFontStatus } from "./text-font-loader.js";
import {
  fontIdForTextElement,
  fontOption,
  P0A_TEXT_TEMPLATES,
  TextEditSession,
  createTextTemplateElement,
  type TextStylePatch,
  type TextTemplateCategory,
  type TextTemplateDefinition,
} from "./text-assets.js";
import { TextTemplatePanel } from "./text-template-panel.js";
import { ColorCardPanel } from "./color-card-panel.js";
import { DynamicInspector } from "./dynamic-inspector.js";
import { DynamicStickerPanel } from "./dynamic-sticker-panel.js";
import {
  LocationConsentGate,
  P0A_DYNAMIC_STICKERS,
  browserGeolocate,
  createDynamicStickerElement,
  overrideDynamicStickerValue,
  type DynamicTemplateId,
} from "./dynamic-provider.js";
import { dynamicFontOptionsFor } from "./dynamic-render-models.js";
import { P0A_STATIC_STICKER_CATALOG, stickerWindow, type StaticStickerCatalogItem } from "./static-sticker-catalog.js";
import {
  createColorCardElement,
  extractPaletteFromImage,
  type ColorCardDefinition,
} from "./palette-provider.js";

import "./editor-page.css";

type Ratio = CanvasState["ratio"];
type CanvasElement = CanvasState["elements"][number];
type EditorAssetPanel = "background" | "color" | "dynamic" | "history" | "stickers" | "text";

interface TextEditState {
  draft: CanvasElement;
  elementId: string;
  originalTemplateId: string;
}

interface EditorSession {
  csrf_token: string;
  user: { creator_name: string; social_id: string; user_id: string };
}

interface LocationDialogState {
  error?: string;
  manualValue: string;
  pending: boolean;
}

interface EditorExportResult {
  blob: Blob | null;
  format: ExportFormat;
  quality?: number;
  status: ExportFlowStatus;
}

interface EditorProject {
  canvas_state?: CanvasState;
  created_at: string;
  current_image_id: string | null;
  images: Array<{ created_at: string; generation_id: string; image_id: string }>;
  name: string;
  pixel_height?: number;
  pixel_width?: number;
  project_id: string;
  ratio: Ratio;
  state_version: number;
}

async function readEditorJson<T>(url: string): Promise<T> {
  const response = await fetch(url, { credentials: "same-origin" });
  if (response.status === 401) {
    window.dispatchEvent(new Event("dada:session-invalid"));
    throw new Error("session_invalid");
  }
  if (!response.ok) throw new Error("editor_request_failed");
  return response.json() as Promise<T>;
}

function formatDate(value: string) {
  return new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(value));
}

function newElementIdentity(): CanvasElementIdentity {
  return { createdAt: new Date().toISOString(), elementId: crypto.randomUUID() };
}

function saveStatusLabel(status: ProjectSaveStatus) {
  return { conflicted: "版本冲突", dirty: "有未保存修改", failed: "保存失败", saved: "已保存", saving: "正在保存" }[status];
}

function EditorDialog(props: { children: ReactNode; className?: string; labelledBy: string; onClose: () => void }) {
  const dialogRef = useDialogFocus(props.onClose);
  return <div className="editor-dialog-backdrop"><section aria-labelledby={props.labelledBy} aria-modal="true" className={props.className ?? "editor-confirm"} ref={dialogRef} role="dialog" tabIndex={-1}>{props.children}</section></div>;
}

export function EditorPage({ projectId }: { projectId: string }) {
  const [session, setSession] = useState<EditorSession>();
  const [project, setProject] = useState<EditorProject>();
  const [canvasState, setCanvasState] = useState<CanvasState>();
  const [draftAdjustments, setDraftAdjustments] = useState(defaultBackgroundAdjustments);
  const [saveStatus, setSaveStatus] = useState<ProjectSaveStatus>("saved");
  const [pendingBackground, setPendingBackground] = useState<string>();
  const [notice, setNotice] = useState("");
  const [activePanel, setActivePanel] = useState<EditorAssetPanel>("background");
  const [stickerScrollTop, setStickerScrollTop] = useState(0);
  const [stickerCatalog, setStickerCatalog] = useState<StaticStickerCatalogItem[]>(P0A_STATIC_STICKER_CATALOG);
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
  const [guides, setGuides] = useState<string[]>([]);
  const [multiMode, setMultiMode] = useState(false);
  const [candidateMenu, setCandidateMenu] = useState<{ elements: CanvasElement[]; point: CanvasPoint }>();
  const [templateCategory, setTemplateCategory] = useState<TextTemplateCategory>();
  const [templateQuery, setTemplateQuery] = useState("");
  const [recentTextIds, setRecentTextIds] = useState<string[]>([]);
  const [fontStatuses, setFontStatuses] = useState<Record<string, ArchivedFontStatus>>({});
  const [textEdit, setTextEdit] = useState<TextEditState>();
  const [locationDialog, setLocationDialog] = useState<LocationDialogState>();
  const [exportOpen, setExportOpen] = useState(false);
  const [exportBusy, setExportBusy] = useState(false);
  const [exportResult, setExportResult] = useState<EditorExportResult>();
  const queueRef = useRef<ProjectAutoSaveQueue | undefined>(undefined);
  const historyRef = useRef<CanvasEditHistory | undefined>(undefined);
  const elementControllerRef = useRef<CanvasElementController | undefined>(undefined);
  const dragRef = useRef<{ base: CanvasState; last: CanvasState; selectedIds: string[] } | undefined>(undefined);
  const opacityGestureRef = useRef<{ base: CanvasState; last: CanvasState; selectedIds: string[] } | undefined>(undefined);
  const clipboardRef = useRef<CanvasElement[]>([]);
  const fontLoaderRef = useRef<ArchivedFontLoader | undefined>(undefined);
  const conflictExportGuardRef = useRef(new ConflictExportGuard());
  const candidateMenuRef = useRef<HTMLDivElement | null>(null);
  const candidateTriggerRef = useRef<HTMLButtonElement | null>(null);

  useEffect(() => {
    let active = true;
    Promise.all([
      readEditorJson<EditorSession>("/api/v1/auth/session"),
      readEditorJson<EditorProject>(`/api/v1/projects/${projectId}`),
    ]).then(([nextSession, nextProject]) => {
      if (!active) return;
      const initial = nextProject.canvas_state ?? createEditorCanvasState({ assetId: nextProject.current_image_id, ratio: nextProject.ratio });
      setSession(nextSession);
      setProject(nextProject);
      setCanvasState(initial);
      setDraftAdjustments(initial.background.adjustments);
      historyRef.current = new CanvasEditHistory(initial);
      elementControllerRef.current = new CanvasElementController(initial);
    }).catch(() => { if (active) setNotice("编辑器暂时无法读取项目"); });
    return () => { active = false; };
  }, [projectId]);

  useEffect(() => {
    if (!session) return;
    let active = true;
    readEditorJson<{ items: Array<{ asset_id: string }> }>("/api/v1/assets/recent?asset_kind=text_template")
      .then((response) => { if (active) setRecentTextIds(response.items.map((item) => item.asset_id)); })
      .catch(() => { if (active) setRecentTextIds([]); });
    return () => { active = false; };
  }, [session?.user.user_id]);

  useEffect(() => {
    let active = true;
    readEditorJson<{ items: StaticStickerCatalogItem[] }>("/api/v1/static-stickers/current")
      .then((response) => {
        if (!active) return;
        const uploaded = response.items.filter((item) => item.enabled && item.origin === "admin_uploaded");
        setStickerCatalog([...P0A_STATIC_STICKER_CATALOG, ...uploaded].sort((left, right) => left.part - right.part || left.order - right.order || left.stable_id.localeCompare(right.stable_id)));
      })
      .catch(() => { if (active) setStickerCatalog(P0A_STATIC_STICKER_CATALOG); });
    return () => { active = false; };
  }, []);

  useEffect(() => {
    if (!project || !session || !canvasState) return undefined;
    const queue = new ProjectAutoSaveQueue({
      initialState: { canvas_state: canvasState, name: project.name },
      initialVersion: project.state_version,
      onStatus: setSaveStatus,
      onSaved: (_snapshot, stateVersion) => {
        setProject((current) => current ? { ...current, state_version: stateVersion } : current);
      },
      save: async (snapshot: ProjectEditableState, stateVersion, operationId) => {
        const response = await fetch(`/api/v1/projects/${projectId}/state`, {
          body: JSON.stringify(snapshot),
          credentials: "same-origin",
          headers: { "Content-Type": "application/json", "Idempotency-Key": operationId, "If-Match": String(stateVersion), "X-CSRF-Token": session.csrf_token },
          method: "PUT",
        });
        if (response.status === 412) throw new Error("project_state_conflict");
        if (!response.ok) throw new Error("project_save_failed");
        const saved = await response.json() as { state_version: number };
        return { stateVersion: saved.state_version };
      },
    });
    queueRef.current = queue;
    return () => { queue.dispose(); if (queueRef.current === queue) queueRef.current = undefined; };
  }, [canvasState === undefined, project?.created_at, projectId, session?.csrf_token]);

  useEffect(() => {
    if (!canvasState) return;
    for (const element of canvasState.elements) {
      const options = element.type === "text_template"
        ? [fontIdForTextElement(element)].map((fontId) => fontId ? fontOption(fontId) : undefined).filter((option) => option !== undefined)
        : element.type === "dynamic_sticker" ? dynamicFontOptionsFor(element.template_or_asset_id) : [];
      for (const option of options) void ensureFont(option.fontId, option.url);
    }
  }, [canvasState?.elements.map((element) => `${element.element_id}:${element.font_override ?? ""}:${element.template_or_asset_id}`).join("|")]);

  useEffect(() => {
    if (activePanel !== "dynamic") return;
    const options = P0A_DYNAMIC_STICKERS.flatMap((definition) => dynamicFontOptionsFor(definition.templateId));
    for (const option of options) void ensureFont(option.fontId, option.url);
  }, [activePanel]);

  useEffect(() => {
    if (candidateMenu) candidateMenuRef.current?.querySelector<HTMLButtonElement>('[role="menuitem"]')?.focus();
  }, [candidateMenu]);

  useEffect(() => {
    if (!canvasState || selectedIds.length !== 1) {
      setTextEdit(undefined);
      return;
    }
    const element = canvasState.elements.find((candidate) => candidate.element_id === selectedIds[0]);
    if (!element || element.type !== "text_template") {
      setTextEdit(undefined);
      return;
    }
    setTextEdit((current) => current?.elementId === element.element_id ? current : {
      draft: structuredClone(element), elementId: element.element_id, originalTemplateId: element.template_or_asset_id,
    });
  }, [canvasState, selectedIds.join("|")]);

  async function ensureFont(fontId: string, url: string, retry = false) {
    const current = fontStatuses[fontId];
    if (current === "ready" || (current === "unavailable" && !retry)) return current;
    const loader = fontLoaderRef.current ?? createBrowserArchivedFontLoader();
    fontLoaderRef.current = loader;
    setFontStatuses((statuses) => ({ ...statuses, [fontId]: "loading" }));
    const status = retry ? await loader.retry({ fontId, url }) : await loader.ensure({ fontId, url });
    setFontStatuses((statuses) => ({ ...statuses, [fontId]: status }));
    return status;
  }

  async function retryTextFonts() {
    const available = P0A_TEXT_TEMPLATES.filter((template) => template.available && template.fontUrl);
    await Promise.all(available.map((template) => ensureFont(template.defaultFontId, template.fontUrl!, true)));
  }

  function commitCanvas(next: CanvasState) {
    if (!project || saveStatus === "conflicted") return;
    historyRef.current?.commit(next);
    elementControllerRef.current?.replaceState(next);
    setSelectedIds(elementControllerRef.current?.selectedIds ?? []);
    setCanvasState(next);
    setDraftAdjustments(next.background.adjustments);
    queueRef.current?.commit({ canvas_state: next, name: project.name });
    setTextEdit(undefined);
  }

  function applyPreview() {
    if (!canvasState) return;
    commitCanvas(updateBackgroundAdjustments(canvasState, draftAdjustments));
    setNotice("底图调整已提交");
  }

  function undo() {
    const previous = historyRef.current?.undo();
    if (previous) {
      elementControllerRef.current?.replaceState(previous);
      setSelectedIds(elementControllerRef.current?.selectedIds ?? []);
      setCanvasState(previous);
      setDraftAdjustments(previous.background.adjustments);
      if (project) queueRef.current?.commit({ canvas_state: previous, name: project.name });
      setTextEdit(undefined);
    }
  }

  function redo() {
    const next = historyRef.current?.redo();
    if (next) {
      elementControllerRef.current?.replaceState(next);
      setSelectedIds(elementControllerRef.current?.selectedIds ?? []);
      setCanvasState(next);
      setDraftAdjustments(next.background.adjustments);
      if (project) queueRef.current?.commit({ canvas_state: next, name: project.name });
      setTextEdit(undefined);
    }
  }

  async function paletteForAsset(assetId: string) {
    const image = new Image();
    const loaded = new Promise<HTMLImageElement>((resolve, reject) => {
      image.onload = () => resolve(image);
      image.onerror = () => reject(new Error("palette_image_unavailable"));
    });
    image.src = `/api/v1/private-assets/projects/${encodeURIComponent(projectId)}/images/${encodeURIComponent(assetId)}`;
    return extractPaletteFromImage(await loaded);
  }

  async function confirmBackground() {
    if (!canvasState || !pendingBackground) return;
    try {
      const palette = await paletteForAsset(pendingBackground);
      commitCanvas(switchBackground(canvasState, pendingBackground, palette));
      setPendingBackground(undefined);
      setNotice("已更换底图，覆盖元素保留，底图处理已重置");
    } catch {
      setNotice("新底图无法读取，未更换底图或刷新色卡");
    }
  }

  function controllerForCurrent() {
    const controller = elementControllerRef.current;
    if (!controller) return undefined;
    if (controller.selectedIds.length !== selectedIds.length || controller.selectedIds.some((elementId, index) => elementId !== selectedIds[index])) controller.selectIds(selectedIds);
    return controller;
  }

  function commitElementOperation(controller: CanvasElementController, message: string) {
    const next = controller.value;
    setSelectedIds(controller.selectedIds);
    commitCanvas(next);
    setGuides([]);
    setCandidateMenu(undefined);
    setNotice(message);
  }

  function addSticker(sticker: StaticStickerCatalogItem) {
    const controller = controllerForCurrent();
    if (!controller || !canvasState) return;
    try {
      controller.add(createStaticStickerElement({
        assetId: sticker.stable_id,
        identity: newElementIdentity(),
        position: { x: 0.5, y: 0.5 },
        resourceVersion: sticker.resource_version,
        zIndex: canvasState.elements.length,
      }));
      commitElementOperation(controller, "贴纸已加入画布");
    } catch (error) {
      if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素，请先删除现有元素。");
      else setNotice("贴纸未能加入画布");
    }
  }

  async function addColorCard(definition: ColorCardDefinition) {
    const assetId = canvasState?.background.asset_id;
    const controller = controllerForCurrent();
    if (!assetId || !canvasState || !controller) return;
    try {
      const palette = await paletteForAsset(assetId);
      controller.add(createColorCardElement(definition, palette, newElementIdentity(), canvasState.elements.length));
      commitElementOperation(controller, "色卡已按原始底图加入画布");
    } catch (error) {
      if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素，请先删除现有元素。");
      else setNotice("无法从原始底图稳定提取五色，色卡未加入画布");
    }
  }

  async function addDynamicSticker(templateId: DynamicTemplateId, location?: { formattedValue: string; latitude?: number; longitude?: number }) {
    const controller = controllerForCurrent();
    if (!canvasState || !controller || !session) return;
    if (templateId === "DYN012") {
      const font = fontOption("FONT081");
      if (!font || await ensureFont(font.fontId, font.url) !== "ready") {
        setNotice("DYN012 的 FONT081 替代字体不可用，未使用系统字体替代。");
        return;
      }
    }
    try {
      controller.add(createDynamicStickerElement(templateId, {
        ...(location ? { location } : {}),
        now: new Date(),
        profile: { creatorName: session.user.creator_name, socialId: session.user.social_id },
      }, newElementIdentity(), canvasState.elements.length));
      commitElementOperation(controller, "动态值已确认并加入画布");
      setLocationDialog(undefined);
    } catch (error) {
      if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素，请先删除现有元素。");
      else setNotice("动态贴纸未能加入画布");
    }
  }

  function chooseDynamicSticker(templateId: DynamicTemplateId) {
    if (templateId === "DYN004") {
      setLocationDialog({ manualValue: "", pending: false });
      return;
    }
    void addDynamicSticker(templateId);
  }

  async function confirmAutomaticLocation() {
    if (!session) return;
    setLocationDialog((current) => {
      if (!current) return current;
      const { error: _error, ...rest } = current;
      return { ...rest, pending: true };
    });
    const gate = new LocationConsentGate({
      geolocate: browserGeolocate,
      reverseGeocode: async (coordinates) => {
        const response = await fetch("/api/v1/location/reverse-geocode", {
          body: JSON.stringify(coordinates),
          credentials: "same-origin",
          headers: { "Content-Type": "application/json", "X-CSRF-Token": session.csrf_token },
          method: "POST",
        });
        if (!response.ok) throw new Error("dynamic_location_service_unavailable");
        const payload = await response.json() as { formatted_value: string };
        return payload.formatted_value;
      },
    });
    try {
      const result = await gate.confirm();
      await addDynamicSticker("DYN004", result);
    } catch {
      setLocationDialog((current) => current ? { ...current, error: "自动定位不可用，请改用手动地点贴纸。", pending: false } : current);
    }
  }

  function commitDynamicOverride(element: CanvasElement, value: string) {
    const controller = controllerForCurrent();
    if (!controller) return;
    try {
      controller.replaceElement(overrideDynamicStickerValue(element, value));
      commitElementOperation(controller, "动态贴纸显示文字已更新");
    } catch {
      setNotice("动态贴纸显示文字不能为空");
    }
  }

  async function recordRecentTextTemplate(templateId: string, resourceVersion: string) {
    if (!session) return;
    try {
      const response = await fetch("/api/v1/assets/recent", {
        body: JSON.stringify({ asset_id: templateId, asset_kind: "text_template", resource_version: resourceVersion }),
        credentials: "same-origin",
        headers: { "Content-Type": "application/json", "X-CSRF-Token": session.csrf_token },
        method: "POST",
      });
      if (!response.ok) return;
      setRecentTextIds((current) => [templateId, ...current.filter((id) => id !== templateId)].slice(0, 12));
    } catch {
      // Recent-use metadata does not roll back an otherwise successful canvas edit.
    }
  }

  async function addTextTemplate(template: TextTemplateDefinition) {
    if (!template.fontUrl || !canvasState) return;
    const status = await ensureFont(template.defaultFontId, template.fontUrl);
    if (status !== "ready") {
      setNotice("素材暂不可用，未使用系统字体替代。");
      return;
    }
    const controller = controllerForCurrent();
    if (!controller) return;
    try {
      controller.add(createTextTemplateElement(template, newElementIdentity(), canvasState.elements.length));
      commitElementOperation(controller, "文字模板已加入画布");
      void recordRecentTextTemplate(template.templateId, template.resourceVersion);
    } catch (error) {
      if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素，请先删除现有元素。");
      else setNotice("文字模板未能加入画布");
    }
  }

  function updateTextDraft(action: (session: TextEditSession) => void) {
    setTextEdit((current) => {
      if (!current) return current;
      try {
        const edit = new TextEditSession(current.draft, P0A_TEXT_TEMPLATES);
        action(edit);
        return { ...current, draft: edit.value };
      } catch {
        setNotice("文字参数不在允许范围内");
        return current;
      }
    });
  }

  async function changeTextTemplate(templateId: string) {
    const template = P0A_TEXT_TEMPLATES.find((candidate) => candidate.templateId === templateId);
    if (!template?.fontUrl) return;
    const status = await ensureFont(template.defaultFontId, template.fontUrl);
    if (status !== "ready") {
      setNotice("素材暂不可用，未使用系统字体替代。");
      return;
    }
    updateTextDraft((edit) => edit.switchTemplate(templateId));
  }

  async function changeTextFont(fontId: string | null) {
    if (!fontId) {
      updateTextDraft((edit) => edit.setStyle({ fontOverride: null }));
      return;
    }
    const option = fontOption(fontId);
    if (!option || await ensureFont(option.fontId, option.url) !== "ready") {
      setNotice("字体素材暂不可用，未使用系统字体替代。");
      return;
    }
    updateTextDraft((edit) => edit.setStyle({ fontOverride: fontId }));
  }

  function completeTextEdit() {
    if (!textEdit) return;
    try {
      const edit = new TextEditSession(textEdit.draft, P0A_TEXT_TEMPLATES);
      const complete = edit.complete();
      const controller = controllerForCurrent();
      if (!controller) return;
      controller.replaceElement(complete);
      commitElementOperation(controller, "文字编辑已完成");
      if (complete.template_or_asset_id !== textEdit.originalTemplateId) {
        void recordRecentTextTemplate(complete.template_or_asset_id, complete.resource_version);
      }
    } catch (error) {
      if (error instanceof Error && error.message === "text_content_required") setNotice("请输入文字内容或删除该元素。");
      else setNotice("文字编辑未能完成");
    }
  }

  function cancelTextEdit() {
    if (canvasState && textEdit) {
      const current = canvasState.elements.find((element) => element.element_id === textEdit.elementId);
      if (current) setTextEdit({ draft: structuredClone(current), elementId: current.element_id, originalTemplateId: current.template_or_asset_id });
    }
    setNotice("已取消未提交的文字修改");
  }

  function pendingTextDraft() {
    if (!canvasState || !textEdit) return undefined;
    const committed = canvasState.elements.find((element) => element.element_id === textEdit.elementId);
    return committed && JSON.stringify(committed) !== JSON.stringify(textEdit.draft) ? textEdit : undefined;
  }

  function openExportDialog() {
    setExportResult(undefined);
    setExportOpen(true);
  }

  async function executeExport(options: { format: ExportFormat; quality?: number }, reuseBlob?: Blob) {
    if (!canvasState || !project || !session) return;
    setExportBusy(true);
    try {
      let exportState = canvasState;
      const pending = reuseBlob ? undefined : pendingTextDraft();
      if (pending) {
        exportState = completePendingTextForExport(canvasState, pending.draft);
        commitCanvas(exportState);
        const completed = exportState.elements.find((element) => element.element_id === pending.elementId);
        if (completed && completed.template_or_asset_id !== pending.originalTemplateId) {
          void recordRecentTextTemplate(completed.template_or_asset_id, completed.resource_version);
        }
      }

      const queue = queueRef.current;
      const projectSaved = saveStatus !== "conflicted" && Boolean(queue && await queue.saveNow());
      const settings = resolveExportSettings({ format: options.format, ratio: exportState.ratio, ...(options.quality === undefined ? {} : { quality: options.quality }) });
      const result = await runExportFlow({
        compose: async () => reuseBlob ?? composeCanvasExport({
          canvasState: exportState,
          fontStatuses,
          format: options.format,
          projectId,
          ...(options.quality === undefined ? {} : { quality: options.quality }),
        }),
        download: (blob) => downloadExportBlob(blob, exportFilename(project.name, options.format)),
        persist: async (blob) => {
          if (!projectSaved || !queue) throw new Error("project_state_not_saved");
          await saveLatestExport({
            blob,
            csrfToken: session.csrf_token,
            format: options.format,
            height: settings.height,
            projectId,
            stateVersion: queue.stateVersion,
            width: settings.width,
          });
        },
      });
      setExportResult({ blob: result.blob, format: options.format, status: result.status, ...(options.quality === undefined ? {} : { quality: options.quality }) });
    } catch {
      setExportResult({ blob: null, format: options.format, status: "composition_failed", ...(options.quality === undefined ? {} : { quality: options.quality }) });
    } finally {
      setExportBusy(false);
    }
  }

  async function startExport(options: { format: ExportFormat; quality?: number }) {
    if (saveStatus !== "conflicted") {
      await executeExport(options);
      return;
    }
    const ran = await conflictExportGuardRef.current.run(() => executeExport(options));
    if (!ran) setNotice("版本冲突时仅允许导出本页版本一次");
  }

  async function retryExportDownload() {
    if (!exportResult?.blob) return;
    await executeExport({ format: exportResult.format, ...(exportResult.quality === undefined ? {} : { quality: exportResult.quality }) }, exportResult.blob);
  }

  function transformSelection(action: (controller: CanvasElementController) => void, message: string) {
    const controller = controllerForCurrent();
    if (!controller || selectedIds.length === 0) return;
    try {
      action(controller);
      commitElementOperation(controller, message);
    } catch (error) {
      if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素，请先删除现有元素。");
      else setNotice("对象操作未完成");
    }
  }

  function beginOpacityGesture() {
    if (!canvasState || selectedIds.length === 0) return;
    opacityGestureRef.current = { base: canvasState, last: canvasState, selectedIds: [...selectedIds] };
  }

  function changeOpacity(value: number) {
    const gesture = opacityGestureRef.current;
    if (!gesture) {
      transformSelection((controller) => { controller.setSelectedOpacity(value); }, "贴纸透明度已提交");
      return;
    }
    const previewController = new CanvasElementController(gesture.base);
    previewController.selectIds(gesture.selectedIds);
    previewController.setSelectedOpacity(value);
    gesture.last = previewController.value;
    setCanvasState(gesture.last);
  }

  function commitOpacityGesture() {
    const gesture = opacityGestureRef.current;
    if (!gesture) return;
    opacityGestureRef.current = undefined;
    if (gesture.last === gesture.base) return;
    commitCanvas(gesture.last);
    setNotice("贴纸透明度已提交");
  }

  function duplicateSelection() {
    transformSelection((controller) => controller.duplicateSelected(() => newElementIdentity()), "已复制选中对象");
  }

  function changeSelectionLayer(command: CanvasLayerCommand) {
    const labels: Record<CanvasLayerCommand, string> = { back: "已置底", backward: "已下移一层", forward: "已上移一层", front: "已置顶" };
    transformSelection((controller) => { controller.changeLayer(command); }, labels[command]);
  }

  function deleteSelection() {
    transformSelection((controller) => { controller.deleteSelected(); }, "已删除选中对象");
  }

  function copySelection() {
    const controller = controllerForCurrent();
    if (!controller || selectedIds.length === 0) return;
    clipboardRef.current = controller.copySelected();
    setNotice("已复制到画布剪贴板");
  }

  function pasteSelection() {
    const controller = controllerForCurrent();
    if (!controller || clipboardRef.current.length === 0) return;
    try {
      controller.pasteElements(clipboardRef.current, () => newElementIdentity());
      commitElementOperation(controller, "已粘贴画布对象");
    } catch (error) {
      if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素，请先删除现有元素。");
    }
  }

  function selectAt(point: CanvasPoint, append: boolean) {
    const controller = controllerForCurrent();
    if (!controller || !canvasState) return false;
    const candidates = controller.candidatesAt(point);
    const selection = controller.selectAt(point, { append: append || multiMode });
    setSelectedIds(selection);
    setCandidateMenu(undefined);
    dragRef.current = { base: canvasState, last: canvasState, selectedIds: selection };
    return candidates.length > 0;
  }

  function previewMove(delta: CanvasPoint) {
    const drag = dragRef.current;
    if (!drag) return;
    const previewController = new CanvasElementController(drag.base);
    previewController.selectIds(drag.selectedIds);
    const preview = previewController.moveSelected(delta);
    drag.last = preview.state;
    setCanvasState(preview.state);
    setGuides(preview.guides);
  }

  function commitMove() {
    const drag = dragRef.current;
    if (!drag) return;
    commitCanvas(drag.last);
    setNotice("对象位置已提交");
    setGuides([]);
    dragRef.current = undefined;
  }

  function marqueeSelect(rectangle: CanvasRect, append: boolean) {
    const controller = controllerForCurrent();
    if (!controller) return;
    setSelectedIds(controller.marqueeSelect(rectangle, append || multiMode));
    setCandidateMenu(undefined);
  }

  function showCandidates(point: CanvasPoint) {
    const controller = controllerForCurrent();
    if (!controller) return;
    const elements = controller.candidatesAt(point);
    if (elements.length > 0) setCandidateMenu({ elements, point });
  }

  function showAllCandidates() {
    if (!canvasState) return;
    const elements = [...canvasState.elements].sort((left, right) => right.z_index - left.z_index);
    if (elements.length > 0) setCandidateMenu({ elements, point: { x: 0.5, y: 0.08 } });
  }

  function chooseCandidate(elementId: string) {
    const controller = controllerForCurrent();
    if (!controller) return;
    setSelectedIds(controller.selectById(elementId, multiMode));
    setCandidateMenu(undefined);
    requestAnimationFrame(() => candidateTriggerRef.current?.focus());
  }

  function closeCandidateMenu() {
    setCandidateMenu(undefined);
    requestAnimationFrame(() => candidateTriggerRef.current?.focus());
  }

  function handleCandidateMenuKeyDown(event: KeyboardEvent<HTMLDivElement>) {
    if (event.key === "Escape") {
      event.preventDefault();
      closeCandidateMenu();
      return;
    }
    if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
    const items = [...event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="menuitem"]')];
    const current = items.indexOf(document.activeElement as HTMLButtonElement);
    const delta = event.key === "ArrowDown" ? 1 : -1;
    const next = (current + delta + items.length) % items.length;
    event.preventDefault();
    items[next]?.focus();
  }

  function clearSelection() {
    elementControllerRef.current?.clearSelection();
    setSelectedIds([]);
    setCandidateMenu(undefined);
    setGuides([]);
  }

  if (!project || !canvasState) return <main className="editor-loading" aria-live="polite">正在加载编辑器</main>;
  const renderedCanvasState = textEdit ? {
    ...canvasState,
    elements: canvasState.elements.map((element) => element.element_id === textEdit.elementId ? textEdit.draft : element),
  } : canvasState;
  const imageUrl = `/api/v1/private-assets/projects/${projectId}/images/${canvasState.background.asset_id ?? project.current_image_id ?? ""}`;
  const canEdit = saveStatus !== "conflicted";
  const selectedElements = renderedCanvasState.elements.filter((element) => selectedIds.includes(element.element_id));
  const selectedStickerOpacity = selectedElements.length > 0 && selectedElements.every((element) => element.type !== "text_template")
    ? Math.round((selectedElements[0]?.opacity ?? 1) * 100)
    : undefined;
  return (
    <div className="editor-page-shell">
      <a className="editor-skip-link" href="#editor-main">跳到主要内容</a>
      <header aria-label="编辑器顶部区域" className="editor-toolbar">
        <div aria-label="编辑器顶部工具栏" className="editor-toolbar-controls" role="toolbar">
          <a aria-label="返回项目" className="editor-back" href={`/app/projects/${projectId}`}>←</a>
          <div className="editor-title"><h1>{project.name}</h1><span>{project.ratio}</span></div>
          <div className="editor-history-actions">
            <button aria-label="撤销" disabled={!historyRef.current?.canUndo || !canEdit} onClick={undo} title="撤销" type="button">↶</button>
            <button aria-label="重做" disabled={!historyRef.current?.canRedo || !canEdit} onClick={redo} title="重做" type="button">↷</button>
          </div>
          <span aria-live={saveStatus === "failed" || saveStatus === "conflicted" ? "assertive" : "polite"} className={`editor-save-status ${saveStatus}`}>{saveStatusLabel(saveStatus)}</span>
          <button disabled type="button">预览</button>
          <button disabled={!canvasState.background.asset_id || (saveStatus === "conflicted" && conflictExportGuardRef.current.used)} onClick={openExportDialog} type="button">导出</button>
        </div>
      </header>
      {notice ? <p aria-live="polite" className="editor-notice" role="status">{notice}</p> : null}
      <main className="editor-layout" id="editor-main" tabIndex={-1}>
        <aside aria-label="素材与底图来源" className="editor-assets-panel">
          <nav aria-label="编辑器素材分类" className="editor-asset-tabs">
            {([
              { label: "底图", panel: "background" as const },
              { label: "历史", panel: "history" as const },
              { label: "文字模板", panel: "text" as const },
              { label: "普通贴纸", panel: "stickers" as const },
              { label: "色卡", panel: "color" as const },
              { label: "动态贴纸", panel: "dynamic" as const },
            ]).map((item) => <button aria-current={item.panel === activePanel ? "page" : undefined} key={item.label} onClick={() => { setActivePanel(item.panel); if (item.panel === "stickers") setStickerScrollTop(0); }} type="button">{item.label}</button>)}
          </nav>
          {activePanel === "background" ? <section><h2>底图</h2><button className="editor-source active" type="button"><span className="editor-thumb" style={{ backgroundImage: `url(${imageUrl})` }} /><span>当前底图</span></button></section> : null}
          {activePanel === "history" ? <section><h2>历史</h2><div className="editor-history-list">
            {project.images.length === 0 ? <p className="editor-muted">暂无其他成功图</p> : project.images.map((image) => <button className="editor-source" key={image.image_id} onClick={() => setPendingBackground(image.image_id)} type="button"><span className="editor-thumb" style={{ backgroundImage: `url(/api/v1/private-assets/projects/${projectId}/images/${image.image_id})` }} /><span>生成图 · {formatDate(image.created_at)}</span></button>)}
          </div></section> : null}
          {activePanel === "text" ? <TextTemplatePanel
            canAdd={canEdit && canvasState.elements.length < 50}
            fontStatuses={fontStatuses}
            onAdd={(template) => { void addTextTemplate(template); }}
            onCategory={setTemplateCategory}
            onQuery={setTemplateQuery}
            onRetry={() => { void retryTextFonts(); }}
            query={templateQuery}
            recentIds={recentTextIds}
            templates={P0A_TEXT_TEMPLATES}
            {...(templateCategory ? { category: templateCategory } : {})}
          /> : null}
          {activePanel === "stickers" ? (() => {
            const visibleStickers = stickerWindow(stickerCatalog, stickerScrollTop, 280);
            return <section><h2>普通贴纸</h2><p aria-live="polite" className="editor-sticker-count">共 {stickerCatalog.length.toLocaleString("zh-CN")} 张</p><div className="editor-sticker-virtual-list" data-testid="static-sticker-list" onScroll={(event) => setStickerScrollTop(event.currentTarget.scrollTop)} role="list">
              <div style={{ paddingTop: visibleStickers.top_spacer_px, paddingBottom: visibleStickers.bottom_spacer_px }}>
                <div className="editor-sticker-grid">
                  {visibleStickers.items.map((sticker) => <button aria-label={`添加贴纸 ${sticker.stable_id}`} data-sticker-id={sticker.stable_id} disabled={!canEdit || canvasState.elements.length >= 50} key={sticker.stable_id} onClick={() => addSticker(sticker)} type="button"><img alt="" className="editor-sticker-preview" decoding="async" loading="lazy" src={sticker.thumbnail_reference.url} /><strong>{sticker.stable_id}</strong><span>part{sticker.part} · {sticker.order}</span></button>)}
                </div>
              </div>
            </div>{canvasState.elements.length >= 50 ? <p className="editor-limit" role="status">画布最多 50 个元素，请先删除现有元素。</p> : null}</section>;
          })() : null}
          {activePanel === "color" ? <ColorCardPanel canAdd={canEdit && canvasState.elements.length < 50} hasBackground={Boolean(canvasState.background.asset_id)} onAdd={(definition) => { void addColorCard(definition); }} /> : null}
          {activePanel === "dynamic" ? <DynamicStickerPanel canAdd={canEdit && canvasState.elements.length < 50} fontStatuses={fontStatuses} onAdd={chooseDynamicSticker} /> : null}
        </aside>
        <section aria-label="画布工作区" className="editor-workspace">
          <div className="editor-canvas-tools" role="toolbar" aria-label="画布选择工具">
            <button aria-pressed={multiMode} className={multiMode ? "active" : ""} disabled={!canEdit} onClick={() => setMultiMode((current) => !current)} type="button">多选模式</button>
            <button disabled={canvasState.elements.length === 0} onClick={showAllCandidates} ref={candidateTriggerRef} type="button">循环选择</button>
            <button disabled={selectedElements.length === 0 || !canEdit} onClick={copySelection} title="复制" type="button">复制</button>
            <button disabled={clipboardRef.current.length === 0 || !canEdit} onClick={pasteSelection} title="粘贴" type="button">粘贴</button>
          </div>
          <div className="editor-canvas-frame" style={{
            aspectRatio: `${canvasState.pixel_width} / ${canvasState.pixel_height}`,
            maxWidth: `min(720px, calc(${(canvasState.pixel_width / canvasState.pixel_height * 100).toFixed(4)}vh - ${(canvasState.pixel_width / canvasState.pixel_height * 168).toFixed(4)}px))`,
          }}>
            <EditorStage
              assetId={canvasState.background.asset_id}
              canvasState={renderedCanvasState}
              fontStatuses={fontStatuses}
              guides={guides}
              onCandidates={showCandidates}
              onClearSelection={clearSelection}
              onCopy={copySelection}
              onDelete={deleteSelection}
              onMarquee={marqueeSelect}
              onMoveCommit={commitMove}
              onMovePreview={previewMove}
              onNudge={(delta) => transformSelection((controller) => { controller.moveSelected(delta, { snap: false }); }, "对象位置已提交")}
              onPaste={pasteSelection}
              onPointerMoved={() => elementControllerRef.current?.pointerMoved()}
              onSelect={selectAt}
              projectId={projectId}
              selectedIds={selectedIds}
            />
            {candidateMenu ? <div aria-label="画布对象候选" className="editor-candidates" onKeyDown={handleCandidateMenuKeyDown} ref={candidateMenuRef} role="menu" style={{ left: `${candidateMenu.point.x * 100}%`, top: `${candidateMenu.point.y * 100}%` }}>
              {candidateMenu.elements.map((element) => <button key={element.element_id} onClick={() => chooseCandidate(element.element_id)} role="menuitem" type="button">{element.content || `${element.type} · ${element.template_or_asset_id}`}</button>)}
            </div> : null}
          </div>
          {guides.length > 0 ? <span aria-live="polite" className="editor-snap-status">已吸附</span> : null}
        </section>
        <aside aria-label={selectedElements.length > 0 ? "对象参数" : "底图参数"} className="editor-inspector">
          {selectedElements.length === 0 ? <>
            <header><p>BACKGROUND</p><h2>底图参数</h2></header>
            <label>适配方式<select disabled={!canEdit} value={draftAdjustments.fit} onChange={(event) => setDraftAdjustments((current) => ({ ...current, fit: event.target.value as typeof current.fit }))}><option value="fill">填充</option><option value="fit">适配</option><option value="crop">裁剪</option></select></label>
            <label>滤镜<select disabled={!canEdit} value={draftAdjustments.filter} onChange={(event) => setDraftAdjustments((current) => ({ ...current, filter: event.target.value }))}><option value="none">无滤镜</option><option value="grayscale">黑白</option><option value="sepia">复古</option></select></label>
            {(["brightness", "contrast", "saturation", "temperature", "sharpness"] as const).map((key) => <label className="editor-range" key={key}><span>{({ brightness: "亮度", contrast: "对比度", saturation: "饱和度", temperature: "色温", sharpness: "锐度" } as const)[key]}<output>{draftAdjustments[key]}</output></span><input disabled={!canEdit} max={100} min={key === "sharpness" ? 0 : -100} onChange={(event) => setDraftAdjustments((current) => ({ ...current, [key]: Number(event.target.value) }))} type="range" value={draftAdjustments[key]} /></label>)}
            <div className="editor-inspector-actions"><button disabled={!canEdit} onClick={() => { if (canvasState) setDraftAdjustments(resetBackgroundAdjustments(canvasState).background.adjustments); }} type="button">恢复原图</button><button className="editor-primary" disabled={!canEdit} onClick={applyPreview} type="button">应用调整</button></div>
          </> : <>
            <header><p>{selectedElements.length > 1 ? "MULTI SELECT" : "OBJECT"}</p><h2>{selectedElements.length > 1 ? `已选 ${selectedElements.length} 个对象` : "对象参数"}</h2></header>
            {selectedElements.length === 1 ? <p className="editor-object-id">{selectedElements[0]?.template_or_asset_id}</p> : null}
            {textEdit && selectedElements.length === 1 ? canEdit ? <TextInspector
              draft={textEdit.draft}
              onCancel={cancelTextEdit}
              onComplete={completeTextEdit}
              onContent={(content) => updateTextDraft((edit) => edit.setContent(content))}
              onFont={(fontId) => { void changeTextFont(fontId); }}
              onFontSize={(value) => updateTextDraft((edit) => edit.setEffectiveFontSize(value))}
              onStyle={(style: TextStylePatch) => updateTextDraft((edit) => edit.setStyle(style))}
              onTemplate={(templateId) => { void changeTextTemplate(templateId); }}
              templates={P0A_TEXT_TEMPLATES}
            /> : <p className="editor-muted">版本冲突，文字属性只读</p> : null}
            {selectedElements.length === 1 && selectedElements[0]?.type === "dynamic_sticker" && canEdit
              ? <DynamicInspector element={selectedElements[0]} onCommit={(value) => commitDynamicOverride(selectedElements[0]!, value)} />
              : null}
            <div className="editor-object-moves" role="group" aria-label="移动对象">
              <button aria-label="向左移动" disabled={!canEdit} onClick={() => transformSelection((controller) => { controller.moveSelected({ x: -0.01, y: 0 }, { snap: false }); }, "对象位置已提交")} title="向左移动" type="button">←</button>
              <button aria-label="向上移动" disabled={!canEdit} onClick={() => transformSelection((controller) => { controller.moveSelected({ x: 0, y: -0.01 }, { snap: false }); }, "对象位置已提交")} title="向上移动" type="button">↑</button>
              <button aria-label="向下移动" disabled={!canEdit} onClick={() => transformSelection((controller) => { controller.moveSelected({ x: 0, y: 0.01 }, { snap: false }); }, "对象位置已提交")} title="向下移动" type="button">↓</button>
              <button aria-label="向右移动" disabled={!canEdit} onClick={() => transformSelection((controller) => { controller.moveSelected({ x: 0.01, y: 0 }, { snap: false }); }, "对象位置已提交")} title="向右移动" type="button">→</button>
            </div>
            <div className="editor-object-tools">
              <button disabled={!canEdit} onClick={() => transformSelection((controller) => { controller.scaleSelected(0.9); }, "对象尺寸已提交")} type="button">缩小</button>
              <button disabled={!canEdit} onClick={() => transformSelection((controller) => { controller.scaleSelected(1.1); }, "对象尺寸已提交")} type="button">放大</button>
              <button disabled={!canEdit} onClick={() => transformSelection((controller) => { controller.rotateSelected(-15); }, "对象旋转已提交")} type="button">逆时针</button>
              <button disabled={!canEdit} onClick={() => transformSelection((controller) => { controller.rotateSelected(15); }, "对象旋转已提交")} type="button">顺时针</button>
            </div>
            {selectedStickerOpacity !== undefined ? <>
              <button className="editor-wide-command" disabled={!canEdit} onClick={() => transformSelection((controller) => { controller.flipSelectedHorizontal(); }, "贴纸翻转已提交")} type="button">水平翻转</button>
              <label className="editor-range"><span>透明度<output>{selectedStickerOpacity}%</output></span><input disabled={!canEdit} max={100} min={0} onBlur={commitOpacityGesture} onChange={(event) => changeOpacity(Number(event.target.value) / 100)} onPointerCancel={commitOpacityGesture} onPointerDown={beginOpacityGesture} onPointerUp={commitOpacityGesture} type="range" value={selectedStickerOpacity} /></label>
            </> : null}
            <div className="editor-layer-tools" role="group" aria-label="对象层级">
              <button disabled={!canEdit} onClick={() => changeSelectionLayer("front")} type="button">置顶</button>
              <button disabled={!canEdit} onClick={() => changeSelectionLayer("forward")} type="button">上移一层</button>
              <button disabled={!canEdit} onClick={() => changeSelectionLayer("backward")} type="button">下移一层</button>
              <button disabled={!canEdit} onClick={() => changeSelectionLayer("back")} type="button">置底</button>
            </div>
            <div className="editor-object-actions"><button disabled={!canEdit} onClick={duplicateSelection} type="button">复制</button><button className="editor-danger" disabled={!canEdit} onClick={deleteSelection} type="button">删除</button></div>
          </>}
        </aside>
      </main>
      <footer aria-label="画布状态" className="editor-statusbar"><span>画布 {canvasState.pixel_width} × {canvasState.pixel_height}</span><span>对象 {canvasState.elements.length} / 50</span><span>缩放 100%</span><span>本机保存 · state version {project.state_version}</span></footer>
      {pendingBackground ? <EditorDialog labelledBy="editor-background-confirm" onClose={() => setPendingBackground(undefined)}><p>CHANGE BACKGROUND</p><h2 id="editor-background-confirm">更换底图</h2><p>覆盖元素会保留，底图编辑参数将重置。</p><div><button className="editor-primary" data-dialog-initial-focus onClick={() => { void confirmBackground(); }} type="button">确认更换</button><button onClick={() => setPendingBackground(undefined)} type="button">取消</button></div></EditorDialog> : null}
      {locationDialog ? <EditorDialog className="editor-confirm editor-location-consent" labelledBy="editor-location-consent" onClose={() => { if (!locationDialog.pending) setLocationDialog(undefined); }}>
        <p>LOCATION PRIVACY</p><h2 id="editor-location-consent">使用自动定位</h2>
        <p>原始经纬度会保存到当前项目并显示在导出成品中。只有确认后才会请求浏览器定位，并由本地后端调用地点服务。</p>
        {locationDialog.error ? <p className="editor-limit" role="alert">{locationDialog.error}</p> : null}
        <label>手动地点文字<input aria-label="手动地点文字" disabled={locationDialog.pending} onChange={(event) => setLocationDialog((current) => current ? { ...current, manualValue: event.target.value } : current)} value={locationDialog.manualValue} /></label>
        <div className="editor-location-actions"><button className="editor-primary" data-dialog-initial-focus disabled={locationDialog.pending} onClick={() => { void confirmAutomaticLocation(); }} type="button">{locationDialog.pending ? "正在定位" : "同意并自动定位"}</button><button disabled={!locationDialog.manualValue.trim() || locationDialog.pending} onClick={() => { void addDynamicSticker("DYN001", { formattedValue: locationDialog.manualValue.trim() }); }} type="button">改用手动地点贴纸</button><button disabled={locationDialog.pending} onClick={() => setLocationDialog(undefined)} type="button">暂不定位</button></div>
      </EditorDialog> : null}
      {exportOpen ? <ExportDialog
        busy={exportBusy}
        onClose={() => { if (!exportBusy) { setExportOpen(false); setExportResult(undefined); } }}
        onExport={(options) => { void startExport(options); }}
        onRetry={() => { void retryExportDownload(); }}
        pendingEdit={Boolean(pendingTextDraft())}
        ratio={canvasState.ratio}
        {...(exportResult?.blob ? { byteSize: exportResult.blob.size } : {})}
        {...(exportResult ? { result: exportResult.status } : {})}
      /> : null}
    </div>
  );
}
