import ExcelJS from "exceljs";
import JSZip from "jszip";
import {
  DownloadIcon,
  FileTextIcon,
  LoaderIcon,
  FileTypeIcon,
} from "lucide-react";
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type ComponentProps,
  type HTMLAttributes,
} from "react";
import { toast } from "sonner";
import { Streamdown } from "streamdown";

import {
  Artifact,
  ArtifactAction,
  ArtifactActions,
  ArtifactContent,
  ArtifactHeader,
  ArtifactTitle,
} from "@/components/ai-elements/artifact";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { DropdownSelector } from "@/components/ui/dropdown-selector";
import { Slider } from "@/components/ui/slider";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { CodeEditor } from "@/components/workspace/code-editor";
import { useArtifactContent } from "@/core/artifacts/hooks";
import { resolveArtifactURL, urlOfArtifact } from "@/core/artifacts/utils";
import { useI18n } from "@/core/i18n/hooks";
import { streamdownPlugins } from "@/core/streamdown";
import { checkCodeFile, getFileName } from "@/core/utils/files";
import { useMarkdownDownload } from "@/core/utils/markdown-download";
import { cn, copyToClipboard, truncateMiddle } from "@/lib/utils";

import { CitationLink } from "../citations/citation-link";

import { useArtifacts } from "./context";

const POST_MESSAGE_TYPES = {
  FULLSCREEN: "fullscreen",
} as const;

type RevoGridColumn = { prop: string; name: string };
type RevoGridRow = Record<string, string>;
type RevoGridSheetData = {
  columns: RevoGridColumn[];
  rows: RevoGridRow[];
};
type RevoGridElement = HTMLElement & {
  columns: RevoGridColumn[];
  source: RevoGridRow[];
  readonly: boolean;
  resize: boolean;
  rowHeaders: boolean;
  theme: string;
};

let revoGridLoaderPromise: Promise<void> | null = null;

function ensureRevoGridDefined() {
  if (typeof window === "undefined") return Promise.resolve();
  if (window.customElements.get("revo-grid")) return Promise.resolve();
  revoGridLoaderPromise ??= import("@revolist/revogrid/loader").then(
    ({ defineCustomElements }) => {
      defineCustomElements(window);
    },
  );
  return revoGridLoaderPromise;
}

function toExcelColumnLabel(index: number): string {
  let n = index;
  let label = "";
  while (n > 0) {
    const remainder = (n - 1) % 26;
    label = String.fromCharCode(65 + remainder) + label;
    n = Math.floor((n - 1) / 26);
  }
  return label || "A";
}

function toGridCellText(cell: ExcelJS.Cell): string {
  if (cell.text) return cell.text;
  const value = cell.value;
  if (value == null) return "";
  if (value instanceof Date) return value.toISOString();
  if (
    typeof value === "string" ||
    typeof value === "number" ||
    typeof value === "boolean" ||
    typeof value === "bigint"
  ) {
    return String(value);
  }
  if (typeof value === "object") {
    if ("result" in value && value.result != null) {
      const result = value.result;
      if (
        typeof result === "string" ||
        typeof result === "number" ||
        typeof result === "boolean" ||
        typeof result === "bigint"
      ) {
        return String(result);
      }
    }
    if ("text" in value && value.text) {
      const text = value.text;
      if (
        typeof text === "string" ||
        typeof text === "number" ||
        typeof text === "boolean" ||
        typeof text === "bigint"
      ) {
        return String(text);
      }
    }
    if ("hyperlink" in value && value.hyperlink) {
      const hyperlink = value.hyperlink;
      if (typeof hyperlink === "string") {
        return hyperlink;
      }
    }
  }
  return "";
}

function toRevoGridSheetData(worksheet: ExcelJS.Worksheet): RevoGridSheetData {
  const maxColumns = Math.max(worksheet.columnCount, 1);
  const headerRow = worksheet.getRow(1);

  const columns = Array.from({ length: maxColumns }, (_, idx) => {
    const columnIndex = idx + 1;
    const key = `c${columnIndex}`;
    const header = toGridCellText(headerRow.getCell(columnIndex)).trim();
    return {
      prop: key,
      name: header || toExcelColumnLabel(columnIndex),
    };
  });

  const rows: RevoGridRow[] = [];
  const lastRow = Math.max(worksheet.actualRowCount, worksheet.rowCount);

  for (let rowIndex = 2; rowIndex <= lastRow; rowIndex += 1) {
    const row = worksheet.getRow(rowIndex);
    const rowData: RevoGridRow = {};
    let hasContent = false;
    for (let columnIndex = 1; columnIndex <= maxColumns; columnIndex += 1) {
      const value = toGridCellText(row.getCell(columnIndex));
      rowData[`c${columnIndex}`] = value;
      if (!hasContent && value !== "") hasContent = true;
    }
    if (hasContent) rows.push(rowData);
  }

  return { columns, rows };
}

function sendToParent(message: unknown): void {
  if (window.parent !== window) {
    window.parent.postMessage(message, "*");
  }
}

export function ArtifactFileDetail({
  className,
  filepath: filepathFromProps,
  threadId,
}: {
  className?: string;
  filepath: string;
  threadId: string;
}) {
  const { t } = useI18n();
  const { artifacts, setOpen, select, fullscreen, setFullscreen } =
    useArtifacts();

  const isWriteFile = useMemo(() => {
    return filepathFromProps.startsWith("write-file:");
  }, [filepathFromProps]);
  const filepath = useMemo(() => {
    if (isWriteFile) {
      const url = new URL(filepathFromProps);
      return decodeURIComponent(url.pathname);
    }
    return filepathFromProps;
  }, [filepathFromProps, isWriteFile]);
  // 获取文件名（不含路径）
  const fileName = useMemo(() => getFileName(filepath), [filepath]);
  const isSkillFile = useMemo(() => {
    return filepath.endsWith(".skill");
  }, [filepath]);
  const { isCodeFile, language } = useMemo(() => {
    if (isWriteFile) {
      let language = checkCodeFile(filepath).language;
      language ??= "text";
      return { isCodeFile: true, language };
    }
    // Treat .skill files as markdown (they contain SKILL.md)
    if (isSkillFile) {
      return { isCodeFile: true, language: "markdown" };
    }
    return checkCodeFile(filepath);
  }, [filepath, isWriteFile, isSkillFile]);
  const previewable = useMemo(() => {
    return language === "html" || language === "markdown";
  }, [language]);
  const artifactUrl = useMemo(() => {
    if (!threadId) {
      return "";
    }
    return urlOfArtifact({ filepath, threadId });
  }, [filepath, threadId]);
  const artifactPreviewKind = useMemo(() => {
    return getArtifactPreviewKind(filepath);
  }, [filepath]);
  const artifactViewerSrcDoc = useMemo(() => {
    if (!artifactUrl) {
      return undefined;
    }
    return buildArtifactViewerSrcDoc({
      artifactUrl,
      fileName,
      kind: artifactPreviewKind,
      pdfPreviewMessage: t.artifactPreview.pdfPreviewFailed,
      unsupportedTypeMessage: t.artifactPreview.unsupportedType,
      openInNewTabLabel: t.artifactPreview.openInNewTab,
    });
  }, [
    artifactUrl,
    fileName,
    artifactPreviewKind,
    t.artifactPreview.openInNewTab,
    t.artifactPreview.pdfPreviewFailed,
    t.artifactPreview.unsupportedType,
  ]);
  // Native PDF iframe rendering is intentionally disabled; PDFs are rendered via pdf.js.
  const artifactViewerSrc = useMemo(() => {
    return undefined;
  }, []);
  const artifactViewerSandbox =
    "allow-same-origin allow-scripts allow-downloads";
  const { content } = useArtifactContent({
    threadId,
    filepath: filepathFromProps,
    enabled: Boolean(threadId) && isCodeFile && !isWriteFile,
  });

  const displayContent = content ?? "";
  const [isPackagingMarkdownBundle, setIsPackagingMarkdownBundle] =
    useState(false);

  const artifactOptions = useMemo(() => {
    return (artifacts ?? []).map((artifactPath) => ({
      value: artifactPath,
      label: getFileName(artifactPath),
    }));
  }, [artifacts]);

  const [viewMode, setViewMode] = useState<"code" | "preview">("code");
  const [zoom, setZoom] = useState(80);

  // 是否可以转换为docx/pdf（仅markdown文件支持）
  const canConvertToDocxPdf = language === "markdown";

  // 使用 Markdown 下载 hook
  const { isDownloading, downloadAsDocx, downloadAsPdf } = useMarkdownDownload({
    onError: (error, format) => {
      console.error(`Failed to download as ${format}:`, error);
      toast.error(`Failed to download as ${format.toUpperCase()}`);
    },
  });

  const resolveMarkdownAssetUrlForDownload = useCallback(
    (rawPath: string): string | null => {
      const normalizedRef = normalizeReference(rawPath);
      if (!normalizedRef) return null;
      if (isExternalReference(normalizedRef)) return normalizedRef;

      if (normalizedRef.startsWith("/mnt/user-data/")) {
        return urlOfArtifact({ filepath: normalizedRef, threadId });
      }
      if (normalizedRef.startsWith("mnt/user-data/")) {
        return urlOfArtifact({ filepath: `/${normalizedRef}`, threadId });
      }

      const resolvedVirtualPath = resolveReferencedVirtualPath(
        normalizedRef,
        filepath,
      );
      if (!resolvedVirtualPath) return null;
      return urlOfArtifact({ filepath: resolvedVirtualPath, threadId });
    },
    [filepath, threadId],
  );

  // 下载为 DOCX
  const handleDownloadDocx = useCallback(() => {
    if (content) {
      void downloadAsDocx(content, fileName, {
        resolveAssetUrl: resolveMarkdownAssetUrlForDownload,
      });
    }
  }, [content, fileName, downloadAsDocx, resolveMarkdownAssetUrlForDownload]);

  // 下载为 PDF
  const handleDownloadPdf = useCallback(() => {
    if (content) {
      void downloadAsPdf(content, fileName, {
        resolveAssetUrl: resolveMarkdownAssetUrlForDownload,
      });
    }
  }, [content, fileName, downloadAsPdf, resolveMarkdownAssetUrlForDownload]);

  const handleDownloadMarkdownBundle = useCallback(async () => {
    if (!threadId || !content) return;
    setIsPackagingMarkdownBundle(true);
    try {
      const zip = new JSZip();
      const markdownEntryPath = toWorkspaceRelativePath(filepath) ?? fileName;
      const referencedTargets = collectMarkdownAssetTargets(content);
      const refToVirtualPath = new Map<string, string>();

      for (const ref of referencedTargets) {
        const resolved = resolveReferencedVirtualPath(ref, filepath);
        if (resolved) {
          refToVirtualPath.set(ref, resolved);
        }
      }

      const refToRelativeZipPath = new Map<string, string>();
      const addedVirtualPaths = new Set<string>();

      for (const [ref, virtualPath] of refToVirtualPath) {
        const artifactEntryPath = toWorkspaceRelativePath(virtualPath);
        if (!artifactEntryPath) continue;

        const relativeFromMarkdown = toRelativePath(
          dirnamePosix(markdownEntryPath),
          artifactEntryPath,
        );
        refToRelativeZipPath.set(
          ref,
          relativeFromMarkdown || getFileName(artifactEntryPath),
        );

        if (addedVirtualPaths.has(virtualPath)) continue;
        addedVirtualPaths.add(virtualPath);

        const response = await fetch(
          urlOfArtifact({
            filepath: virtualPath,
            threadId,
          }),
        );
        if (!response.ok) {
          continue;
        }
        const data = await response.arrayBuffer();
        zip.file(artifactEntryPath, data);
      }

      const rewrittenMarkdown = rewriteMarkdownLinksForBundle(
        content,
        refToRelativeZipPath,
      );
      zip.file(markdownEntryPath, rewrittenMarkdown);

      const zipBlob = await zip.generateAsync({ type: "blob" });
      const zipName = `${fileName.replace(/\.md$/i, "") || "document"}.zip`;
      const url = URL.createObjectURL(zipBlob);
      const anchor = document.createElement("a");
      anchor.href = url;
      anchor.download = zipName;
      anchor.click();
      URL.revokeObjectURL(url);
      toast.success(t.common.exportSuccess);
    } catch (error) {
      console.error("Failed to package markdown bundle:", error);
      toast.error("Failed to package markdown with referenced files.");
    } finally {
      setIsPackagingMarkdownBundle(false);
    }
  }, [threadId, content, filepath, fileName, t.common.exportSuccess]);

  // 全屏切换处理
  const handleFullscreenToggle = useCallback(() => {
    const newFullscreen = !fullscreen;
    setFullscreen(newFullscreen);
    sendToParent({
      type: POST_MESSAGE_TYPES.FULLSCREEN,
      fullscreen: newFullscreen,
    });
  }, [fullscreen, setFullscreen]);

  useEffect(() => {
    if (previewable) {
      setViewMode("preview");
    } else {
      setViewMode("code");
    }
  }, [previewable]);

  return (
    // 给滚动遮挡头部定位relative
    <Artifact
      className={cn(
        "bg-background relative h-full overflow-hidden rounded-2xl",
        className,
      )}
    >
      <ArtifactHeader className="grid grid-cols-24">
        <div className="col-span-7 flex min-w-0 items-center justify-start gap-2 overflow-hidden">
          {previewable && (
            <ToggleGroup
              type="single"
              variant={null}
              size="default"
              className="bg-ws-surface-base h-[28px]"
              value={viewMode}
              onValueChange={(value) => {
                if (value) {
                  setViewMode(value as "code" | "preview");
                }
              }}
            >
              <ToggleGroupItem value="code">
                <svg
                  width="18"
                  height="18"
                  viewBox="0 0 18 18"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M5 6L2 9L5 12"
                    stroke="currentColor"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                  <path
                    d="M11 3L7 15"
                    stroke="currentColor"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                  <path
                    d="M13 6L16 9L13 12"
                    stroke="currentColor"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              </ToggleGroupItem>
              <ToggleGroupItem value="preview">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  width="16"
                  height="10"
                  viewBox="0 0 16 10"
                  fill="none"
                >
                  <path
                    d="M8 0.5C10.4943 0.5 12.8473 1.84466 14.792 4.21973C15.1644 4.67466 15.1644 5.32534 14.792 5.78027C12.8473 8.15534 10.4943 9.5 8 9.5C5.50561 9.49989 3.15269 8.15543 1.20801 5.78027C0.835561 5.32534 0.835562 4.67466 1.20801 4.21973C3.15269 1.84457 5.50561 0.500106 8 0.5Z"
                    stroke="currentColor"
                  />
                  <circle cx="8" cy="5" r="1.5" stroke="currentColor" />
                </svg>
              </ToggleGroupItem>
            </ToggleGroup>
          )}
          {/* 代码视图显示缩放控制；Markdown 预览也显示缩放控制 */}
          {(isCodeFile && viewMode === "code") ||
          (language === "markdown" && viewMode === "preview") ? (
            <ArtifactZoomSelector value={zoom} onChange={setZoom} />
          ) : null}
        </div>
        <div className="col-span-10 flex min-w-0 items-center justify-center px-1">
          <ArtifactTitle>
            {isWriteFile ? (
              <div className="w-full overflow-hidden px-2 text-center text-ellipsis whitespace-nowrap">
                {truncateMiddle(getFileName(filepath), 20)}
              </div>
            ) : (
              <DropdownSelector
                value={filepath}
                options={artifactOptions}
                onChange={select}
              />
            )}
          </ArtifactTitle>
        </div>
        <div className="col-span-7 flex min-w-0 items-center justify-end overflow-hidden">
          <ArtifactActions>
            {isCodeFile && (
              <ArtifactAction
                label={t.clipboard.copyToClipboard}
                disabled={!content}
                onClick={async () => {
                  try {
                    await copyToClipboard(displayContent ?? "");
                    toast.success(t.clipboard.copiedToClipboard);
                  } catch (error) {
                    toast.error("Failed to copy to clipboard");
                    console.error(error);
                  }
                }}
                tooltip={t.clipboard.copyToClipboard}
              >
                <svg
                  width="18"
                  height="18"
                  viewBox="0 0 18 18"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M6 2H13C14.1046 2 15 2.89543 15 4V13"
                    stroke="currentColor"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                  <rect
                    x="2.5"
                    y="4.5"
                    width="10"
                    height="11"
                    rx="1.5"
                    stroke="currentColor"
                  />
                </svg>
              </ArtifactAction>
            )}
            {!isWriteFile && (
              <DropdownMenu>
                <DropdownMenuTrigger asChild>
                  <ArtifactAction
                    label={t.common.download}
                    tooltip={t.common.download}
                  >
                    {isDownloading ? (
                      <LoaderIcon className="size-4 animate-spin" />
                    ) : (
                      <svg
                        width="18"
                        height="18"
                        viewBox="0 0 18 18"
                        fill="none"
                        xmlns="http://www.w3.org/2000/svg"
                      >
                        <path
                          d="M16 9V14C16 15.1046 15.1046 16 14 16H4C2.89543 16 2 15.1046 2 14V9"
                          stroke="currentColor"
                          strokeLinecap="round"
                        />
                        <path
                          d="M9 2V13M9 13L5 9M9 13L13 9"
                          stroke="currentColor"
                          strokeLinecap="round"
                          strokeLinejoin="round"
                        />
                      </svg>
                    )}
                  </ArtifactAction>
                </DropdownMenuTrigger>
                <DropdownMenuContent align="end" className="min-w-[160px]">
                  {language === "markdown" ? (
                    <DropdownMenuItem
                      onClick={handleDownloadMarkdownBundle}
                      disabled={isPackagingMarkdownBundle}
                      className="cursor-pointer"
                    >
                      {isPackagingMarkdownBundle ? (
                        <LoaderIcon className="size-4 animate-spin" />
                      ) : (
                        <DownloadIcon className="size-4" />
                      )}
                      {t.common.downloadOriginal}
                    </DropdownMenuItem>
                  ) : (
                    <DropdownMenuItem asChild>
                      <a
                        href={urlOfArtifact({
                          filepath,
                          threadId: threadId ?? "",
                          download: true,
                        })}
                        target="_blank"
                        className="w-full cursor-pointer"
                      >
                        <DownloadIcon className="size-4" />
                        {t.common.downloadOriginal}
                      </a>
                    </DropdownMenuItem>
                  )}
                  {/* DOCX 和 PDF 导出选项仅对 Markdown 文件显示。 */}
                  {canConvertToDocxPdf && (
                    <>
                      <DropdownMenuItem
                        onClick={handleDownloadDocx}
                        disabled={isDownloading !== null || !content}
                        className="cursor-pointer"
                      >
                        <FileTextIcon className="size-4" />
                        {isDownloading === "docx"
                          ? t.common.loading
                          : t.common.downloadAsDocx}
                      </DropdownMenuItem>
                      <DropdownMenuItem
                        onClick={handleDownloadPdf}
                        disabled={isDownloading !== null || !content}
                        className="cursor-pointer"
                      >
                        <FileTypeIcon className="size-4" />
                        {isDownloading === "pdf"
                          ? t.common.loading
                          : t.common.downloadAsPdf}
                      </DropdownMenuItem>
                    </>
                  )}
                </DropdownMenuContent>
              </DropdownMenu>
            )}
            {/* 全屏按钮 */}
            <ArtifactAction
              label={
                fullscreen ? t.common.closeFullScreen : t.common.fullScreen
              }
              onClick={handleFullscreenToggle}
              tooltip={
                fullscreen ? t.common.closeFullScreen : t.common.fullScreen
              }
            >
              {fullscreen ? (
                <svg
                  width="18"
                  height="18"
                  viewBox="0 0 18 18"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M6 2V4C6 5.10457 5.10457 6 4 6H2"
                    stroke="currentColor"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                  <path
                    d="M6 16V14C6 12.8954 5.10457 12 4 12H2"
                    stroke="currentColor"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                  <path
                    d="M12 2V4C12 5.10457 12.8954 6 14 6H16"
                    stroke="currentColor"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                  <path
                    d="M12 16V14C12 12.8954 12.8954 12 14 12H16"
                    stroke="currentColor"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              ) : (
                <svg
                  width="18"
                  height="18"
                  viewBox="0 0 18 18"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M5.75 15.5H4.5C3.39543 15.5 2.5 14.6046 2.5 13.5V12.25M2.5 5.75V4.5C2.5 3.39543 3.39543 2.5 4.5 2.5H5.75M12.25 2.5H13.5C14.6046 2.5 15.5 3.39543 15.5 4.5V5.75M15.5 12.25V13.5C15.5 14.6046 14.6046 15.5 13.5 15.5H12.25"
                    stroke="currentColor"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              )}
            </ArtifactAction>
            {!fullscreen && (
              <ArtifactAction
                label={t.common.close}
                onClick={() => setOpen(false)}
                tooltip={t.common.close}
              >
                <svg
                  width="18"
                  height="18"
                  viewBox="0 0 18 18"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M4 14L14 4M4 4L14 14"
                    stroke="currentColor"
                    strokeLinecap="round"
                  />
                </svg>
              </ArtifactAction>
            )}
          </ArtifactActions>
        </div>
      </ArtifactHeader>
      <ArtifactContent>
        {/* 遮挡多余的滚动顶部 */}
        {/* <div className="absolute w-[calc(100%-40px)] bg-ws-surface-base z-20 h-5 rounded-t-[10px] top-[57px]"></div> */}
        {previewable &&
          viewMode === "preview" &&
          (language === "markdown" || language === "html") && (
            <ArtifactFilePreview
              content={displayContent}
              language={language ?? "text"}
              zoom={zoom}
              threadId={threadId}
              filepath={filepath}
            />
          )}
        {isCodeFile && viewMode === "code" && (
          <div className="bg-ws-surface-base mb-0 mb-[207px] min-h-full rounded-b-[10px] p-0">
            <CodeEditor
              className="size-full resize-none rounded-none border-none py-[20px]"
              value={displayContent ?? ""}
              zoom={zoom}
              readonly
            />
          </div>
        )}
        {!isCodeFile &&
          (artifactPreviewKind === "pdf" ? (
            <ArtifactPdfPreview
              className="mb-[207px] h-full"
              artifactUrl={artifactUrl}
              fileName={fileName}
            />
          ) : isOfficePreviewKind(artifactPreviewKind) ? (
            <ArtifactOfficePreview
              className="mb-[207px] h-full"
              kind={artifactPreviewKind}
              artifactUrl={artifactUrl}
              fileName={fileName}
            />
          ) : (
            <PreviewIframe
              className="size-full border-0"
              containerClassName="h-full mb-[207px]"
              src={artifactViewerSrc}
              srcDoc={artifactViewerSrcDoc}
              sandbox={artifactViewerSandbox}
              title={`Artifact preview: ${fileName}`}
            />
          ))}
      </ArtifactContent>
    </Artifact>
  );
}

const USER_DATA_PREFIX = "/mnt/user-data/";

function normalizePosixPath(path: string): string {
  const isAbs = path.startsWith("/");
  const parts = path.split("/").filter((part) => part.length > 0);
  const stack: string[] = [];
  for (const part of parts) {
    if (part === ".") continue;
    if (part === "..") {
      if (stack.length > 0) stack.pop();
      continue;
    }
    stack.push(part);
  }
  return `${isAbs ? "/" : ""}${stack.join("/")}`;
}

function dirnamePosix(path: string): string {
  const normalized = normalizePosixPath(path);
  const index = normalized.lastIndexOf("/");
  if (index <= 0) return "";
  return normalized.slice(0, index);
}

function toWorkspaceRelativePath(virtualPath: string): string | null {
  const normalized = normalizePosixPath(virtualPath);
  if (!normalized.startsWith(USER_DATA_PREFIX)) return null;
  return normalized.slice(USER_DATA_PREFIX.length) || "artifact";
}

function toRelativePath(fromDir: string, targetPath: string): string {
  const from = normalizePosixPath(fromDir).split("/").filter(Boolean);
  const to = normalizePosixPath(targetPath).split("/").filter(Boolean);
  let i = 0;
  while (i < from.length && i < to.length && from[i] === to[i]) {
    i += 1;
  }
  const up = new Array(from.length - i).fill("..");
  const down = to.slice(i);
  return [...up, ...down].join("/") || ".";
}

function normalizeReference(ref: string): string {
  const trimmed = ref.trim().replace(/^<|>$/g, "");
  return trimmed.split(/[ \t]/)[0] ?? "";
}

function isExternalReference(ref: string): boolean {
  return (
    !ref ||
    ref.startsWith("#") ||
    ref.startsWith("//") ||
    /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(ref)
  );
}

function resolveReferencedVirtualPath(
  ref: string,
  baseFilepath: string,
): string | null {
  const normalizedRef = normalizeReference(ref);
  if (isExternalReference(normalizedRef)) return null;

  let withoutHash = normalizedRef.split("#")[0] ?? normalizedRef;
  withoutHash = withoutHash.split("?")[0] ?? withoutHash;
  if (!withoutHash) return null;

  if (withoutHash.startsWith("/mnt/user-data/")) {
    return normalizePosixPath(withoutHash);
  }
  if (withoutHash.startsWith("mnt/user-data/")) {
    return normalizePosixPath(`/${withoutHash}`);
  }
  if (withoutHash.startsWith("/")) {
    return null;
  }

  const baseDir = dirnamePosix(baseFilepath);
  const combined = normalizePosixPath(`${baseDir}/${withoutHash}`);
  if (!combined.startsWith(USER_DATA_PREFIX)) return null;
  return combined;
}

function collectMarkdownAssetTargets(markdown: string): Set<string> {
  const targets = new Set<string>();
  const markdownRefRegex = /!?\[[^\]]*\]\(([^)]+)\)/g;
  const htmlAttrRegex =
    /<(?:img|a)\b[^>]*\b(?:src|href)\s*=\s*(["'])([^"']+)\1/gi;

  for (const match of markdown.matchAll(markdownRefRegex)) {
    const raw = match[1]?.trim();
    if (raw) targets.add(raw);
  }
  for (const match of markdown.matchAll(htmlAttrRegex)) {
    const raw = match[2]?.trim();
    if (raw) targets.add(raw);
  }
  return targets;
}

function rewriteMarkdownLinksForBundle(
  markdown: string,
  refToRelativeZipPath: Map<string, string>,
): string {
  const rewriteTarget = (rawTarget: string): string => {
    const normalized = normalizeReference(rawTarget);
    return refToRelativeZipPath.get(normalized) ?? rawTarget;
  };

  const markdownRewritten = markdown.replace(
    /(!?\[[^\]]*\]\()([^)]+)(\))/g,
    (_full, prefix, target, suffix) => {
      return `${prefix}${rewriteTarget(String(target))}${suffix}`;
    },
  );

  return markdownRewritten.replace(
    /(<(?:img|a)\b[^>]*\b(?:src|href)\s*=\s*["'])([^"']+)(["'])/gi,
    (_full, prefix, target, suffix) => {
      return `${prefix}${rewriteTarget(String(target))}${suffix}`;
    },
  );
}

export function ArtifactFilePreview({
  content,
  language,
  zoom = 100,
  threadId,
  filepath,
}: {
  content: string;
  language: string;
  zoom?: number;
  threadId: string;
  filepath?: string;
}) {
  const zoomScale = zoom / 100;
  const normalizedContent = useMemo(() => {
    return rewriteArtifactImagePaths(content ?? "", threadId, filepath);
  }, [content, threadId, filepath]);

  if (language === "markdown") {
    return (
      <div
        className={cn("bg-ws-surface-base mb-[207px] w-full p-[20px]")}
        style={{ "--zoom-scale": zoomScale } as CSSProperties}
      >
        <Streamdown
          className="w-full"
          {...streamdownPlugins}
          components={{ a: CitationLink }}
        >
          {normalizedContent}
        </Streamdown>
      </div>
    );
  }
  if (language === "html") {
    return (
      <PreviewIframe
        className="size-full"
        containerClassName="h-full mb-[207px]"
        title="Artifact preview"
        srcDoc={normalizedContent}
        sandbox="allow-scripts allow-forms"
        style={{ zoom: zoomScale }}
      />
    );
  }
  return null;
}

function PreviewIframe({
  className,
  containerClassName,
  onLoad,
  src,
  srcDoc,
  ...props
}: ComponentProps<"iframe"> & {
  containerClassName?: string;
}) {
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setIsLoading(true);
  }, [src, srcDoc]);

  return (
    <div className={cn("relative", containerClassName)}>
      <iframe
        className={className}
        src={src}
        srcDoc={srcDoc}
        onLoad={(event) => {
          setIsLoading(false);
          onLoad?.(event);
        }}
        {...props}
      />
      {isLoading && (
        <div className="bg-ws-surface-base/85 absolute inset-0 z-10 flex items-center justify-center">
          <LoaderIcon className="text-muted-foreground size-5 animate-spin" />
        </div>
      )}
    </div>
  );
}

function ArtifactPdfPreview({
  className,
  artifactUrl,
  fileName,
}: {
  className?: string;
  artifactUrl: string;
  fileName: string;
}) {
  const { t } = useI18n();
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [pageCount, setPageCount] = useState(0);
  const containerRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    let disposed = false;

    async function renderPdf() {
      if (!artifactUrl || !containerRef.current) return;

      setIsLoading(true);
      setError(null);
      setPageCount(0);
      containerRef.current.innerHTML = "";

      try {
        const response = await fetch(artifactUrl);
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        const data = await response.arrayBuffer();

        const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
        pdfjs.GlobalWorkerOptions.workerSrc = new URL(
          "pdfjs-dist/legacy/build/pdf.worker.min.mjs",
          import.meta.url,
        ).toString();

        const loadingTask = pdfjs.getDocument({ data });
        const pdf = await loadingTask.promise;
        if (disposed || !containerRef.current) {
          await loadingTask.destroy();
          return;
        }

        setPageCount(pdf.numPages);

        const hostWidth = Math.max(
          640,
          Math.min(containerRef.current.clientWidth || 960, 1200),
        );
        const dpr = window.devicePixelRatio || 1;

        for (let pageNum = 1; pageNum <= pdf.numPages; pageNum += 1) {
          if (disposed || !containerRef.current) break;

          const page = await pdf.getPage(pageNum);
          const baseViewport = page.getViewport({ scale: 1 });
          const scale = (hostWidth - 32) / baseViewport.width;
          const viewport = page.getViewport({ scale });

          const pageWrapper = document.createElement("div");
          pageWrapper.className =
            "mx-auto mb-4 w-fit rounded-md border border-ws-line-default bg-ws-surface-base p-2 shadow-sm";

          const canvas = document.createElement("canvas");
          canvas.style.width = `${viewport.width}px`;
          canvas.style.height = `${viewport.height}px`;
          canvas.width = Math.floor(viewport.width * dpr);
          canvas.height = Math.floor(viewport.height * dpr);

          const context = canvas.getContext("2d");
          if (!context) continue;
          context.scale(dpr, dpr);

          pageWrapper.appendChild(canvas);
          containerRef.current.appendChild(pageWrapper);

          const renderTask = page.render({
            canvas,
            canvasContext: context,
            viewport,
          });
          await renderTask.promise;
        }
      } catch (err) {
        console.error("Failed to render pdf preview:", err);
        if (!disposed) {
          setError(t.artifactPreview.pdfPreviewFailed);
        }
      } finally {
        if (!disposed) {
          setIsLoading(false);
        }
      }
    }

    void renderPdf();

    return () => {
      disposed = true;
    };
  }, [artifactUrl, t.artifactPreview.pdfPreviewFailed]);

  if (error) {
    return (
      <div
        className={cn(
          "bg-ws-surface-subtle relative overflow-auto p-4",
          className,
        )}
      >
        <div className="border-ws-line-default bg-ws-surface-base mx-auto grid max-w-xl gap-3 rounded-md border p-5 text-center">
          <p className="text-sm font-medium break-all">{fileName}</p>
          <p className="text-muted-foreground text-sm">{error}</p>
          <a
            className="text-sm font-semibold text-blue-600 hover:underline"
            href={artifactUrl}
            target="_blank"
            rel="noopener noreferrer"
          >
            {t.artifactPreview.openInNewTab}
          </a>
        </div>
      </div>
    );
  }

  return (
    <div
      className={cn(
        "bg-ws-surface-subtle relative overflow-auto p-4",
        className,
      )}
    >
      <div className="text-ws-text-muted mb-3 text-center text-xs">
        {pageCount > 0
          ? t.artifactPreview.pageCountLabel(fileName, pageCount)
          : fileName}
      </div>
      <div ref={containerRef} />
      {isLoading && (
        <div className="bg-ws-surface-base/70 absolute inset-0 z-10 flex items-center justify-center">
          <LoaderIcon className="text-muted-foreground size-5 animate-spin" />
        </div>
      )}
    </div>
  );
}

function ArtifactOfficePreview({
  className,
  kind,
  artifactUrl,
  fileName,
}: {
  className?: string;
  kind: ArtifactPreviewKind;
  artifactUrl: string;
  fileName: string;
}) {
  const { t } = useI18n();
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [sheetNames, setSheetNames] = useState<string[]>([]);
  const [activeSheet, setActiveSheet] = useState<string>("");
  const [xlsxColumns, setXlsxColumns] = useState<RevoGridColumn[]>([]);
  const [xlsxRows, setXlsxRows] = useState<RevoGridRow[]>([]);
  const docxContainerRef = useRef<HTMLDivElement | null>(null);
  const xlsxGridContainerRef = useRef<HTMLDivElement | null>(null);
  const xlsxGridRef = useRef<RevoGridElement | null>(null);
  const workbookRef = useRef<ExcelJS.Workbook | null>(null);

  const canRenderDocx = kind === "docx";
  const canRenderXlsx = kind === "xlsx";
  const canRenderPptx = kind === "pptx";

  useEffect(() => {
    let disposed = false;

    async function renderDocx() {
      if (!canRenderDocx || !artifactUrl || !docxContainerRef.current) {
        return;
      }
      setIsLoading(true);
      setError(null);
      try {
        const response = await fetch(artifactUrl);
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        const blob = await response.blob();
        const { renderAsync } = await import("docx-preview");
        if (disposed || !docxContainerRef.current) {
          return;
        }
        docxContainerRef.current.innerHTML = "";
        await renderAsync(blob, docxContainerRef.current, undefined, {
          ignoreWidth: false,
          ignoreHeight: false,
          breakPages: true,
          inWrapper: true,
        });
      } catch (err) {
        console.error("Failed to render docx preview:", err);
        if (!disposed) {
          setError(t.artifactPreview.docxPreviewFailed);
        }
      } finally {
        if (!disposed) {
          setIsLoading(false);
        }
      }
    }

    void renderDocx();
    return () => {
      disposed = true;
    };
  }, [artifactUrl, canRenderDocx, t.artifactPreview.docxPreviewFailed]);

  useEffect(() => {
    let disposed = false;

    async function parseXlsx() {
      if (!canRenderXlsx || !artifactUrl) {
        return;
      }
      setIsLoading(true);
      setError(null);
      setSheetNames([]);
      setActiveSheet("");
      setXlsxColumns([]);
      setXlsxRows([]);
      workbookRef.current = null;
      try {
        const response = await fetch(artifactUrl);
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        const bytes = await response.arrayBuffer();
        const workbook = new ExcelJS.Workbook();
        await workbook.xlsx.load(bytes);
        workbookRef.current = workbook;
        const names = workbook.worksheets.map((sheet) => sheet.name);
        if (names.length === 0) {
          throw new Error("Empty workbook");
        }
        if (disposed) return;
        setSheetNames(names);
        setActiveSheet(names[0] ?? "");
      } catch (err) {
        console.error("Failed to render xlsx preview:", err);
        if (!disposed) {
          setError(t.artifactPreview.excelPreviewFailed);
        }
      } finally {
        if (!disposed) {
          setIsLoading(false);
        }
      }
    }

    void parseXlsx();
    return () => {
      disposed = true;
    };
  }, [artifactUrl, canRenderXlsx, t.artifactPreview.excelPreviewFailed]);

  useEffect(() => {
    if (!canRenderXlsx || !activeSheet || !workbookRef.current) {
      return;
    }
    try {
      const sheet = workbookRef.current.getWorksheet(activeSheet);
      if (!sheet) return;
      const { columns, rows } = toRevoGridSheetData(sheet);
      setXlsxColumns(columns);
      setXlsxRows(rows);
    } catch (err) {
      console.error("Failed to switch xlsx sheet:", err);
      setError(t.artifactPreview.switchSheetFailed);
    }
  }, [activeSheet, canRenderXlsx, t.artifactPreview.switchSheetFailed]);

  useEffect(() => {
    if (!canRenderXlsx || !xlsxGridContainerRef.current) {
      return;
    }
    let disposed = false;

    async function renderGrid() {
      try {
        await ensureRevoGridDefined();
        if (disposed || !xlsxGridContainerRef.current) return;

        let grid = xlsxGridRef.current;
        if (!grid) {
          grid = document.createElement("revo-grid") as RevoGridElement;
          grid.style.width = "100%";
          grid.style.height = "100%";
          grid.readonly = true;
          grid.resize = true;
          grid.rowHeaders = true;
          grid.theme = "default";
          xlsxGridContainerRef.current.innerHTML = "";
          xlsxGridContainerRef.current.appendChild(grid);
          xlsxGridRef.current = grid;
        }

        grid.columns = xlsxColumns;
        grid.source = xlsxRows;
      } catch (err) {
        console.error("Failed to render RevoGrid preview:", err);
        if (!disposed) {
          setError(t.artifactPreview.excelGridPreviewFailed);
        }
      }
    }

    void renderGrid();

    return () => {
      disposed = true;
    };
  }, [
    canRenderXlsx,
    xlsxColumns,
    xlsxRows,
    t.artifactPreview.excelGridPreviewFailed,
  ]);
  useEffect(() => {
    if (!canRenderPptx) {
      return;
    }
    setIsLoading(false);
    setError(t.artifactPreview.pptxDownloadHint);
  }, [canRenderPptx, t.artifactPreview.pptxDownloadHint]);

  return (
    <div
      className={cn(
        "bg-ws-surface-base relative h-full overflow-hidden",
        className,
      )}
    >
      {canRenderXlsx && sheetNames.length > 0 && (
        <div className="border-border flex items-center gap-1 overflow-x-auto border-b p-2">
          {sheetNames.map((sheetName) => (
            <button
              key={sheetName}
              type="button"
              className={cn(
                "rounded px-4 py-3 text-xs whitespace-nowrap",
                activeSheet === sheetName
                  ? "bg-ws-accent-tint-soft text-foreground"
                  : "text-muted-foreground hover:text-foreground",
              )}
              onClick={() => setActiveSheet(sheetName)}
            >
              {sheetName}
            </button>
          ))}
        </div>
      )}

      <div className="h-full overflow-auto p-4">
        {canRenderDocx && (
          <div
            ref={docxContainerRef}
            className="docx-preview-wrap mx-auto max-w-5xl"
          />
        )}
        {canRenderXlsx && (
          <div
            ref={xlsxGridContainerRef}
            className="h-full min-h-[320px] overflow-hidden rounded border"
          />
        )}
      </div>

      {error && (
        <ArtifactPreviewFallback
          fileName={fileName}
          artifactUrl={artifactUrl}
          message={error}
        />
      )}
      {isLoading && (
        <div className="bg-ws-surface-base/85 absolute inset-0 z-10 flex items-center justify-center">
          <LoaderIcon className="text-muted-foreground size-5 animate-spin" />
        </div>
      )}
    </div>
  );
}

function ArtifactPreviewFallback({
  message,
  fileName,
  artifactUrl,
}: {
  message: string;
  fileName: string;
  artifactUrl: string;
}) {
  const { t } = useI18n();
  return (
    <div className="bg-ws-surface-base absolute inset-0 z-20 grid place-content-center p-6 text-center">
      <p className="text-foreground mb-2 text-sm font-medium">{fileName}</p>
      <p className="text-muted-foreground mb-3 text-xs">{message}</p>
      <a
        className="text-primary text-sm font-medium underline"
        href={artifactUrl}
        target="_blank"
        rel="noreferrer"
      >
        {t.artifactPreview.clickToDownload}
      </a>
    </div>
  );
}

function rewriteArtifactImagePaths(
  content: string,
  threadId?: string,
  baseFilepath?: string,
) {
  if (!threadId) {
    return content;
  }

  const encodeVirtualPath = (path: string) =>
    path
      .split("/")
      .map((segment) => {
        try {
          return encodeURIComponent(decodeURIComponent(segment));
        } catch {
          return encodeURIComponent(segment);
        }
      })
      .join("/");
  const toArtifactUrl = (rawPath: string) => {
    const trimmedPath = rawPath.trim();
    const normalizedPath = trimmedPath.startsWith("/")
      ? trimmedPath
      : `/${trimmedPath}`;
    return resolveArtifactURL(encodeVirtualPath(normalizedPath), threadId);
  };
  const toArtifactUrlFromRelative = (rawPath: string) => {
    const trimmed = rawPath.trim();
    if (!baseFilepath || !trimmed) return null;
    if (trimmed.startsWith("/") || trimmed.startsWith("//")) return null;
    if (trimmed.startsWith("#")) return null;
    if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return null;

    const baseDir = baseFilepath.replace(/[^/]*$/, "");
    if (!baseDir.startsWith("/")) return null;

    const absolutePath = new URL(trimmed, `file://${baseDir}`).pathname;
    if (!absolutePath.startsWith("/mnt/user-data/")) return null;
    return resolveArtifactURL(encodeVirtualPath(absolutePath), threadId);
  };

  const markdownRewritten = content.replace(
    /!\[([^\]]*)\]\(\s*(\/?mnt\/user-data\/(?:outputs|uploads)\/[^)]+?)\s*\)/g,
    (_full, alt, rawPath) => {
      return `![${alt}](${toArtifactUrl(rawPath)})`;
    },
  );
  const markdownRelativeRewritten = markdownRewritten.replace(
    /!\[([^\]]*)\]\(\s*([^)]+?)\s*\)/g,
    (_full, alt, rawPath) => {
      const absoluteUrl = toArtifactUrlFromRelative(rawPath);
      if (!absoluteUrl) {
        return `![${alt}](${rawPath})`;
      }
      return `![${alt}](${absoluteUrl})`;
    },
  );

  const shorthandMarkdownRewritten = markdownRelativeRewritten.replace(
    /!(?!\[)([^\n()（）]+?)\s*[（(]\s*(\/?mnt\/user-data\/(?:outputs|uploads)\/[^)）]+?)\s*[)）]/g,
    (_full, alt, rawPath) => {
      return `![${String(alt).trim()}](${toArtifactUrl(rawPath)})`;
    },
  );

  return shorthandMarkdownRewritten.replace(
    /(<img\b[^>]*\bsrc\s*=\s*)(["'])([^"']+)\2/gi,
    (_full, prefix, quote, rawPath) => {
      if (/^\/?mnt\/user-data\/(?:outputs|uploads)\//.test(rawPath)) {
        return `${prefix}${quote}${toArtifactUrl(rawPath)}${quote}`;
      }
      const absoluteUrl = toArtifactUrlFromRelative(rawPath);
      if (absoluteUrl) {
        return `${prefix}${quote}${absoluteUrl}${quote}`;
      }
      return `${prefix}${quote}${rawPath}${quote}`;
    },
  );
}

type ArtifactPreviewKind =
  | "html"
  | "image"
  | "video"
  | "audio"
  | "pdf"
  | "docx"
  | "xlsx"
  | "pptx"
  | "other";

function getArtifactPreviewKind(filepath: string): ArtifactPreviewKind {
  const lower = filepath.toLowerCase();
  if (/\.(html?|xhtml)$/.test(lower)) return "html";
  if (/\.(png|jpe?g|gif|webp|bmp|svg|ico|avif|tiff?)$/.test(lower))
    return "image";
  if (/\.(mp4|webm|ogg|mov|m4v)$/.test(lower)) return "video";
  if (/\.(mp3|wav|ogg|m4a|aac|flac)$/.test(lower)) return "audio";
  if (lower.endsWith(".pdf")) return "pdf";
  if (/\.(docx?)$/.test(lower)) return "docx";
  if (/\.(xlsx?)$/.test(lower)) return "xlsx";
  if (/\.(pptx?)$/.test(lower)) return "pptx";
  return "other";
}

const OFFICE_PREVIEW_KINDS = new Set<ArtifactPreviewKind>([
  "docx",
  "xlsx",
  "pptx",
]);

function isOfficePreviewKind(kind: ArtifactPreviewKind) {
  return OFFICE_PREVIEW_KINDS.has(kind);
}

function escapeHtml(value: string): string {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function buildArtifactViewerSrcDoc({
  artifactUrl,
  fileName,
  kind,
  pdfPreviewMessage,
  unsupportedTypeMessage,
  openInNewTabLabel,
}: {
  artifactUrl: string;
  fileName: string;
  kind: ArtifactPreviewKind;
  pdfPreviewMessage: string;
  unsupportedTypeMessage: string;
  openInNewTabLabel: string;
}) {
  const safeUrl = escapeHtml(artifactUrl);
  const safeName = escapeHtml(fileName);
  const safePdfPreviewMessage = escapeHtml(pdfPreviewMessage);
  const safeUnsupportedTypeMessage = escapeHtml(unsupportedTypeMessage);
  const safeOpenInNewTabLabel = escapeHtml(openInNewTabLabel);

  const content = (() => {
    if (kind === "image") {
      return `<img class="preview image" src="${safeUrl}" alt="${safeName}" />`;
    }
    if (kind === "video") {
      return `<video class="preview media" src="${safeUrl}" controls playsinline preload="metadata"></video>`;
    }
    if (kind === "audio") {
      return `<div class="audio-wrap"><audio class="audio" src="${safeUrl}" controls preload="metadata"></audio></div>`;
    }
    if (kind === "pdf") {
      return `<div class="fallback">
      <p class="title">${safeName}</p>
      <p class="desc">${safePdfPreviewMessage}</p>
      <a class="link" href="${safeUrl}" target="_blank" rel="noopener noreferrer">${safeOpenInNewTabLabel}</a>
    </div>`;
    }
    if (kind === "html") {
      return `<iframe class="preview frame" src="${safeUrl}" sandbox="allow-scripts allow-forms allow-modals allow-popups allow-downloads"></iframe>`;
    }
    return `<div class="fallback">
      <p class="title">${safeName}</p>
      <p class="desc">${safeUnsupportedTypeMessage}</p>
      <a class="link" href="${safeUrl}" target="_blank" rel="noopener noreferrer">${safeOpenInNewTabLabel}</a>
    </div>`;
  })();

  const bodyClass = kind === "image" ? "fullbleed" : "";

  return `<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <style>
    :root {
      --ws-color-surface-app: rgb(248 249 251);
      --ws-color-surface-base: rgb(255 255 255);
      --ws-color-text-primary-strong: rgb(15 23 42);
      --ws-color-text-muted: rgb(102 112 133);
      --ws-color-line-default: rgb(228 231 236);
      --ws-color-surface-checker: rgb(244 244 245);
      --ws-color-black-solid: rgb(0 0 0);
      --ws-color-info-primary: rgb(37 99 235);
      --bg: var(--ws-color-surface-app);
      --panel: var(--ws-color-surface-base);
      --text: var(--ws-color-text-primary-strong);
      --muted: var(--ws-color-text-muted);
      --line: var(--ws-color-line-default);
      --checker: var(--ws-color-surface-checker);
      --media-bg: var(--ws-color-black-solid);
      --link: var(--ws-color-info-primary);
      --radius: 12px;
    }
    @media (prefers-color-scheme: dark) {
      :root {
        --ws-color-surface-app: rgb(32 36 44);
        --ws-color-surface-base: rgb(42 39 49);
        --ws-color-text-primary-strong: rgb(230 234 242);
        --ws-color-text-muted: rgb(152 162 179);
        --ws-color-line-default: rgb(58 61 69);
        --ws-color-surface-checker: rgb(44 47 56);
        --ws-color-black-solid: rgb(0 0 0);
        --ws-color-info-primary: rgb(127 178 255);
      }
    }
    * { box-sizing: border-box; }
    html, body {
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
      background: var(--bg);
      color: var(--text);
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
    }
    body {
      display: flex;
      align-items: stretch;
      justify-content: stretch;
      overflow: hidden;
      padding: 12px;
    }
    body.fullbleed {
      padding: 0;
    }
    .preview {
      width: 100%;
      height: 100%;
      padding: 10px 12px;
      border: 1px solid var(--line);
      border-radius: var(--radius);
      background: var(--panel);
      display: block;
    }
    .image {
      object-fit: contain;
      object-position: center;
      background:
        linear-gradient(45deg, var(--checker) 25%, transparent 25%, transparent 75%, var(--checker) 75%, var(--checker)) 0 0/16px 16px,
        linear-gradient(45deg, var(--checker) 25%, transparent 25%, transparent 75%, var(--checker) 75%, var(--checker)) 8px 8px/16px 16px,
        var(--panel);
    }
    .media {
      object-fit: contain;
      background: var(--media-bg);
    }
    .frame {
      border: 1px solid var(--line);
    }
    .audio-wrap {
      width: 100%;
      height: 100%;
      border: 1px solid var(--line);
      border-radius: var(--radius);
      background: var(--panel);
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 16px;
    }
    .audio {
      width: min(720px, 100%);
    }
    .fallback {
      width: 100%;
      height: 100%;
      border: 1px solid var(--line);
      border-radius: var(--radius);
      background: var(--panel);
      display: grid;
      place-content: center;
      text-align: center;
      gap: 10px;
      padding: 24px;
    }
    .title {
      margin: 0;
      font-size: 16px;
      font-weight: 600;
      word-break: break-all;
    }
    .desc {
      margin: 0;
      font-size: 13px;
      color: var(--muted);
    }
    .link {
      color: var(--link);
      text-decoration: none;
      font-weight: 600;
    }
    .link:hover { text-decoration: underline; }
  </style>
</head>
<body class="${bodyClass}">
  ${content}
</body>
</html>`;
}

// 缩放比例选项
const ZOOM_LEVELS = [50, 60, 70, 80, 90, 100, 110, 120, 130, 150, 175, 200];

export type ArtifactZoomSelectorProps = Omit<
  HTMLAttributes<HTMLDivElement>,
  "onChange"
> & {
  value?: number;
  onChange?: (value: number) => void;
};

export const ArtifactZoomSelector = ({
  value = 100,
  onChange,
  className,
  ...props
}: ArtifactZoomSelectorProps) => {
  const { t } = useI18n();
  const resolvedIndex = useMemo(() => {
    const exactIndex = ZOOM_LEVELS.indexOf(value);
    if (exactIndex >= 0) return exactIndex;
    let nearestIndex = 0;
    let nearestDistance = Number.POSITIVE_INFINITY;
    ZOOM_LEVELS.forEach((level, index) => {
      const distance = Math.abs(level - value);
      if (distance < nearestDistance) {
        nearestDistance = distance;
        nearestIndex = index;
      }
    });
    return nearestIndex;
  }, [value]);

  return (
    <div className={cn("inline-flex", className)} {...props}>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <button
            type="button"
            aria-label={t.artifactPreview.zoomIn}
            className={cn(
              "bg-background border-border text-muted-foreground hover:text-foreground inline-flex h-[28px] w-[28px] items-center justify-center rounded-[10px] border transition-colors",
              "hover:bg-muted/60",
            )}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="16"
              height="16"
              viewBox="0 0 16 16"
              fill="none"
            >
              <circle
                cx="7.55558"
                cy="7.55534"
                r="6.16667"
                stroke="currentColor"
              />
              <path
                d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
                fill="currentColor"
              />
              <path
                d="M5.33325 7.5H9.7777M7.55547 5V10"
                stroke="currentColor"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent
          align="start"
          sideOffset={8}
          className="w-52 p-[20px]"
        >
          <div className="mb-2 flex items-center justify-between">
            <span className="text-muted-foreground text-xs">
              {ZOOM_LEVELS[0]}%
            </span>
            <span className="text-foreground text-xs font-medium">
              {value}%
            </span>
          </div>
          <Slider
            min={0}
            max={ZOOM_LEVELS.length - 1}
            step={1}
            value={[resolvedIndex]}
            onValueChange={(values) => {
              const nextIndex = values[0];
              if (nextIndex === undefined) return;
              const nextValue = ZOOM_LEVELS[nextIndex];
              if (nextValue !== undefined) onChange?.(nextValue);
            }}
          />
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
};
