import { strFromU8, unzipSync, zipSync } from "fflate";

export const PARTNER_BATCH_UPLOAD_MAX_BYTES = 80_000_000;

type WorkbookCell = {
  reference: string;
  row: number;
  column: number;
  attributes: string;
  body: string;
  value: string;
};

type ScreenshotColumns = {
  headerRow: number;
  columns: Set<number>;
};

function decodeXml(value: string) {
  return value
    .replace(/<[^>]+>/g, "")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&amp;/g, "&")
    .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
    .replace(/&#x([0-9a-f]+);/gi, (_, code) =>
      String.fromCodePoint(Number.parseInt(code, 16)),
    );
}

function textNodes(xml: string) {
  return [...xml.matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/g)]
    .map((match) => decodeXml(match[1]))
    .join("");
}

function columnIndex(reference: string) {
  const letters = reference.match(/^[A-Z]+/i)?.[0]?.toUpperCase() ?? "";
  let result = 0;
  for (const letter of letters) result = result * 26 + letter.charCodeAt(0) - 64;
  return Math.max(0, result - 1);
}

function normalizeHeader(value: string) {
  return value.replace(/[\s_\-（）()]/g, "").toLocaleLowerCase("zh-CN");
}

function parseCells(worksheetXml: string, sharedStrings: string[]) {
  const cells: WorkbookCell[] = [];
  for (const match of worksheetXml.matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>/g)) {
    const attributes = match[1];
    const body = match[2];
    const reference = attributes.match(/\br="([A-Z]+\d+)"/i)?.[1] ?? "";
    if (!reference) continue;
    const type = attributes.match(/\bt="([^"]+)"/)?.[1] ?? "";
    const rawValue = body.match(/<v>([\s\S]*?)<\/v>/)?.[1] ?? "";
    const value =
      type === "s"
        ? sharedStrings[Number(rawValue)] ?? ""
        : type === "inlineStr"
          ? textNodes(body)
          : decodeXml(rawValue);
    cells.push({
      reference,
      row: Number(reference.match(/\d+$/)?.[0] ?? 0),
      column: columnIndex(reference),
      attributes,
      body,
      value: value.trim(),
    });
  }
  return cells;
}

function findScreenshotColumns(cells: WorkbookCell[]): ScreenshotColumns {
  for (let row = 1; row <= 8; row += 1) {
    const columns = new Set<number>();
    for (const cell of cells) {
      if (cell.row !== row) continue;
      const header = normalizeHeader(cell.value);
      if (
        header === normalizeHeader("笔记截图") ||
        header === normalizeHeader("发布截图") ||
        header === normalizeHeader("数据分析截图") ||
        header === normalizeHeader("数据分析截图（单篇笔记数据分析截图）") ||
        header === normalizeHeader("创作者中心截图")
      ) {
        columns.add(cell.column);
      }
    }
    if (columns.size >= 2) return { headerRow: row, columns };
  }
  throw new Error("表格结构不正确，请使用本领取页面导出的批量回填表");
}

function relationshipMap(xml: string) {
  const relationships = new Map<string, string>();
  for (const match of xml.matchAll(/<Relationship\b([^>]*)\/?\s*>/g)) {
    const id = match[1].match(/\bId="([^"]+)"/)?.[1] ?? "";
    const target = match[1].match(/\bTarget="([^"]+)"/)?.[1] ?? "";
    if (id && target) relationships.set(id, decodeXml(target));
  }
  return relationships;
}

function normalizeZipPath(value: string) {
  const result: string[] = [];
  for (const part of value.split("/")) {
    if (!part || part === ".") continue;
    if (part === "..") result.pop();
    else result.push(part);
  }
  return result.join("/");
}

function resolveZipPath(base: string, target: string) {
  const slash = base.lastIndexOf("/");
  const directory = slash >= 0 ? base.slice(0, slash + 1) : "";
  return normalizeZipPath(`${directory}${target}`);
}

function wpsScreenshotMedia(
  entries: Record<string, Uint8Array>,
  cells: WorkbookCell[],
  screenshotColumns: ScreenshotColumns,
) {
  const result = new Set<string>();
  let expected = 0;
  const screenshotIds = new Set<string>();
  for (const cell of cells) {
    if (
      cell.row <= screenshotColumns.headerRow ||
      !screenshotColumns.columns.has(cell.column)
    ) {
      continue;
    }
    const id = decodeXml(cell.body).match(/DISPIMG\("([^"]+)"/i)?.[1];
    if (id) {
      expected += 1;
      screenshotIds.add(id);
    }
  }
  if (screenshotIds.size === 0) return { result, expected, resolved: 0 };

  const cellImagesXml = entries["xl/cellimages.xml"]
    ? strFromU8(entries["xl/cellimages.xml"])
    : "";
  const relationships = relationshipMap(
    entries["xl/_rels/cellimages.xml.rels"]
      ? strFromU8(entries["xl/_rels/cellimages.xml.rels"])
      : "",
  );
  let resolved = 0;
  for (const match of cellImagesXml.matchAll(
    /<(?:etc:)?cellImage\b[^>]*>([\s\S]*?)<\/(?:etc:)?cellImage>/g,
  )) {
    const id = match[1].match(/<(?:xdr:)?cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
    const relationshipId = match[1].match(
      /<(?:a:)?blip\b[^>]*\br:embed="([^"]+)"/,
    )?.[1];
    if (!id || !relationshipId || !screenshotIds.has(id)) continue;
    const target = relationships.get(relationshipId);
    if (!target) continue;
    result.add(resolveZipPath("xl/cellimages.xml", target));
    resolved += 1;
  }
  return { result, expected, resolved };
}

function drawingScreenshotMedia(
  entries: Record<string, Uint8Array>,
  screenshotColumns: ScreenshotColumns,
) {
  const result = new Set<string>();
  let expected = 0;
  let resolved = 0;
  const worksheetXml = entries["xl/worksheets/sheet1.xml"]
    ? strFromU8(entries["xl/worksheets/sheet1.xml"])
    : "";
  const sheetRelationships = relationshipMap(
    entries["xl/worksheets/_rels/sheet1.xml.rels"]
      ? strFromU8(entries["xl/worksheets/_rels/sheet1.xml.rels"])
      : "",
  );
  const drawingId = worksheetXml.match(/<drawing\b[^>]*r:id="([^"]+)"/)?.[1];
  const drawingTarget = drawingId ? sheetRelationships.get(drawingId) : undefined;
  if (!drawingTarget) return { result, expected, resolved };
  const drawingPath = resolveZipPath("xl/worksheets/sheet1.xml", drawingTarget);
  const drawingXml = entries[drawingPath] ? strFromU8(entries[drawingPath]) : "";
  const relationshipPath = `${drawingPath.slice(0, drawingPath.lastIndexOf("/") + 1)}_rels/${drawingPath.slice(drawingPath.lastIndexOf("/") + 1)}.rels`;
  const drawingRelationships = relationshipMap(
    entries[relationshipPath] ? strFromU8(entries[relationshipPath]) : "",
  );
  for (const anchor of drawingXml.matchAll(
    /<xdr:(?:oneCellAnchor|twoCellAnchor)\b[^>]*>[\s\S]*?<xdr:from>([\s\S]*?)<\/xdr:from>[\s\S]*?<a:blip\b[^>]*r:embed="([^"]+)"[\s\S]*?<\/xdr:(?:oneCellAnchor|twoCellAnchor)>/g,
  )) {
    const column = Number(anchor[1].match(/<xdr:col>(\d+)<\/xdr:col>/)?.[1]);
    const zeroBasedRow = Number(anchor[1].match(/<xdr:row>(\d+)<\/xdr:row>/)?.[1]);
    if (
      !Number.isInteger(column) ||
      !Number.isInteger(zeroBasedRow) ||
      zeroBasedRow + 1 <= screenshotColumns.headerRow ||
      !screenshotColumns.columns.has(column)
    ) {
      continue;
    }
    expected += 1;
    const target = drawingRelationships.get(anchor[2]);
    if (!target) continue;
    result.add(resolveZipPath(drawingPath, target));
    resolved += 1;
  }
  return { result, expected, resolved };
}

function richValueScreenshotMedia(
  entries: Record<string, Uint8Array>,
  cells: WorkbookCell[],
  screenshotColumns: ScreenshotColumns,
) {
  const result = new Set<string>();
  let expected = 0;
  let resolved = 0;
  const metadataXml = entries["xl/metadata.xml"]
    ? strFromU8(entries["xl/metadata.xml"])
    : "";
  const richValueXml = entries["xl/richData/rdrichvalue.xml"]
    ? strFromU8(entries["xl/richData/rdrichvalue.xml"])
    : "";
  const richValueRelXml = entries["xl/richData/richValueRel.xml"]
    ? strFromU8(entries["xl/richData/richValueRel.xml"])
    : "";
  const relationships = relationshipMap(
    entries["xl/richData/_rels/richValueRel.xml.rels"]
      ? strFromU8(entries["xl/richData/_rels/richValueRel.xml.rels"])
      : "",
  );
  if (!metadataXml || !richValueXml || !richValueRelXml || !relationships.size) {
    return { result, expected, resolved };
  }
  const valueMetadataXml =
    metadataXml.match(/<valueMetadata\b[^>]*>([\s\S]*?)<\/valueMetadata>/)?.[1] ??
    "";
  const metadataToRichValue = [
    ...valueMetadataXml.matchAll(
      /<bk\b[^>]*>[\s\S]*?<rc\b[^>]*\bv="(\d+)"[^>]*\/>[\s\S]*?<\/bk>/g,
    ),
  ].map((match) => Number(match[1]));
  const richValueToRelationship = [
    ...richValueXml.matchAll(/<rv\b[^>]*>([\s\S]*?)<\/rv>/g),
  ].map((match) => Number(match[1].match(/<v>(\d+)<\/v>/)?.[1] ?? -1));
  const relationshipIds = [
    ...richValueRelXml.matchAll(/<rel\b[^>]*\br:id="([^"]+)"[^>]*\/>/g),
  ].map((match) => match[1]);

  for (const cell of cells) {
    if (
      cell.row <= screenshotColumns.headerRow ||
      !screenshotColumns.columns.has(cell.column)
    ) {
      continue;
    }
    const metadataIndex = Number(cell.attributes.match(/\bvm="(\d+)"/)?.[1] ?? 0);
    if (!metadataIndex) continue;
    expected += 1;
    const richValueIndex = metadataToRichValue[metadataIndex - 1];
    const relationshipIndex = richValueToRelationship[richValueIndex];
    const relationshipId = relationshipIds[relationshipIndex];
    const target = relationships.get(relationshipId);
    if (!target) continue;
    result.add(resolveZipPath("xl/richData/richValueRel.xml", target));
    resolved += 1;
  }
  return { result, expected, resolved };
}

export type CompactedPartnerBatchWorkbook = {
  bytes: Uint8Array;
  removedMediaCount: number;
  preservedScreenshotCount: number;
};

/**
 * Oversized exports are usually caused by full-resolution source images. The
 * upload only needs the two screenshot columns, so retain those image entries
 * and omit source media from the temporary upload copy.
 */
export function compactPartnerBatchWorkbookForUpload(
  input: Uint8Array,
): CompactedPartnerBatchWorkbook {
  const isMediaFile = (name: string) =>
    name.startsWith("xl/media/") && !name.endsWith("/");
  const structure = unzipSync(input, {
    filter: (file) => !isMediaFile(file.name),
  });
  const worksheetXml = structure["xl/worksheets/sheet1.xml"]
    ? strFromU8(structure["xl/worksheets/sheet1.xml"])
    : "";
  if (!worksheetXml) throw new Error("Excel 中没有可读取的批量回填工作表");
  const sharedXml = structure["xl/sharedStrings.xml"]
    ? strFromU8(structure["xl/sharedStrings.xml"])
    : "";
  const sharedStrings = [
    ...sharedXml.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/g),
  ].map((match) => textNodes(match[1]));
  const cells = parseCells(worksheetXml, sharedStrings);
  const screenshotColumns = findScreenshotColumns(cells);
  const formats = [
    wpsScreenshotMedia(structure, cells, screenshotColumns),
    drawingScreenshotMedia(structure, screenshotColumns),
    richValueScreenshotMedia(structure, cells, screenshotColumns),
  ];
  const screenshotMedia = new Set<string>();
  let expectedScreenshotCount = 0;
  let resolvedScreenshotCount = 0;
  for (const format of formats) {
    expectedScreenshotCount += format.expected;
    resolvedScreenshotCount += format.resolved;
    for (const name of format.result) screenshotMedia.add(name);
  }
  if (resolvedScreenshotCount < expectedScreenshotCount) {
    throw new Error("表格中的回填截图无法完整识别，请重新导出最新版回填表");
  }

  let mediaCount = 0;
  const entries = unzipSync(input, {
    filter: (file) => {
      if (!isMediaFile(file.name)) return true;
      mediaCount += 1;
      return screenshotMedia.has(file.name);
    },
  });
  const bytes = zipSync(entries, { level: 6 });
  return {
    bytes,
    removedMediaCount: Math.max(0, mediaCount - screenshotMedia.size),
    preservedScreenshotCount: screenshotMedia.size,
  };
}
