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

import {
  ConflictExportGuard,
  ProjectAutoSaveQueue,
  ProjectStateConflict,
  type ProjectSaveStatus,
} from "./project-autosave.js";

import "./project-pages.css";

type Ratio = "3:4" | "1:1" | "4:3" | "9:16";
type ProjectStatus = "active" | "failed_empty" | "trashed";

interface SessionPayload {
  credits: { available_balance: number; reserved_balance: number };
  csrf_token: string;
  local_data?: LocalDataPayload;
  user: { creator_name: string };
}

interface LocalDataPayload {
  capacity_status: "normal" | "warning" | "critical" | "full" | "unavailable";
  hard_limit_bytes: number;
  managed_content_bytes: number;
}

interface AccountSettingsPayload {
  local_data: LocalDataPayload;
}

interface ModelPayload {
  config_set_version: number;
  configured_default_model_id: string;
  models: Array<{
    config_version: number;
    contract_validation_status: "verified" | "unverified";
    credit_cost: number;
    enabled: boolean;
    is_default: boolean;
    model_id: string;
    prompt_max_length: number;
    reference_limits: { max_file_bytes: number; max_files: number; max_total_bytes: number };
    runtime_availability: { available_for_new_jobs: boolean; checked_at: string; reason: string | null };
    supported_ratios: Ratio[];
  }>;
  recommended_model_id: string | null;
}

interface GenerationTaskPayload {
  confirmed_credit_cost: number;
  created_at: string;
  error_category: GenerationErrorCategory | null;
  generation_id: string;
  model_config_version: number;
  model_id: string;
  project_id: string;
  prompt: string;
  ratio: Ratio;
  reference_asset_ids?: string[];
  reference_count: number;
  reserved_credits: number;
  status: "queued" | "running" | "succeeded" | "failed" | "rejected";
  updated_at: string;
}

type GenerationErrorCategory = "upstream_timeout" | "upstream_failed" | "safety_rejected" | "model_disabled"
  | "gateway_balance_insufficient" | "gateway_contract_invalid" | "reference_invalid"
  | "unknown_retryable" | "unknown_non_retryable";

const generationErrorActions: Record<GenerationErrorCategory, string> = {
  gateway_balance_insufficient: "选择未受影响模型或联系管理员",
  gateway_contract_invalid: "选择其他模型或联系管理员",
  model_disabled: "选择其他模型或等待",
  reference_invalid: "更换或移除参考图",
  safety_rejected: "修改提示词或参考图",
  unknown_non_retryable: "联系管理员",
  unknown_retryable: "稍后重试",
  upstream_failed: "稍后重试",
  upstream_timeout: "使用原输入重试",
};

interface ProjectSummary {
  current_image_id: string | null;
  deleted_at?: string | null;
  name: string;
  project_id: string;
  purge_at?: string | null;
  ratio: Ratio;
  state_version: number;
  status: ProjectStatus;
  successful_image_count: number;
  updated_at: string;
}

interface ProjectListPayload {
  active_count: number;
  active_limit: 20;
  projects: ProjectSummary[];
}

interface ProjectDetailPayload extends ProjectSummary {
  canvas_state: CanvasState;
  created_at: string;
  draft_prompt: string;
  generations: Array<{
    created_at: string;
    error_category: string | null;
    generation_id: string;
    prompt: string;
    ratio: Ratio;
    status: "queued" | "running" | "succeeded" | "failed" | "rejected";
    updated_at: string;
  }>;
  images: Array<{ created_at: string; generation_id: string; image_id: string }>;
  latest_exports: Array<{
    byte_size: number;
    created_at: string;
    download_url: string;
    export_id: string;
    format: "jpg" | "png";
    pixel_height: number;
    pixel_width: number;
    sha256: string;
    state_version: number;
  }>;
  pixel_height?: number;
  pixel_width?: number;
  save_status: "saved";
}

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

async function readOptionalJson<T>(url: string): Promise<T | undefined> {
  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.status === 404) return undefined;
  if (!response.ok) throw new Error("request_failed");
  return response.json() as Promise<T>;
}

function formatUpdatedAt(value: string) {
  return new Intl.DateTimeFormat("zh-CN", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
}

function initialCanvasState(project: Pick<ProjectDetailPayload, "current_image_id" | "pixel_height" | "pixel_width" | "ratio">): CanvasState {
  return {
    background: {
      adjustments: {
        brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill",
        saturation: 0, sharpness: 0, temperature: 0,
      },
      asset_id: project.current_image_id,
    },
    elements: [],
    pixel_height: project.pixel_height ?? (project.ratio === "9:16" ? 1920 : project.ratio === "3:4" ? 1440 : 1080),
    pixel_width: project.pixel_width ?? (project.ratio === "4:3" ? 1440 : 1080),
    ratio: project.ratio,
    schema_version: 1,
  };
}

async function downloadConflictPng(canvasState: CanvasState, projectName: string) {
  const canvas = document.createElement("canvas");
  canvas.width = canvasState.pixel_width;
  canvas.height = canvasState.pixel_height;
  const context = canvas.getContext("2d");
  if (!context) throw new Error("canvas_unavailable");
  context.fillStyle = "#f6f6f4";
  context.fillRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = "#d4d4cf";
  const stripe = canvas.width / 4;
  for (let index = 0; index < 4; index += 1) {
    if (index % 2 === 1) context.fillRect(index * stripe, 0, stripe, canvas.height);
  }
  context.fillStyle = "#111111";
  context.font = `900 ${Math.max(64, Math.floor(canvas.width / 7))}px Arial`;
  context.textAlign = "center";
  context.textBaseline = "middle";
  context.fillText("DADA", canvas.width / 2, canvas.height / 2);
  const blob = await new Promise<Blob>((resolve, reject) => {
    canvas.toBlob((value) => value ? resolve(value) : reject(new Error("canvas_export_failed")), "image/png");
  });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.download = `${projectName.trim().replace(/[\\/:*?"<>|]+/g, "-") || "Dada"}-本页版本.png`;
  anchor.href = url;
  anchor.click();
  setTimeout(() => URL.revokeObjectURL(url), 0);
}

export function ProductHeader({ current }: { current: "workspace" | "projects" | "credits" }) {
  return (
    <header className="product-header">
      <a className="product-brand" href="/app">DADA</a>
      <nav aria-label="主导航">
        <a aria-current={current === "workspace" ? "page" : undefined} href="/app">创作</a>
        <a aria-current={current === "projects" ? "page" : undefined} href="/app/projects">项目</a>
        <a aria-current={current === "credits" ? "page" : undefined} href="/app/credits">点数</a>
        <a href="/app/settings">设置</a>
      </nav>
    </header>
  );
}

export function LocalOnlyFooter() {
  return <footer className="local-only-footer">测试数据仅保存在本机，不自动备份，也不会迁移到正式系统。</footer>;
}

function LoadingPage({ label }: { label: string }) {
  return <main className="product-loading" aria-live="polite">{label}</main>;
}

function ProjectPlaceholder({ ratio, status }: { ratio: Ratio; status: ProjectStatus }) {
  return (
    <div className="project-placeholder" data-ratio={ratio} data-status={status} aria-hidden="true">
      <span>D</span><span>A</span><span>D</span><span>A</span>
    </div>
  );
}

export function WorkspacePage() {
  const promptId = useId();
  const [session, setSession] = useState<SessionPayload>();
  const [projects, setProjects] = useState<ProjectListPayload>();
  const [models, setModels] = useState<ModelPayload>();
  const [selectedModelId, setSelectedModelId] = useState<string>();
  const [currentTask, setCurrentTask] = useState<GenerationTaskPayload>();
  const [localData, setLocalData] = useState<LocalDataPayload>();
  const [generationStateLoaded, setGenerationStateLoaded] = useState(false);
  const [loadingFailed, setLoadingFailed] = useState(false);
  const [prompt, setPrompt] = useState("");
  const [ratio, setRatio] = useState<Ratio>("3:4");
  const [references, setReferences] = useState<File[]>([]);
  const [submitting, setSubmitting] = useState(false);
  const [generationNotice, setGenerationNotice] = useState("");
  const [requiresReconfirmation, setRequiresReconfirmation] = useState(false);
  const query = useMemo(() => new URLSearchParams(window.location.search), []);
  const [targetProjectId, setTargetProjectId] = useState<string | undefined>(() => query.get("retry") ?? query.get("continue") ?? undefined);
  const [existingReferenceAssetIds, setExistingReferenceAssetIds] = useState<string[]>([]);

  useEffect(() => {
    let active = true;
    Promise.all([
      readJson<SessionPayload>("/api/v1/auth/session"),
      readJson<ProjectListPayload>("/api/v1/projects?status=active"),
    ]).then(([nextSession, nextProjects]) => {
      if (!active) return;
      setSession(nextSession);
      setProjects(nextProjects);
      setLocalData(nextSession.local_data);
      return Promise.allSettled([
        readOptionalJson<ModelPayload>("/api/v1/models"),
        readOptionalJson<GenerationTaskPayload>("/api/v1/generations/current"),
        readOptionalJson<AccountSettingsPayload>("/api/v1/account/settings"),
      ]).then(([modelResult, taskResult, settingsResult]) => {
        if (!active) return;
        if (modelResult.status === "fulfilled") setModels(modelResult.value);
        if (taskResult.status === "fulfilled") setCurrentTask(taskResult.value);
        if (settingsResult.status === "fulfilled" && settingsResult.value) setLocalData(settingsResult.value.local_data);
        setGenerationStateLoaded(true);
      });
    }).catch((error) => {
      if (active && error instanceof Error && error.message !== "session_invalid") setLoadingFailed(true);
    });
    return () => { active = false; };
  }, []);

  useEffect(() => {
    if (!targetProjectId || prompt) return;
    readOptionalJson<ProjectDetailPayload>(`/api/v1/projects/${targetProjectId}`).then((project) => {
      if (!project) return;
      setPrompt(project.draft_prompt);
      setRatio(project.ratio);
    }).catch(() => setGenerationNotice("暂时无法读取原项目。"));
  }, [prompt, targetProjectId]);

  useEffect(() => {
    if (!currentTask || !["queued", "running"].includes(currentTask.status)) return;
    const timer = window.setInterval(() => {
      readOptionalJson<GenerationTaskPayload>(`/api/v1/generations/${currentTask.generation_id}`)
        .then((task) => { if (task) setCurrentTask(task); })
        .catch(() => undefined);
    }, 2_000);
    return () => window.clearInterval(timer);
  }, [currentTask?.generation_id, currentTask?.status]);

  const selectedModel = useMemo(() => {
    if (!models) return undefined;
    const modelId = selectedModelId ?? models.recommended_model_id ?? models.configured_default_model_id;
    return models.models.find((model) => model.model_id === modelId && model.enabled
      && model.contract_validation_status === "verified" && model.runtime_availability.available_for_new_jobs);
  }, [models, selectedModelId]);

  const referenceBytes = references.reduce((sum, file) => sum + file.size, 0);
  const referencesValid = selectedModel !== undefined
    && references.length <= selectedModel.reference_limits.max_files
    && referenceBytes <= selectedModel.reference_limits.max_total_bytes
    && references.every((file) => file.size > 0 && file.size <= selectedModel.reference_limits.max_file_bytes);
  const capacityBlocksGeneration = localData?.capacity_status === "full" || localData?.capacity_status === "unavailable";
  const canSubmit = generationStateLoaded && !currentTask && !submitting && !requiresReconfirmation && selectedModel !== undefined
    && prompt.trim().length > 0 && prompt.trim().length <= selectedModel.prompt_max_length
    && selectedModel.supported_ratios.includes(ratio) && referencesValid
    && session !== undefined && session.credits.available_balance >= selectedModel.credit_cost && !capacityBlocksGeneration;

  async function submitGeneration(event: FormEvent) {
    event.preventDefault();
    if (!session || !selectedModel || !canSubmit) return;
    const body = new FormData();
    body.append("client_submission_id", crypto.randomUUID());
    body.append("confirmed_credit_cost", String(selectedModel.credit_cost));
    body.append("creation_mode", targetProjectId ? "existing_project" : "new_project");
    body.append("existing_reference_asset_ids", JSON.stringify(existingReferenceAssetIds));
    body.append("model_config_version", String(selectedModel.config_version));
    body.append("model_id", selectedModel.model_id);
    body.append("prompt", prompt.trim());
    body.append("ratio", ratio);
    if (targetProjectId) body.append("project_id", targetProjectId);
    body.append("reference_manifest", JSON.stringify(references.map((file) => ({
      file_name: file.name,
      mime_type: file.type,
      size: file.size,
    }))));
    for (const file of references) body.append("reference_files", file, file.name);
    setSubmitting(true);
    setGenerationNotice("");
    try {
      const response = await fetch("/api/v1/generations", {
        body,
        credentials: "same-origin",
        headers: {
          "Idempotency-Key": `generation-${crypto.randomUUID()}`,
          "X-CSRF-Token": session.csrf_token,
        },
        method: "POST",
      });
      if (response.status === 401) {
        window.dispatchEvent(new Event("dada:session-invalid"));
        return;
      }
      if (response.status === 412) {
        setRequiresReconfirmation(true);
        setGenerationNotice("模型配置已更新，请确认最新配置后重新提交。");
        return;
      }
      if (!response.ok) {
        setGenerationNotice(response.status === 507 ? "本机存储空间不足，当前不能创建新任务。" : "任务未提交，请检查当前状态后重试。");
        return;
      }
      const result = await response.json() as { created: boolean; task: GenerationTaskPayload };
      setCurrentTask(result.task);
      setSession((current) => current ? {
        ...current,
        credits: {
          available_balance: current.credits.available_balance - (result.created ? result.task.reserved_credits : 0),
          reserved_balance: current.credits.reserved_balance + (result.created ? result.task.reserved_credits : 0),
        },
      } : current);
      setGenerationNotice(result.created ? "任务已提交。" : "已返回当前进行中的任务。");
    } catch {
      setGenerationNotice("任务未提交，请检查本机服务后重试。");
    } finally {
      setSubmitting(false);
    }
  }

  function handleTerminalAction(task: GenerationTaskPayload) {
    const category = task.error_category;
    if (!category) return;
    if (category === "unknown_non_retryable") {
      setGenerationNotice("请联系超级管理员处理此任务。");
      return;
    }
    setPrompt(task.prompt);
    setRatio(task.ratio);
    setTargetProjectId(task.project_id);
    setExistingReferenceAssetIds(task.reference_asset_ids ?? []);
    setCurrentTask(undefined);
    if (["model_disabled", "gateway_balance_insufficient", "gateway_contract_invalid"].includes(category)) {
      setSelectedModelId(undefined);
      setGenerationNotice("请选择当前可用模型后重新提交。");
    } else if (category === "safety_rejected" || category === "reference_invalid") {
      setGenerationNotice("请修改输入后重新提交。");
    } else {
      setGenerationNotice("已恢复原任务输入，可以重新提交。");
    }
  }

  async function confirmLatestModelConfiguration() {
    try {
      const next = await readJson<ModelPayload>("/api/v1/models");
      setModels(next);
      setRequiresReconfirmation(false);
      setGenerationNotice("已确认最新配置，请重新检查点数和参考图后提交。");
    } catch {
      setGenerationNotice("暂时无法读取最新模型配置。");
    }
  }

  if (loadingFailed) {
    return (
      <main className="product-loading">
        <p role="alert">创作工作台暂时无法读取。</p>
        <button onClick={() => window.location.reload()} type="button">重新读取</button>
      </main>
    );
  }
  if (!session || !projects) return <LoadingPage label="正在读取创作工作台" />;
  const empty = projects.projects.length === 0;

  return (
    <div className="product-page">
      <ProductHeader current="workspace" />
      {empty ? (
        <section className="workspace-art" aria-labelledby="workspace-title">
          <div>
            <strong>DADA</strong>
            <h1 id="workspace-title">开始一张新作品</h1>
          </div>
          <div className="workspace-art-system" aria-hidden="true">
            <i /><b>NEW PROJECT</b><b>LOCAL ONLY</b><b>NO CLOUD SYNC</b>
          </div>
        </section>
      ) : null}
      <main className={`workspace-main ${empty ? "is-empty" : "has-projects"}`}>
        <form className="generation-area" aria-labelledby={empty ? undefined : "workspace-compact-title"} onSubmit={submitGeneration}>
          {!empty ? (
            <div className="workspace-compact-heading">
              <div><p>NEW PROJECT</p><h1 id="workspace-compact-title">新建创作</h1></div>
              <span>当前可用 {session.credits.available_balance} 点</span>
            </div>
          ) : null}
          <label className="prompt-label" htmlFor={promptId}>描述你想生成的画面</label>
          <textarea
            id={promptId}
            maxLength={4_000}
            onChange={(event) => setPrompt(event.target.value)}
            placeholder="输入提示词…"
            value={prompt}
          />
          <div className="generation-options">
            <div className="model-status" aria-live="polite">
              <span>模型</span>
              {models?.models.some((model) => model.enabled && model.contract_validation_status === "verified" && model.runtime_availability.available_for_new_jobs) ? (
                <select aria-label="模型" onChange={(event) => setSelectedModelId(event.target.value || undefined)} value={selectedModel?.model_id ?? ""}>
                  {models.models.filter((model) => model.enabled && model.contract_validation_status === "verified" && model.runtime_availability.available_for_new_jobs)
                    .map((model) => <option key={model.model_id} value={model.model_id}>{model.model_id}</option>)}
                </select>
              ) : <strong>当前没有可用于新任务的模型</strong>}
            </div>
            <fieldset className="ratio-control">
              <legend>画面比例</legend>
              <div>
                {(["3:4", "1:1", "4:3", "9:16"] as const).map((value) => (
                  <label key={value}>
                    <input
                      checked={ratio === value}
                      disabled={selectedModel !== undefined && !selectedModel.supported_ratios.includes(value)}
                      name="ratio"
                      onChange={() => setRatio(value)}
                      type="radio"
                      value={value}
                    />
                    <span>{value}</span>
                  </label>
                ))}
              </div>
            </fieldset>
          </div>
          <label className="reference-input">
            <span>添加参考图</span>
            <small>{references.length > 0 ? references.map((file) => file.name).join("、") : "尚未提交，仅保留在当前页面"}</small>
            <input
              accept="image/jpeg,image/png,image/webp"
              multiple
              onChange={(event) => setReferences(Array.from(event.target.files ?? []))}
              type="file"
            />
          </label>
          {localData?.capacity_status === "critical" ? (
            <div className="capacity-critical" role="status">
              <strong>存储空间已超过 90%</strong>
              <span>请尽快清理本机内容，达到上限后将无法提交新任务。</span>
            </div>
          ) : null}
          {generationNotice ? (
            <div className="generation-notice" role="status">
              <span>{generationNotice}</span>
              {requiresReconfirmation ? (
                <button onClick={confirmLatestModelConfiguration} type="button">确认最新配置</button>
              ) : null}
            </div>
          ) : null}
          <div className="generation-submit">
            <span>{selectedModel ? `本次预计冻结 ${selectedModel.credit_cost} 点` : "预计点数将在模型可用后显示"}</span>
            <button disabled={!canSubmit} type="submit">{submitting ? "正在提交" : "生成一张图片"}</button>
          </div>
        </form>
        <aside className="current-task" aria-labelledby="current-task-title">
          <h2 id="current-task-title">当前任务</h2>
          {currentTask ? (
            <div className="current-task-active">
              <span className="task-state">{{
                failed: "生成失败", queued: "排队中", rejected: "请求未通过", running: "处理中", succeeded: "生成成功",
              }[currentTask.status]}</span>
              <strong>{currentTask.prompt}</strong>
              <dl>
                <div><dt>画面比例</dt><dd>{currentTask.ratio}</dd></div>
                <div><dt>参考图</dt><dd>{currentTask.reference_count} 张</dd></div>
                <div><dt>点数</dt><dd>{currentTask.status === "succeeded" ? `已扣除 ${currentTask.confirmed_credit_cost} 点` : ["failed", "rejected"].includes(currentTask.status) ? "已释放冻结点" : `已冻结 ${currentTask.reserved_credits} 点`}</dd></div>
              </dl>
              {currentTask.status === "succeeded" ? <a href={`/app/projects/${currentTask.project_id}`}>进入编辑</a> : null}
              {["queued", "running"].includes(currentTask.status) ? <a href={`/app/projects/${currentTask.project_id}`}>返回当前项目</a> : null}
              {["failed", "rejected"].includes(currentTask.status) && currentTask.error_category ? (
                <button onClick={() => handleTerminalAction(currentTask)} type="button">{generationErrorActions[currentTask.error_category]}</button>
              ) : null}
            </div>
          ) : (
            <div className="current-task-empty"><i /><strong>没有进行中的任务</strong><span>任务状态会持续显示在这里</span></div>
          )}
        </aside>
        {!empty ? (
          <section className="recent-projects" aria-labelledby="recent-title">
            <header><h2 id="recent-title">最近项目</h2><a href="/app/projects">查看全部项目</a></header>
            <div className="project-grid compact">
              {projects.projects.slice(0, 6).map((project) => <ProjectCard key={project.project_id} project={project} />)}
            </div>
          </section>
        ) : null}
      </main>
      <LocalOnlyFooter />
    </div>
  );
}

function ProjectCard({ activeLimitReached, busy, onPurge, onRestore, onSelect, onTrash, project, selectable, selected }: {
  activeLimitReached?: boolean;
  busy?: boolean;
  onPurge?: (() => void) | undefined;
  onRestore?: (() => void) | undefined;
  onSelect?: (selected: boolean) => void;
  onTrash?: (() => void) | undefined;
  project: ProjectSummary;
  selectable?: boolean;
  selected?: boolean;
}) {
  return (
    <article className="project-card" data-status={project.status}>
      {selectable ? (
        <label className="project-select">
          <input
            aria-label={`选择失败草稿：${project.name}`}
            checked={selected}
            onChange={(event) => onSelect?.(event.target.checked)}
            type="checkbox"
          />
        </label>
      ) : null}
      <ProjectPlaceholder ratio={project.ratio} status={project.status} />
      <div className="project-card-body">
        <div><h3 title={project.name}>{project.name}</h3><span>{project.status === "failed_empty" ? "生成失败" : project.status === "trashed" ? "回收站" : "项目"}</span></div>
        <p>{project.successful_image_count} 张成功图 · {project.ratio}</p>
        {project.status === "trashed" && project.purge_at ? <p className="trash-expiry">将在 {formatUpdatedAt(project.purge_at)} 永久删除</p> : null}
        <time dateTime={project.updated_at}>{formatUpdatedAt(project.updated_at)}</time>
        <div className="project-card-actions">
          {project.status !== "trashed" ? <a aria-label={`打开项目：${project.name}`} href={`/app/projects/${project.project_id}`}>打开</a> : null}
          {onTrash ? <button aria-label={`移入回收站：${project.name}`} disabled={busy} onClick={onTrash} type="button">移入回收站</button> : null}
          {onRestore ? <button aria-label={`恢复项目：${project.name}`} disabled={busy || activeLimitReached} onClick={onRestore} type="button">恢复</button> : null}
          {onPurge ? <button aria-label={`永久删除：${project.name}`} disabled={busy} onClick={onPurge} type="button">永久删除</button> : null}
        </div>
      </div>
    </article>
  );
}

export function ProjectsPage() {
  const [session, setSession] = useState<SessionPayload>();
  const [payload, setPayload] = useState<ProjectListPayload>();
  const [status, setStatus] = useState<"active" | "trashed">("active");
  const [query, setQuery] = useState("");
  const [sort, setSort] = useState<"updated" | "name">("updated");
  const [selected, setSelected] = useState<string[]>([]);
  const [loadingFailed, setLoadingFailed] = useState(false);
  const [notice, setNotice] = useState("");
  const [busyProjectId, setBusyProjectId] = useState<string>();
  const [pendingPurge, setPendingPurge] = useState<ProjectSummary>();

  useEffect(() => {
    let active = true;
    setLoadingFailed(false);
    Promise.all([
      session ? Promise.resolve(session) : readJson<SessionPayload>("/api/v1/auth/session"),
      readJson<ProjectListPayload>(`/api/v1/projects?status=${status}`),
    ]).then(([nextSession, nextProjects]) => {
      if (!active) return;
      setSession(nextSession);
      setPayload(nextProjects);
      setSelected([]);
    }).catch((error) => {
      if (active && error instanceof Error && error.message !== "session_invalid") setLoadingFailed(true);
    });
    return () => { active = false; };
  }, [status]);

  const visible = useMemo(() => {
    const normalized = query.trim().toLocaleLowerCase("zh-CN");
    const items = (payload?.projects ?? []).filter((project) => project.name.toLocaleLowerCase("zh-CN").includes(normalized));
    return items.toSorted((left, right) => sort === "name"
      ? left.name.localeCompare(right.name, "zh-CN")
      : Date.parse(right.updated_at) - Date.parse(left.updated_at));
  }, [payload, query, sort]);

  async function batchTrash() {
    if (!session || selected.length === 0) return;
    try {
      const result = await readJson<{ ignored_project_ids: string[]; trashed_project_ids: string[] }>("/api/v1/projects/failed-empty/trash", {
        body: JSON.stringify({ project_ids: selected }),
        headers: { "Content-Type": "application/json", "X-CSRF-Token": session.csrf_token },
        method: "POST",
      });
      setPayload((current) => current ? {
        ...current,
        active_count: Math.max(0, current.active_count - result.trashed_project_ids.length),
        projects: current.projects.filter((project) => !result.trashed_project_ids.includes(project.project_id)),
      } : current);
      setSelected([]);
      setNotice("失败草稿已移入回收站");
    } catch {
      setNotice("操作未完成，请重试");
    }
  }

  async function runLifecycle(project: ProjectSummary, action: "trash" | "restore" | "purge") {
    if (!session || busyProjectId) return;
    setBusyProjectId(project.project_id);
    try {
      await readJson(`/api/v1/projects/${project.project_id}/${action}`, {
        headers: { "X-CSRF-Token": session.csrf_token },
        method: "POST",
      });
      setPayload((current) => current ? {
        ...current,
        active_count: action === "trash"
          ? Math.max(0, current.active_count - 1)
          : action === "restore"
            ? Math.min(current.active_limit, current.active_count + 1)
            : current.active_count,
        projects: current.projects.filter((item) => item.project_id !== project.project_id),
      } : current);
      setNotice(action === "trash"
        ? "项目已移入回收站"
        : action === "restore"
          ? "项目已恢复"
          : "项目已永久删除，物理清理将在后台完成");
      if (action === "purge") setPendingPurge(undefined);
    } catch {
      setNotice("操作未完成，请重试");
    } finally {
      setBusyProjectId(undefined);
    }
  }

  if (!payload && !loadingFailed) return <LoadingPage label="正在读取项目" />;

  return (
    <div className="product-page">
      <ProductHeader current="projects" />
      <main className="projects-page">
        <header className="projects-title">
          <div><p>LOCAL PROJECTS</p><h1>项目</h1></div>
          <strong>{payload?.active_count ?? 0} / 20 active</strong>
        </header>
        <div className="projects-tabs" role="tablist" aria-label="项目状态">
          <button aria-selected={status === "active"} onClick={() => setStatus("active")} role="tab" type="button">项目</button>
          <button aria-selected={status === "trashed"} onClick={() => setStatus("trashed")} role="tab" type="button">回收站</button>
        </div>
        <div className="projects-controls">
          <input aria-label="搜索项目" onChange={(event) => setQuery(event.target.value)} placeholder="搜索项目" type="search" value={query} />
          <select aria-label="状态筛选" value={status} onChange={(event) => setStatus(event.target.value as "active" | "trashed")}>
            <option value="active">active</option><option value="trashed">trashed</option>
          </select>
          <select aria-label="项目排序" value={sort} onChange={(event) => setSort(event.target.value as "updated" | "name")}>
            <option value="updated">最近更新</option><option value="name">名称</option>
          </select>
        </div>
        {loadingFailed ? <p className="projects-stale" role="alert">项目列表读取失败。{payload ? "当前显示上次读取的数据。" : ""}</p> : null}
        {notice ? <p className="projects-notice" role="status">{notice}</p> : null}
        {status === "trashed" && payload && payload.active_count === payload.active_limit ? (
          <p className="projects-stale">活动项目已达 20 个，请先释放名额</p>
        ) : null}
        {visible.length === 0 ? (
          <section className="projects-empty">
            <h2>{query ? "没有符合条件的项目" : status === "active" ? "还没有项目" : "回收站为空"}</h2>
            {query ? <button onClick={() => setQuery("")} type="button">清除筛选</button> : status === "active" ? <a href="/app">前往创作</a> : null}
          </section>
        ) : (
          <div className="project-grid">
            {visible.map((project) => (
              <ProjectCard
                key={project.project_id}
                onSelect={(checked) => setSelected((current) => checked
                  ? [...current, project.project_id]
                  : current.filter((projectId) => projectId !== project.project_id))}
                project={project}
                activeLimitReached={payload?.active_count === payload?.active_limit}
                busy={busyProjectId === project.project_id}
                onPurge={status === "trashed" ? () => setPendingPurge(project) : undefined}
                onRestore={status === "trashed" ? () => runLifecycle(project, "restore") : undefined}
                onTrash={status === "active" ? () => runLifecycle(project, "trash") : undefined}
                selectable={status === "active" && project.status === "failed_empty"}
                selected={selected.includes(project.project_id)}
              />
            ))}
          </div>
        )}
        {status === "active" && payload?.projects.some((project) => project.status === "failed_empty") ? (
          <section className="failed-draft-bar" aria-label="失败草稿快捷清理">
            <span>已选 {selected.length} 个无成功图草稿</span>
            <button disabled={selected.length === 0} onClick={batchTrash} type="button">批量移入回收站</button>
          </section>
        ) : null}
      </main>
      {pendingPurge ? (
        <div className="project-purge-overlay">
          <section aria-labelledby="project-purge-title" aria-modal="true" className="project-purge-dialog" role="dialog">
            <p>IRREVERSIBLE ACTION</p>
            <h2 id="project-purge-title">永久删除项目</h2>
            <p>“{pendingPurge.name}”将立即无法恢复。关联数据和文件会进入后台物理清理队列。</p>
            <div>
              <button disabled={busyProjectId === pendingPurge.project_id} onClick={() => runLifecycle(pendingPurge, "purge")} type="button">确认永久删除</button>
              <button disabled={Boolean(busyProjectId)} onClick={() => setPendingPurge(undefined)} type="button">取消</button>
            </div>
          </section>
        </div>
      ) : null}
      <LocalOnlyFooter />
    </div>
  );
}

export function ProjectDetailPage({ projectId }: { projectId: string }) {
  const [session, setSession] = useState<SessionPayload>();
  const [project, setProject] = useState<ProjectDetailPayload>();
  const [name, setName] = useState("");
  const [saveStatus, setSaveStatus] = useState<ProjectSaveStatus>("saved");
  const [conflictVersions, setConflictVersions] = useState<{ latest: number; page: number }>();
  const [conflictExportBusy, setConflictExportBusy] = useState(false);
  const [conflictExportUsed, setConflictExportUsed] = useState(false);
  const [conflictNotice, setConflictNotice] = useState("");
  const [pendingNavigation, setPendingNavigation] = useState<string>();
  const [leaving, setLeaving] = useState(false);
  const [leaveStatus, setLeaveStatus] = useState("");
  const [loadingFailed, setLoadingFailed] = useState(false);
  const queueRef = useRef<ProjectAutoSaveQueue | undefined>(undefined);
  const saveStatusRef = useRef<ProjectSaveStatus>("saved");
  const conflictExportGuard = useRef(new ConflictExportGuard());

  useEffect(() => {
    let active = true;
    Promise.all([
      readJson<SessionPayload>("/api/v1/auth/session"),
      readJson<ProjectDetailPayload>(`/api/v1/projects/${projectId}`),
    ]).then(([nextSession, nextProject]) => {
      if (!active) return;
      setSession(nextSession);
      const normalizedProject = {
        ...nextProject,
        canvas_state: nextProject.canvas_state ?? initialCanvasState(nextProject),
        latest_exports: nextProject.latest_exports ?? [],
        save_status: nextProject.save_status ?? "saved",
      };
      setProject(normalizedProject);
      setName(nextProject.name);
    }).catch((error) => {
      if (active && error instanceof Error && error.message !== "session_invalid") setLoadingFailed(true);
    });
    return () => { active = false; };
  }, [projectId]);

  useEffect(() => {
    if (!project || !session) return;
    const queue = new ProjectAutoSaveQueue({
      initialState: { canvas_state: project.canvas_state, name: project.name },
      initialVersion: project.state_version,
      onConflict: (latestVersion) => setConflictVersions({ latest: latestVersion, page: queue.stateVersion }),
      onSaved: (snapshot, stateVersion) => {
        setProject((current) => current ? {
          ...current, canvas_state: snapshot.canvas_state, name: snapshot.name,
          save_status: "saved", state_version: stateVersion,
        } : current);
        setName(snapshot.name);
      },
      onStatus: (status) => {
        saveStatusRef.current = status;
        setSaveStatus(status);
      },
      save: async (snapshot, 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 === 401) {
          window.dispatchEvent(new Event("dada:session-invalid"));
          throw new Error("session_invalid");
        }
        if (response.status === 412) {
          const conflict = await response.json() as { latest_state_version: number };
          throw new ProjectStateConflict(conflict.latest_state_version);
        }
        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;
    saveStatusRef.current = "saved";
    setSaveStatus("saved");
    return () => {
      queue.dispose();
      if (queueRef.current === queue) queueRef.current = undefined;
    };
  }, [project?.created_at, projectId, session?.csrf_token]);

  useEffect(() => {
    const guardActive = () => ["dirty", "saving", "failed"].includes(saveStatusRef.current);
    const beforeUnload = (event: BeforeUnloadEvent) => {
      if (!guardActive()) return;
      event.preventDefault();
      event.returnValue = "";
      void queueRef.current?.saveNow();
    };
    const interceptNavigation = (event: MouseEvent) => {
      if (!guardActive() || event.defaultPrevented || event.button !== 0) return;
      const target = event.target instanceof Element ? event.target.closest("a[href]") : null;
      if (!(target instanceof HTMLAnchorElement) || target.target || target.download) return;
      const destination = new URL(target.href, window.location.href);
      if (destination.origin !== window.location.origin) return;
      event.preventDefault();
      setLeaveStatus("");
      setPendingNavigation(destination.href);
    };
    window.addEventListener("beforeunload", beforeUnload);
    document.addEventListener("click", interceptNavigation, true);
    return () => {
      window.removeEventListener("beforeunload", beforeUnload);
      document.removeEventListener("click", interceptNavigation, true);
    };
  }, []);

  function updateName(value: string) {
    if (!project || saveStatus === "conflicted") return;
    setName(value);
    const snapshot: ProjectEditableState = { canvas_state: project.canvas_state, name: value };
    queueRef.current?.commit(snapshot);
  }

  async function rename(event: FormEvent) {
    event.preventDefault();
    if (!name.trim() || saveStatus === "conflicted") return;
    await queueRef.current?.saveNow();
  }

  async function exportConflictVersion() {
    if (!project || conflictExportBusy) return;
    setConflictExportBusy(true);
    setConflictNotice("");
    try {
      await conflictExportGuard.current.run(() => downloadConflictPng(project.canvas_state, name));
      setConflictNotice("仅下载本页版本，未写入项目");
    } catch {
      setConflictNotice("本页版本导出失败，未写入项目");
    } finally {
      setConflictExportUsed(conflictExportGuard.current.used);
      setConflictExportBusy(false);
    }
  }

  async function saveAndLeave() {
    if (!pendingNavigation) return;
    setLeaving(true);
    setLeaveStatus("");
    const saved = await queueRef.current?.saveNow();
    if (saved) window.location.assign(pendingNavigation);
    else setLeaveStatus("保存未完成，仍停留在当前页面");
    setLeaving(false);
  }

  if (!project && !loadingFailed) return <LoadingPage label="正在读取项目详情" />;
  if (!project) {
    return <main className="product-loading"><p role="alert">项目详情暂时无法读取。</p><a href="/app/projects">返回项目</a></main>;
  }
  const atHistoryLimit = project.successful_image_count >= 10;
  const conflicted = saveStatus === "conflicted";
  const saveLabel = {
    conflicted: "版本冲突",
    dirty: "有未保存修改",
    failed: "未保存",
    saved: "已保存",
    saving: "正在保存",
  }[saveStatus];

  return (
    <div className="product-page">
      <ProductHeader current="projects" />
      <main className="project-detail-page">
        <header className="project-detail-header">
          <a href="/app/projects" aria-label="返回项目列表">←</a>
          <div><p>{project.status === "failed_empty" ? "FAILED EMPTY" : project.status.toUpperCase()}</p><h1>{project.name}</h1></div>
          <span>固定比例 {project.ratio}</span>
        </header>
        {conflicted && conflictVersions ? (
          <section className="project-conflict" aria-live="assertive">
            <div>
              <p>STATE VERSION CONFLICT</p>
              <h2>版本冲突</h2>
              <span>本页版本 {conflictVersions.page}</span>
              <span>最新版本 {conflictVersions.latest}</span>
            </div>
            <p>此页面已转为只读。可本地导出当前页一次，然后刷新本机后端保存的最新版本。</p>
            <div className="project-conflict-actions">
              <button disabled={conflictExportBusy || conflictExportUsed} onClick={exportConflictVersion} type="button">本地导出本页版本</button>
              <button onClick={() => window.location.reload()} type="button">刷新最新版本</button>
            </div>
            {conflictNotice ? <strong role="status">{conflictNotice}</strong> : null}
          </section>
        ) : null}
        <section className="project-identity" aria-labelledby="rename-title">
          <div><h2 id="rename-title">项目名称</h2><p>state version {project.state_version}</p><strong className={`save-status ${saveStatus}`} aria-live={saveStatus === "failed" || conflicted ? "assertive" : "polite"}>{saveLabel}</strong></div>
          <form onSubmit={rename}>
            <input aria-label="项目名称" disabled={conflicted} maxLength={80} onChange={(event) => updateName(event.target.value)} value={name} />
            <button disabled={conflicted || saveStatus === "saving" || !name.trim() || (saveStatus === "saved" && name.trim() === project.name)} type="submit">立即保存</button>
          </form>
        </section>
        <div className="project-detail-grid">
          <section className="project-current" aria-labelledby="current-image-title">
            <header><h2 id="current-image-title">当前底图</h2><span>{project.pixel_width ?? 1080} × {project.pixel_height ?? 1440}</span></header>
            <ProjectPlaceholder ratio={project.ratio} status={project.status} />
            <div className="project-actions">
              <button disabled={conflicted || atHistoryLimit} onClick={() => window.location.assign(`/app?continue=${project.project_id}`)} type="button">继续生成</button>
              {conflicted || !project.current_image_id ? <button disabled type="button">进入编辑器</button> : <a href={`/app/projects/${project.project_id}/editor`}>进入编辑器</a>}
              {conflicted || !project.current_image_id ? <button disabled type="button">下载原始图</button> : <a href={`/api/v1/private-assets/projects/${project.project_id}/images/${project.current_image_id}`}>下载原始图</a>}
            </div>
            {atHistoryLimit ? <p className="project-blocker">请先删除一张非当前底图的历史图</p> : null}
            {project.status === "failed_empty" && !conflicted ? <a className="project-retry" href={`/app?retry=${project.project_id}`}>修改并重试</a> : null}
          </section>
          <section className="project-history" aria-labelledby="history-title">
            <header><h2 id="history-title">生成历史</h2><strong>{project.successful_image_count} / 10 张成功图</strong></header>
            {project.images.length === 0 ? (
              <div className="history-empty"><strong>还没有成功图片</strong><p>{project.draft_prompt}</p></div>
            ) : (
              <ol>
                {project.images.toReversed().map((image, index) => (
                  <li key={image.image_id} data-current={image.image_id === project.current_image_id}>
                    <ProjectPlaceholder ratio={project.ratio} status="active" />
                    <div><strong>生成结果 {project.images.length - index}</strong><time dateTime={image.created_at}>{formatUpdatedAt(image.created_at)}</time><a href={`/api/v1/private-assets/projects/${project.project_id}/images/${image.image_id}`}>下载原始图</a></div>
                  </li>
                ))}
              </ol>
            )}
          </section>
        </div>
        <section aria-labelledby="latest-exports-title" className="project-latest-exports">
          <header><div><p>LOCAL LATEST</p><h2 id="latest-exports-title">最新成品</h2></div><span>同一电脑可重新下载</span></header>
          {project.latest_exports.length === 0 ? (
            <div className="latest-exports-empty">
              <strong>暂无导出成品</strong>
              <div>
                <a href={`/app/projects/${project.project_id}/editor`}>进入编辑并导出</a>
                {project.current_image_id ? <a href={`/api/v1/private-assets/projects/${project.project_id}/images/${project.current_image_id}`}>下载原始生成图</a> : null}
              </div>
            </div>
          ) : (
            <ul>
              {project.latest_exports.map((item) => (
                <li key={item.export_id}>
                  <div><strong>{item.format.toUpperCase()}</strong><span>{item.pixel_width} × {item.pixel_height}</span></div>
                  <time dateTime={item.created_at}>{formatUpdatedAt(item.created_at)}</time>
                  <a href={item.download_url}>重新下载</a>
                </li>
              ))}
            </ul>
          )}
        </section>
      </main>
      {pendingNavigation ? (
        <div className="project-leave-overlay" role="presentation">
          <section aria-labelledby="leave-project-title" aria-modal="true" className="project-leave-dialog" role="dialog">
            <p>UNSAVED PROJECT</p>
            <h2 id="leave-project-title">有未保存修改</h2>
            <span>保存成功后才会离开当前项目。</span>
            <div>
              <button disabled={leaving} onClick={saveAndLeave} type="button">保存并离开</button>
              <button disabled={leaving} onClick={() => window.location.assign(pendingNavigation)} type="button">放弃修改</button>
              <button disabled={leaving} onClick={() => setPendingNavigation(undefined)} type="button">取消</button>
            </div>
            {leaveStatus ? <strong role="alert">{leaveStatus}</strong> : null}
          </section>
        </div>
      ) : null}
      <LocalOnlyFooter />
    </div>
  );
}
