import archiver from "archiver";
import { inflateRawSync } from "node:zlib";

import type { MailboxContactInput } from "@/lib/official-mail";

const MAX_CONTACT_ROWS = 5_000;

export type ContactExportField = "companyName" | "displayName" | "email" | "groupName" | "memo" | "phone";

const FIELD_DEFINITIONS: Array<{ field: ContactExportField; label: string }> = [
  { field: "displayName", label: "이름" },
  { field: "email", label: "이메일" },
  { field: "phone", label: "전화번호" },
  { field: "companyName", label: "회사·소속" },
  { field: "groupName", label: "그룹" },
  { field: "memo", label: "메모" }
];

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

function escapeXml(value: string) {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

function parseDelimited(text: string, delimiter: string) {
  const rows: string[][] = [];
  let row: string[] = [];
  let value = "";
  let quoted = false;
  for (let index = 0; index < text.length; index += 1) {
    const character = text[index];
    if (character === '"') {
      if (quoted && text[index + 1] === '"') {
        value += '"';
        index += 1;
      } else {
        quoted = !quoted;
      }
      continue;
    }
    if (!quoted && character === delimiter) {
      row.push(value.trim());
      value = "";
      continue;
    }
    if (!quoted && (character === "\n" || character === "\r")) {
      if (character === "\r" && text[index + 1] === "\n") index += 1;
      row.push(value.trim());
      value = "";
      if (row.some(Boolean)) rows.push(row);
      row = [];
      continue;
    }
    value += character;
  }
  row.push(value.trim());
  if (row.some(Boolean)) rows.push(row);
  return rows;
}

function normalizeHeader(value: string) {
  return value.trim().toLowerCase().replace(/[\s_·.-]+/g, "");
}

function rowsToContacts(rows: string[][]): MailboxContactInput[] {
  if (rows.length === 0) return [];
  const aliases: Record<ContactExportField, string[]> = {
    companyName: ["회사", "회사소속", "소속", "company", "organization"],
    displayName: ["이름", "성명", "name", "displayname"],
    email: ["이메일", "메일", "email", "emailaddress"],
    groupName: ["그룹", "그룹명", "group", "groupname"],
    memo: ["메모", "비고", "memo", "note", "notes"],
    phone: ["전화번호", "연락처", "휴대폰", "phone", "mobile", "tel"]
  };
  const normalizedHeaders = rows[0].map(normalizeHeader);
  const positions = Object.fromEntries(
    FIELD_DEFINITIONS.map(({ field }) => [
      field,
      normalizedHeaders.findIndex((header) => aliases[field].includes(header))
    ])
  ) as Record<ContactExportField, number>;
  const hasHeader = positions.email >= 0 || positions.displayName >= 0 || positions.phone >= 0;
  if (!hasHeader) {
    positions.displayName = 0;
    positions.email = 1;
    positions.phone = 2;
    positions.companyName = 3;
    positions.groupName = 4;
    positions.memo = 5;
  }
  return rows
    .slice(hasHeader ? 1 : 0)
    .map((row) => ({
      companyName: positions.companyName >= 0 ? row[positions.companyName] ?? "" : "",
      displayName: positions.displayName >= 0 ? row[positions.displayName] ?? "" : "",
      email: positions.email >= 0 ? row[positions.email] ?? "" : "",
      groupName: positions.groupName >= 0 ? row[positions.groupName] ?? "" : "",
      memo: positions.memo >= 0 ? row[positions.memo] ?? "" : "",
      phone: positions.phone >= 0 ? row[positions.phone] ?? "" : ""
    }))
    .filter((contact) => contact.email.trim())
    .slice(0, MAX_CONTACT_ROWS);
}

function parseVCard(text: string) {
  return text
    .replace(/\r?\n[ \t]/g, "")
    .split(/END:VCARD/i)
    .map((block) => {
      const value = (field: string) => {
        const match = block.match(new RegExp(`(?:^|\\r?\\n)${field}(?:;[^:]*)?:(.*)`, "i"));
        return match?.[1]?.trim() ?? "";
      };
      const name = value("FN") || value("N").split(";").filter(Boolean).reverse().join(" ");
      return {
        companyName: value("ORG").replace(/;/g, " "),
        displayName: name,
        email: value("EMAIL"),
        groupName: value("CATEGORIES").split(",")[0] ?? "",
        memo: value("NOTE").replace(/\\n/g, "\n"),
        phone: value("TEL")
      };
    })
    .filter((contact) => contact.email)
    .slice(0, MAX_CONTACT_ROWS);
}

function extractZipEntries(buffer: Buffer) {
  let endOffset = -1;
  for (let index = buffer.length - 22; index >= Math.max(0, buffer.length - 65_557); index -= 1) {
    if (buffer.readUInt32LE(index) === 0x06054b50) {
      endOffset = index;
      break;
    }
  }
  if (endOffset < 0) throw new Error("contact-file-invalid");
  const entryCount = buffer.readUInt16LE(endOffset + 10);
  if (entryCount > 1_000) throw new Error("contact-file-invalid");
  let offset = buffer.readUInt32LE(endOffset + 16);
  const entries = new Map<string, Buffer>();
  for (let index = 0; index < entryCount; index += 1) {
    if (buffer.readUInt32LE(offset) !== 0x02014b50) throw new Error("contact-file-invalid");
    const method = buffer.readUInt16LE(offset + 10);
    const compressedSize = buffer.readUInt32LE(offset + 20);
    const fileNameLength = buffer.readUInt16LE(offset + 28);
    const extraLength = buffer.readUInt16LE(offset + 30);
    const commentLength = buffer.readUInt16LE(offset + 32);
    const localOffset = buffer.readUInt32LE(offset + 42);
    const fileName = buffer.subarray(offset + 46, offset + 46 + fileNameLength).toString("utf8");
    if (buffer.readUInt32LE(localOffset) !== 0x04034b50) throw new Error("contact-file-invalid");
    const localNameLength = buffer.readUInt16LE(localOffset + 26);
    const localExtraLength = buffer.readUInt16LE(localOffset + 28);
    const dataOffset = localOffset + 30 + localNameLength + localExtraLength;
    if (dataOffset < 0 || dataOffset + compressedSize > buffer.length) throw new Error("contact-file-invalid");
    const compressed = buffer.subarray(dataOffset, dataOffset + compressedSize);
    if (fileName === "xl/sharedStrings.xml" || fileName === "xl/worksheets/sheet1.xml") {
      if (method === 0) entries.set(fileName, compressed);
      if (method === 8) entries.set(fileName, inflateRawSync(compressed, { maxOutputLength: 10 * 1024 * 1024 }));
    }
    offset += 46 + fileNameLength + extraLength + commentLength;
  }
  return entries;
}

function parseXlsx(buffer: Buffer) {
  const entries = extractZipEntries(buffer);
  const sheet = entries.get("xl/worksheets/sheet1.xml")?.toString("utf8");
  if (!sheet) throw new Error("contact-file-invalid");
  const sharedStringsXml = entries.get("xl/sharedStrings.xml")?.toString("utf8") ?? "";
  const sharedStrings = [...sharedStringsXml.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/gi)].map((match) =>
    decodeXml([...match[1].matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/gi)].map((part) => part[1]).join(""))
  );
  const rows = [...sheet.matchAll(/<row\b[^>]*>([\s\S]*?)<\/row>/gi)].map((rowMatch) => {
    const row: string[] = [];
    for (const cellMatch of rowMatch[1].matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>/gi)) {
      const reference = cellMatch[1].match(/\br="([A-Z]+)\d+"/i)?.[1]?.toUpperCase() ?? "A";
      let column = 0;
      for (const character of reference) column = column * 26 + character.charCodeAt(0) - 64;
      const type = cellMatch[1].match(/\bt="([^"]+)"/i)?.[1] ?? "";
      const rawValue = cellMatch[2].match(/<v>([\s\S]*?)<\/v>/i)?.[1]
        ?? cellMatch[2].match(/<t\b[^>]*>([\s\S]*?)<\/t>/i)?.[1]
        ?? "";
      row[column - 1] = type === "s" ? sharedStrings[Number(rawValue)] ?? "" : decodeXml(rawValue);
    }
    return row;
  });
  return rowsToContacts(rows);
}

function parseSpreadsheetXml(text: string) {
  const rows = [...text.matchAll(/<(?:\w+:)?Row\b[^>]*>([\s\S]*?)<\/(?:\w+:)?Row>/gi)].map((rowMatch) =>
    [...rowMatch[1].matchAll(/<(?:\w+:)?Data\b[^>]*>([\s\S]*?)<\/(?:\w+:)?Data>/gi)].map((cell) =>
      decodeXml(cell[1].replace(/<[^>]+>/g, ""))
    )
  );
  return rowsToContacts(rows);
}

export function parseContactFile(fileName: string, buffer: Buffer) {
  const extension = fileName.toLowerCase().split(".").pop() ?? "";
  if (buffer.length > 5 * 1024 * 1024) throw new Error("contact-file-too-large");
  if (extension === "xlsx") return parseXlsx(buffer);
  const text = buffer.toString("utf8").replace(/^\uFEFF/, "");
  if (extension === "vcf" || extension === "vcard") return parseVCard(text);
  if (extension === "xls" && /<Workbook[\s>]/i.test(text)) return parseSpreadsheetXml(text);
  const delimiter = extension === "txt"
    ? (text.split(/\r?\n/, 1)[0]?.includes("\t") ? "\t" : ",")
    : ",";
  return rowsToContacts(parseDelimited(text, delimiter));
}

function csvValue(value: string) {
  return `"${value.replace(/"/g, '""')}"`;
}

function columnName(index: number) {
  let value = index + 1;
  let result = "";
  while (value > 0) {
    result = String.fromCharCode(65 + ((value - 1) % 26)) + result;
    value = Math.floor((value - 1) / 26);
  }
  return result;
}

async function buildXlsx(rows: string[][]) {
  const sheetRows = rows.map((row, rowIndex) =>
    `<row r="${rowIndex + 1}">${row.map((value, columnIndex) => `<c r="${columnName(columnIndex)}${rowIndex + 1}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(value)}</t></is></c>`).join("")}</row>`
  ).join("");
  const archive = archiver("zip", { zlib: { level: 6 } });
  const chunks: Buffer[] = [];
  const completed = new Promise<Buffer>((resolve, reject) => {
    archive.on("data", (chunk: Buffer) => chunks.push(chunk));
    archive.on("end", () => resolve(Buffer.concat(chunks)));
    archive.on("error", reject);
  });
  archive.append(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>`, { name: "[Content_Types].xml" });
  archive.append(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`, { name: "_rels/.rels" });
  archive.append(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="주소록" sheetId="1" r:id="rId1"/></sheets></workbook>`, { name: "xl/workbook.xml" });
  archive.append(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>`, { name: "xl/_rels/workbook.xml.rels" });
  archive.append(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${sheetRows}</sheetData></worksheet>`, { name: "xl/worksheets/sheet1.xml" });
  await archive.finalize();
  return completed;
}

export async function buildContactExport(
  contacts: MailboxContactInput[],
  format: "csv" | "vcard" | "xlsx",
  fieldsInput?: ContactExportField[]
) {
  const allowedFields = new Set(FIELD_DEFINITIONS.map((item) => item.field));
  const fields = (fieldsInput?.length ? fieldsInput : FIELD_DEFINITIONS.map((item) => item.field))
    .filter((field): field is ContactExportField => allowedFields.has(field));
  if (!fields.includes("email")) fields.push("email");
  const definitions = fields.map((field) => FIELD_DEFINITIONS.find((item) => item.field === field)!);
  const rows = [definitions.map((item) => item.label), ...contacts.map((contact) =>
    definitions.map((item) => String(contact[item.field] ?? ""))
  )];
  const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");

  if (format === "vcard") {
    const body = contacts.map((contact) => [
      "BEGIN:VCARD",
      "VERSION:3.0",
      `FN:${String(contact.displayName ?? "").replace(/\r?\n/g, " ")}`,
      `EMAIL:${contact.email}`,
      contact.phone ? `TEL:${contact.phone}` : "",
      contact.companyName ? `ORG:${contact.companyName}` : "",
      contact.groupName ? `CATEGORIES:${contact.groupName}` : "",
      contact.memo ? `NOTE:${contact.memo.replace(/\r?\n/g, "\\n")}` : "",
      "END:VCARD"
    ].filter(Boolean).join("\r\n")).join("\r\n");
    return { body: Buffer.from(body, "utf8"), contentType: "text/vcard; charset=utf-8", fileName: `officialmail-contacts-${date}.vcf` };
  }
  if (format === "xlsx") {
    return {
      body: await buildXlsx(rows),
      contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      fileName: `officialmail-contacts-${date}.xlsx`
    };
  }
  const body = `\uFEFF${rows.map((row) => row.map(csvValue).join(",")).join("\r\n")}`;
  return { body: Buffer.from(body, "utf8"), contentType: "text/csv; charset=utf-8", fileName: `officialmail-contacts-${date}.csv` };
}
