import {
  Document as DocxDocument,
  Packer,
  Paragraph,
  TextRun,
  HeadingLevel,
  ImageRun,
  type ParagraphChild,
} from "docx";
import { marked } from "marked";

// ============================================================================
// Types
// ============================================================================

/**
 * Markdown Token 类型（简化版）
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type MarkdownToken = any;

/**
 * PDF 转换选项
 */
export interface PdfOptions {
  /**
   * 页边距 [上, 右, 下, 左]，单位 mm
   * @default [15, 15, 15, 15]
   */
  margin?: [number, number, number, number];
  /**
   * 页面格式
   * @default "a4"
   */
  format?: "a3" | "a4" | "a5" | "letter" | "legal";
  /**
   * 页面方向
   * @default "portrait"
   */
  orientation?: "portrait" | "landscape";
  /**
   * 缩放比例
   * @default 2
   */
  scale?: number;
}

/**
 * DOCX 转换选项
 */
export interface DocxOptions {
  /**
   * 代码块字体
   * @default "Courier New"
   */
  codeFont?: string;
  /**
   * 代码块字号（半磅）
   * @default 22 (11pt)
   */
  codeFontSize?: number;
  /**
   * 解析 Markdown 里的资源路径（如图片相对路径）
   */
  resolveAssetUrl?: (rawPath: string) => string | null | Promise<string | null>;
}

// ============================================================================
// DOCX Converter
// ============================================================================

/**
 * 将 Markdown 内容转换为 DOCX 文件并下载
 *
 * @param markdown - Markdown 文本内容
 * @param filename - 文件名（不含扩展名，或包含 .md 扩展名）
 * @param options - 转换选项
 *
 * @example
 * ```ts
 * await downloadMarkdownAsDocx("# Hello World", "document");
 * ```
 */
export async function downloadMarkdownAsDocx(
  markdown: string,
  filename: string,
  options: DocxOptions = {},
): Promise<void> {
  const {
    codeFont = "Courier New",
    codeFontSize = 22,
    resolveAssetUrl,
  } = options;

  const tokens = marked.lexer(markdown);
  const children = await parseTokensToDocx(tokens, {
    codeFont,
    codeFontSize,
    resolveAssetUrl,
  });

  const doc = new DocxDocument({
    sections: [{ children }],
  });

  const blob = await Packer.toBlob(doc);
  downloadBlob(blob, normalizeFilename(filename, ".docx"));
}

// ============================================================================
// PDF Converter
// ============================================================================

/**
 * 将 Markdown 内容转换为 PDF 文件并下载
 *
 * @param markdown - Markdown 文本内容
 * @param filename - 文件名（不含扩展名，或包含 .md 扩展名）
 * @param options - 转换选项
 *
 * @example
 * ```ts
 * await downloadMarkdownAsPdf("# Hello World", "document");
 * ```
 */
export async function downloadMarkdownAsPdf(
  markdown: string,
  filename: string,
  options: PdfOptions & {
    resolveAssetUrl?: (
      rawPath: string,
    ) => string | null | Promise<string | null>;
  } = {},
): Promise<void> {
  const html2pdf = await loadHtml2Pdf();

  const {
    margin = [15, 15, 15, 15],
    format = "a4",
    orientation = "portrait",
    scale = 2,
    resolveAssetUrl,
  } = options;

  const normalizedMarkdown = await rewriteMarkdownImageSources(
    markdown,
    resolveAssetUrl,
  );

  // 解析 Markdown 为 HTML
  const htmlContent = await marked.parse(normalizedMarkdown);

  // 创建容器并应用样式
  const container = createStyledContainer(htmlContent);

  // 配置 html2pdf
  const opt = {
    margin,
    filename: normalizeFilename(filename, ".pdf"),
    image: { type: "jpeg" as const, quality: 0.98 },
    html2canvas: {
      scale,
      useCORS: true,
      logging: false,
      onclone: fixColorsForHtml2Canvas,
    },
    jsPDF: { unit: "mm" as const, format, orientation },
  };

  await html2pdf().set(opt).from(container).save();
}

// ============================================================================
// Internal Utilities
// ============================================================================

/**
 * 动态加载 html2pdf.js
 */
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
async function loadHtml2Pdf(): Promise<Function> {
  const html2pdf = await import("html2pdf.js");
  return html2pdf.default;
}

/**
 * 创建带样式的 HTML 容器
 */
function createStyledContainer(htmlContent: string): HTMLDivElement {
  const container = document.createElement("div");
  container.innerHTML = htmlContent;

  // 容器基础样式
  container.style.cssText = `
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    font-size: 14px;
    line-height: 1.6;
    padding: 20px;
    max-width: 800px;
    color: #333333;
    background-color: #ffffff;
  `;

  // 应用元素样式
  applyElementStyles(container);

  return container;
}

/**
 * 应用元素样式
 */
function applyElementStyles(container: HTMLElement): void {
  // 标题
  container.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
    const el = h as HTMLElement;
    el.style.marginTop = "1.5em";
    el.style.marginBottom = "0.5em";
    el.style.fontWeight = "600";
    el.style.color = "#1a1a1a";
  });

  // 段落
  container.querySelectorAll("p").forEach((p) => {
    (p as HTMLElement).style.marginBottom = "1em";
  });

  // 代码块
  container.querySelectorAll("pre, code").forEach((code) => {
    const el = code as HTMLElement;
    el.style.fontFamily = "'SF Mono', 'Fira Code', Consolas, monospace";
    el.style.backgroundColor = "#f5f5f5";
    el.style.color = "#333333";
    el.style.fontSize = "13px";
    if (code.tagName === "PRE") {
      el.style.padding = "12px";
      el.style.borderRadius = "6px";
      el.style.overflow = "auto";
    } else {
      el.style.padding = "2px 4px";
      el.style.borderRadius = "3px";
    }
  });

  // 列表
  container.querySelectorAll("ul, ol").forEach((list) => {
    const el = list as HTMLElement;
    el.style.marginBottom = "1em";
    el.style.paddingLeft = "2em";
  });

  // 引用块
  container.querySelectorAll("blockquote").forEach((bq) => {
    const el = bq as HTMLElement;
    el.style.borderLeft = "4px solid #dddddd";
    el.style.marginLeft = "0";
    el.style.paddingLeft = "16px";
    el.style.color = "#666666";
  });

  // 表格
  container.querySelectorAll("table").forEach((table) => {
    const el = table as HTMLElement;
    el.style.borderCollapse = "collapse";
    el.style.width = "100%";
    el.style.marginBottom = "1em";
  });

  container.querySelectorAll("th, td").forEach((cell) => {
    const el = cell as HTMLElement;
    el.style.border = "1px solid #dddddd";
    el.style.padding = "8px";
  });

  // 链接
  container.querySelectorAll("a").forEach((link) => {
    const el = link as HTMLElement;
    el.style.color = "#0066cc";
    el.style.textDecoration = "underline";
  });

  // 分割线
  container.querySelectorAll("hr").forEach((hr) => {
    const el = hr as HTMLElement;
    el.style.border = "none";
    el.style.borderTop = "1px solid #dddddd";
    el.style.margin = "2em 0";
  });
}

/**
 * 修复 html2canvas 不支持的颜色函数
 */
function fixColorsForHtml2Canvas(clonedDoc: Document): void {
  // 移除外部样式表（可能包含 lab、oklab 等不支持的颜色）
  clonedDoc
    .querySelectorAll<
      HTMLStyleElement | HTMLLinkElement
    >('link[rel="stylesheet"], style')
    .forEach((sheet) => sheet.remove());

  // 重置所有元素的颜色属性为安全值
  clonedDoc.querySelectorAll<HTMLElement>("*").forEach((el) => {
    const props = [
      "color",
      "background-color",
      "border-color",
      "border-top-color",
      "border-bottom-color",
      "border-left-color",
      "border-right-color",
      "outline-color",
      "text-decoration-color",
      "caret-color",
      "column-rule-color",
      "accent-color",
      "fill",
      "stroke",
    ];

    props.forEach((prop) => el.style.removeProperty(prop));

    el.style.color = "#333333";
    el.style.backgroundColor = "transparent";
  });

  // 设置 body 背景
  const body = clonedDoc.body;
  body.style.color = "#333333";
  body.style.backgroundColor = "#ffffff";
}

/**
 * 解析 Markdown Token 为 DOCX Paragraph
 */
async function parseTokensToDocx(
  tokens: MarkdownToken[],
  options: Required<Pick<DocxOptions, "codeFont" | "codeFontSize">> &
    Pick<DocxOptions, "resolveAssetUrl">,
): Promise<Paragraph[]> {
  const paragraphs: Paragraph[] = [];

  for (const token of tokens) {
    switch (token.type) {
      case "heading": {
        const runs = await parseInlineTokens(token.tokens ?? [], options);
        paragraphs.push(
          new Paragraph({
            children: runs,
            heading: getHeadingLevel(token.depth),
            spacing: { before: 240, after: 120 },
          }),
        );
        break;
      }

      case "paragraph": {
        const runs = await parseInlineTokens(token.tokens ?? [], options);
        paragraphs.push(
          new Paragraph({
            children: runs.length > 0 ? runs : [new TextRun("")],
            spacing: { after: 200 },
          }),
        );
        break;
      }

      case "code": {
        const lines = token.text.split("\n");
        lines.forEach((line: string) => {
          paragraphs.push(
            new Paragraph({
              children: [
                new TextRun({
                  text: line.length > 0 ? line : " ",
                  font: options.codeFont,
                  size: options.codeFontSize,
                }),
              ],
              shading: { fill: "F5F5F5" },
            }),
          );
        });
        paragraphs.push(new Paragraph({ children: [] }));
        break;
      }

      case "list": {
        for (const item of token.items ?? []) {
          const runs = await parseInlineTokens(
            item.tokens?.[0]?.tokens ?? [],
            options,
          );
          paragraphs.push(
            new Paragraph({
              children: runs.length > 0 ? runs : [new TextRun("")],
              bullet: { level: 0 },
              spacing: { after: 80 },
            }),
          );
        }
        break;
      }

      case "blockquote": {
        const runs = await parseInlineTokens(
          token.tokens?.[0]?.tokens ?? [],
          options,
        );
        paragraphs.push(
          new Paragraph({
            children: runs.length > 0 ? runs : [new TextRun("")],
            indent: { left: 720 },
            border: { left: { style: "single", size: 12, color: "CCCCCC" } },
            spacing: { after: 200 },
          }),
        );
        break;
      }

      case "hr": {
        paragraphs.push(
          new Paragraph({
            children: [new TextRun({ text: "─".repeat(50), color: "CCCCCC" })],
            spacing: { before: 200, after: 200 },
          }),
        );
        break;
      }

      case "space": {
        paragraphs.push(new Paragraph({ children: [] }));
        break;
      }

      case "image": {
        const imageRun = await createImageRunFromToken(token, options);
        if (imageRun) {
          paragraphs.push(
            new Paragraph({
              children: [imageRun],
              spacing: { after: 200 },
            }),
          );
        }
        break;
      }
    }
  }

  return paragraphs;
}

/**
 * 解析行内 Token 为 TextRun
 */
async function parseInlineTokens(
  tokens: MarkdownToken[],
  options: Required<Pick<DocxOptions, "codeFont" | "codeFontSize">> &
    Pick<DocxOptions, "resolveAssetUrl">,
): Promise<ParagraphChild[]> {
  const runs: ParagraphChild[] = [];

  for (const token of tokens) {
    switch (token.type) {
      case "text":
        runs.push(new TextRun(token.raw ?? token.text ?? ""));
        break;

      case "strong":
        runs.push(new TextRun({ text: token.text, bold: true }));
        break;

      case "em":
        runs.push(new TextRun({ text: token.text, italics: true }));
        break;

      case "codespan":
        runs.push(
          new TextRun({
            text: token.text,
            font: options.codeFont,
            shading: { fill: "F0F0F0" },
          }),
        );
        break;

      case "link":
        runs.push(
          new TextRun({
            text: token.text,
            color: "0066CC",
            underline: {},
          }),
        );
        break;

      case "br":
        runs.push(new TextRun({ text: "", break: 1 }));
        break;

      case "image": {
        const imageRun = await createImageRunFromToken(token, options);
        if (imageRun) {
          runs.push(imageRun);
        }
        break;
      }

      default:
        runs.push(new TextRun(token.raw ?? ""));
    }
  }

  return runs;
}

async function createImageRunFromToken(
  token: MarkdownToken,
  options: Pick<DocxOptions, "resolveAssetUrl">,
): Promise<ImageRun | null> {
  const rawHref = String(token?.href ?? token?.text ?? "").trim();
  if (!rawHref) return null;

  const resolvedUrl = await resolveAssetReference(
    rawHref,
    options.resolveAssetUrl,
  );
  if (!resolvedUrl || !isRenderableImageUrl(resolvedUrl)) {
    return null;
  }

  try {
    const response = await fetch(resolvedUrl);
    if (!response.ok) {
      return null;
    }
    const blob = await response.blob();
    const imageType = getDocxImageType(blob.type, resolvedUrl);
    if (!imageType) {
      return null;
    }
    const bytes = new Uint8Array(await blob.arrayBuffer());
    const { width, height } = await getImageDimensions(blob);
    const maxWidth = 560;
    const scale = width > maxWidth ? maxWidth / width : 1;
    return new ImageRun({
      data: bytes,
      type: imageType,
      transformation: {
        width: Math.max(1, Math.round(width * scale)),
        height: Math.max(1, Math.round(height * scale)),
      },
    });
  } catch {
    return null;
  }
}

async function getImageDimensions(
  blob: Blob,
): Promise<{ width: number; height: number }> {
  return await new Promise((resolve) => {
    const url = URL.createObjectURL(blob);
    const img = new Image();
    img.onload = () => {
      const width = img.naturalWidth || 1;
      const height = img.naturalHeight || 1;
      URL.revokeObjectURL(url);
      resolve({ width, height });
    };
    img.onerror = () => {
      URL.revokeObjectURL(url);
      resolve({ width: 600, height: 400 });
    };
    img.src = url;
  });
}

async function rewriteMarkdownImageSources(
  markdown: string,
  resolveAssetUrl?: (rawPath: string) => string | null | Promise<string | null>,
): Promise<string> {
  if (!resolveAssetUrl) {
    return markdown;
  }

  let rewritten = markdown;
  const markdownMatches = [...rewritten.matchAll(/!\[([^\]]*)\]\(([^)]+)\)/g)];
  for (const match of markdownMatches) {
    const alt = match[1] ?? "";
    const rawTarget = match[2]?.trim() ?? "";
    const resolved = await resolveAssetReference(rawTarget, resolveAssetUrl);
    if (!resolved || resolved === rawTarget) continue;
    rewritten = rewritten.replace(match[0], `![${alt}](${resolved})`);
  }

  const htmlMatches = [
    ...rewritten.matchAll(/(<img\b[^>]*\bsrc\s*=\s*)(["'])([^"']+)\2/gi),
  ];
  for (const match of htmlMatches) {
    const rawTarget = match[3]?.trim() ?? "";
    const resolved = await resolveAssetReference(rawTarget, resolveAssetUrl);
    if (!resolved || resolved === rawTarget) continue;
    rewritten = rewritten.replace(
      match[0],
      `${match[1]}${match[2]}${resolved}${match[2]}`,
    );
  }

  return rewritten;
}

async function resolveAssetReference(
  rawPath: string,
  resolveAssetUrl?: (rawPath: string) => string | null | Promise<string | null>,
): Promise<string | null> {
  const normalized = normalizeReference(rawPath);
  if (!normalized) return null;
  if (isExternalReference(normalized)) return normalized;
  if (!resolveAssetUrl) return normalized;
  return (await resolveAssetUrl(normalized)) ?? normalized;
}

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("//") ||
    ref.startsWith("data:") ||
    ref.startsWith("blob:") ||
    /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(ref)
  );
}

function isRenderableImageUrl(url: string): boolean {
  if (url.startsWith("data:image/")) return true;
  if (/\.(png|jpe?g|gif|webp|bmp|ico|avif|tiff?)([?#].*)?$/i.test(url))
    return true;
  if (/^https?:\/\//i.test(url)) return true;
  if (url.startsWith("/")) return true;
  return false;
}

function getDocxImageType(
  mimeType: string,
  src: string,
): "png" | "jpg" | "gif" | "bmp" {
  const mime = mimeType.toLowerCase();
  if (mime.includes("png")) return "png";
  if (mime.includes("jpeg") || mime.includes("jpg")) return "jpg";
  if (mime.includes("gif")) return "gif";
  if (mime.includes("bmp")) return "bmp";

  const lower = src.toLowerCase();
  if (lower.includes(".png")) return "png";
  if (lower.includes(".jpg") || lower.includes(".jpeg")) return "jpg";
  if (lower.includes(".gif")) return "gif";
  if (lower.includes(".bmp")) return "bmp";

  return "png";
}

/**
 * 获取标题级别
 */
function getHeadingLevel(
  depth: number,
): (typeof HeadingLevel)[keyof typeof HeadingLevel] | undefined {
  const levels = [
    HeadingLevel.HEADING_1,
    HeadingLevel.HEADING_2,
    HeadingLevel.HEADING_3,
    HeadingLevel.HEADING_4,
    HeadingLevel.HEADING_5,
    HeadingLevel.HEADING_6,
  ];
  return levels[depth - 1];
}

/**
 * 规范化文件名
 */
function normalizeFilename(filename: string, extension: string): string {
  return filename.replace(/\.md$/i, "") + extension;
}

/**
 * 触发 Blob 下载
 */
function downloadBlob(blob: Blob, filename: string): void {
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(url);
}
