const crypto = require('node:crypto');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { spawn } = require('node:child_process');
const { pathToFileURL } = require('node:url');
const JSZip = require('jszip');

const TEMPLATE_DIR = path.resolve(process.env.TEMPLATE_DIR || path.join(process.cwd(), 'templates'));
const FONTS_DIR = path.resolve(process.env.FONTS_DIR || path.join(process.cwd(), 'fonts'));
const TEMP_DIR = path.resolve(process.env.TEMP_DIR || path.join(os.tmpdir(), 'rental-contracts'));
const SOFFICE_BIN = process.env.SOFFICE_BIN || (
  process.platform === 'win32'
    ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
    : 'soffice'
);
const SUPPORTED_TEMPLATE_EXTENSIONS = new Set(['.doc', '.docx']);

const PLACEHOLDERS = [
  ['{{每月租金}}', 'monthlyRent', '每月租金', true],
  ['{{繳款日期}}', 'paymentDay', '繳款日期', true],
  ['{{保證金}}', 'deposit', '保證金', true],
  ['{{清潔費}}', 'cleaningFee', '清潔費', true],
  ['{{租賃開始年}}', 'leaseStartYear', '租賃開始年', false],
  ['{{租賃開始月}}', 'leaseStartMonth', '租賃開始月', false],
  ['{{租賃開始日}}', 'leaseStartDay', '租賃開始日', false],
  ['{{租賃結束年}}', 'leaseEndYear', '租賃結束年', false],
  ['{{租賃結束月}}', 'leaseEndMonth', '租賃結束月', false],
  ['{{租賃結束日}}', 'leaseEndDay', '租賃結束日', false],
  ['{{租期年數}}', 'leaseDurationYears', '租期年數', false],
];

async function listTemplates() {
  await fs.mkdir(TEMPLATE_DIR, { recursive: true });
  const entries = await fs.readdir(TEMPLATE_DIR, { withFileTypes: true });

  return entries
    .filter((entry) => entry.isFile())
    .filter((entry) => SUPPORTED_TEMPLATE_EXTENSIONS.has(path.extname(entry.name).toLowerCase()))
    .map((entry) => entry.name)
    .sort((a, b) => a.localeCompare(b, 'zh-Hant'));
}

async function createContractPdf(input) {
  const values = normalizeContractInput(input);
  const templatePath = await resolveTemplatePath(input.template);
  const workDir = await createWorkDir();

  try {
    const extension = path.extname(templatePath);
    const copiedDocPath = path.join(workDir, `contract-${crypto.randomUUID()}${extension}`);

    await fs.copyFile(templatePath, copiedDocPath);
    const ext = path.extname(copiedDocPath).toLowerCase();
    const replacementCounts = ext === '.docx'
      ? await applyPlaceholdersToDocx(copiedDocPath, values)
      : await applyPlaceholdersToDoc(copiedDocPath, values);
    assertAllPlaceholdersWereFound(replacementCounts);

    const pdfPath = await convertToPdf(copiedDocPath, workDir);
    const buffer = await fs.readFile(pdfPath);

    return {
      buffer,
      fileName: buildPdfFileName(templatePath),
    };
  } finally {
    await removeWorkDir(workDir);
  }
}

function normalizeContractInput(input) {
  const start = normalizeLeaseDate(input.leaseStart, '租賃開始日');
  const end = normalizeLeaseDate(input.leaseEnd, '租賃結束日');

  if (end.gregorianYear < start.gregorianYear ||
    (end.gregorianYear === start.gregorianYear && end.month < start.month) ||
    (end.gregorianYear === start.gregorianYear && end.month === start.month && end.day <= start.day)) {
    throwClientError('租賃結束日必須晚於開始日。');
  }

  const leaseDurationYears = end.gregorianYear - start.gregorianYear;

  return {
    monthlyRent: normalizePositiveInteger(input.monthlyRent, '每月租金'),
    paymentDay: normalizePaymentDay(input.paymentDay),
    deposit: normalizePositiveInteger(input.deposit, '保證金'),
    cleaningFee: normalizePositiveInteger(input.cleaningFee, '清潔費'),
    leaseStartYear: String(start.rocYear),
    leaseStartMonth: String(start.month),
    leaseStartDay: String(start.day),
    leaseEndYear: String(end.rocYear),
    leaseEndMonth: String(end.month),
    leaseEndDay: String(end.day),
    leaseDurationYears: String(leaseDurationYears),
  };
}

function normalizeLeaseDate(value, label) {
  const text = String(value ?? '').trim();
  if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) {
    throwClientError(`${label}格式不正確。`);
  }
  const [y, m, d] = text.split('-').map(Number);
  if (y < 1912 || m < 1 || m > 12 || d < 1 || d > 31) {
    throwClientError(`${label}日期不正確。`);
  }
  return { gregorianYear: y, rocYear: y - 1911, month: m, day: d };
}

function normalizePositiveInteger(value, label) {
  const text = String(value ?? '').trim();
  if (!/^[1-9]\d*$/.test(text)) {
    throwClientError(`${label}只能輸入正整數。`);
  }
  return text;
}

function normalizePaymentDay(value) {
  const text = String(value ?? '').trim();
  const day = Number(text);
  if (!/^\d+$/.test(text) || day < 1 || day > 31) {
    throwClientError('繳款日期只能輸入 1 到 31。');
  }
  return text;
}

async function resolveTemplatePath(templateName) {
  const name = String(templateName ?? '').trim();
  if (!name) {
    throwClientError('請選擇 Word 範本。');
  }

  if (name !== path.basename(name)) {
    throwClientError('範本檔名不正確。');
  }

  const templatePath = path.resolve(TEMPLATE_DIR, name);
  if (!isInsideDirectory(TEMPLATE_DIR, templatePath)) {
    throwClientError('範本路徑不正確。');
  }

  if (!SUPPORTED_TEMPLATE_EXTENSIONS.has(path.extname(templatePath).toLowerCase())) {
    throwClientError('目前僅支援 .doc / .docx 範本。');
  }

  try {
    await fs.access(templatePath);
  } catch {
    throwClientError('找不到指定的 Word 範本。');
  }

  return templatePath;
}

async function createWorkDir() {
  const workDir = path.resolve(TEMP_DIR, crypto.randomUUID());
  if (!isInsideDirectory(TEMP_DIR, workDir)) {
    throw new Error('Temporary directory resolved outside of TEMP_DIR.');
  }
  await fs.mkdir(workDir, { recursive: true });
  return workDir;
}

async function removeWorkDir(workDir) {
  if (!workDir || !isInsideDirectory(TEMP_DIR, workDir)) {
    return;
  }
  await fs.rm(workDir, { recursive: true, force: true });
}

// Word 會把佔位符拆成多個 run，用這個 pattern 允許相鄰字元之間出現 run 邊界 XML
// [\s\S]*? 改用負向預查避免跨越 <w:t 邊界，防止 backtracking 吃掉多個佔位符
const RUN_BOUNDARY = '(?:</w:t></w:r[^>]*>(?:<w:bookmarkStart[^/]*/?>|<w:bookmarkEnd[^/]*/?>)*<w:r[^>]*>(?:<w:rPr>(?:(?!<w:t)[\\s\\S])*?</w:rPr>)?<w:t[^>]*>)?';

function buildPlaceholderPattern(placeholder) {
  const chars = [...placeholder].map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
  return new RegExp(chars.join(RUN_BOUNDARY), 'g');
}

async function applyPlaceholdersToDocx(docPath, values) {
  const data = await fs.readFile(docPath);
  const zip = await JSZip.loadAsync(data);
  const xmlFiles = ['word/document.xml', 'word/header1.xml', 'word/footer1.xml'];
  const counts = new Map(PLACEHOLDERS.map(([p]) => [p, 0]));

  for (const xmlFile of xmlFiles) {
    const entry = zip.file(xmlFile);
    if (!entry) continue;

    let xml = await entry.async('string');
    for (const [placeholder, key] of PLACEHOLDERS) {
      const pattern = buildPlaceholderPattern(placeholder);
      xml = xml.replace(pattern, () => {
        counts.set(placeholder, counts.get(placeholder) + 1);
        return values[key];
      });
    }
    zip.file(xmlFile, xml);
  }

  const out = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
  await fs.writeFile(docPath, out);
  return counts;
}

async function applyPlaceholdersToDoc(docPath, values) {
  const buffer = await fs.readFile(docPath);
  const counts = new Map();

  for (const [placeholder, key, label] of PLACEHOLDERS) {
    const value = values[key];
    if ([...value].length > [...placeholder].length) {
      throwClientError(`${label}的字數不可超過 ${[...placeholder].length} 個字。`);
    }

    counts.set(placeholder, replaceUtf16Le(buffer, placeholder, value));
  }

  await fs.writeFile(docPath, buffer);
  return counts;
}

function replaceUtf16Le(buffer, placeholder, value) {
  const search = Buffer.from(placeholder, 'utf16le');
  const replacement = Buffer.from(value.padEnd([...placeholder].length, ' '), 'utf16le');
  let count = 0;
  let index = buffer.indexOf(search);

  while (index !== -1) {
    replacement.copy(buffer, index);
    count += 1;
    index = buffer.indexOf(search, index + replacement.length);
  }

  return count;
}

function assertAllPlaceholdersWereFound(replacementCounts) {
  const requiredPlaceholders = new Set(
    PLACEHOLDERS.filter(([, , , required]) => required).map(([p]) => p),
  );

  const missing = [...replacementCounts.entries()]
    .filter(([placeholder, count]) => count === 0 && requiredPlaceholders.has(placeholder))
    .map(([placeholder]) => placeholder);

  if (missing.length > 0) {
    throwClientError(`範本缺少佔位符：${missing.join('、')}`);
  }
}

async function copyFontsToProfile(profileDir) {
  let fontFiles;
  try {
    fontFiles = await fs.readdir(FONTS_DIR);
  } catch {
    return;
  }
  const profileFontsDir = path.join(profileDir, 'user', 'fonts');
  await fs.mkdir(profileFontsDir, { recursive: true });
  await Promise.all(
    fontFiles
      .filter((f) => /\.(ttf|otf|ttc)$/i.test(f))
      .map((f) => fs.copyFile(path.join(FONTS_DIR, f), path.join(profileFontsDir, f))),
  );
}

async function convertToPdf(inputPath, outputDir) {
  const libreOfficeProfileDir = path.join(outputDir, 'lo-profile');
  await fs.mkdir(libreOfficeProfileDir, { recursive: true });
  await copyFontsToProfile(libreOfficeProfileDir);

  const args = [
    `-env:UserInstallation=${pathToFileURL(libreOfficeProfileDir).href}`,
    '--headless',
    '--nologo',
    '--nofirststartwizard',
    '--nolockcheck',
    '--convert-to',
    'pdf',
    '--outdir',
    outputDir,
    inputPath,
  ];

  await runProcess(SOFFICE_BIN, args);

  const expectedPdfPath = path.join(
    outputDir,
    `${path.basename(inputPath, path.extname(inputPath))}.pdf`,
  );

  try {
    await fs.access(expectedPdfPath);
    return expectedPdfPath;
  } catch {
    const files = await fs.readdir(outputDir);
    const pdf = files.find((file) => path.extname(file).toLowerCase() === '.pdf');
    if (pdf) {
      return path.join(outputDir, pdf);
    }
    throw new Error('LibreOffice did not create a PDF file.');
  }
}

function runProcess(command, args) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      windowsHide: true,
      env: process.env,
    });

    let stdout = '';
    let stderr = '';

    child.stdout.on('data', (chunk) => {
      stdout += chunk.toString();
    });

    child.stderr.on('data', (chunk) => {
      stderr += chunk.toString();
    });

    child.on('error', (error) => {
      reject(new Error(`無法執行 LibreOffice (${command})：${error.message}`));
    });

    child.on('close', (code) => {
      if (code === 0) {
        resolve();
        return;
      }

      reject(new Error(`LibreOffice 轉檔失敗：${stderr || stdout || `exit code ${code}`}`));
    });
  });
}

function buildPdfFileName(templatePath) {
  const baseName = path.basename(templatePath, path.extname(templatePath));
  const timestamp = new Date()
    .toISOString()
    .replace(/[-:]/g, '')
    .replace(/\.\d{3}Z$/, '');

  return `${baseName}-${timestamp}.pdf`;
}

function isInsideDirectory(parentDir, targetPath) {
  const relative = path.relative(path.resolve(parentDir), path.resolve(targetPath));
  return Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative);
}

function throwClientError(message) {
  const error = new Error(message);
  error.exposeToClient = true;
  throw error;
}

function buildPreviewValues(input) {
  const values = {};

  if (input.monthlyRent) values.monthlyRent = String(input.monthlyRent);
  if (input.paymentDay) values.paymentDay = String(input.paymentDay);
  if (input.deposit) values.deposit = String(input.deposit);
  if (input.cleaningFee) values.cleaningFee = String(input.cleaningFee);

  if (input.leaseStart && /^\d{4}-\d{2}-\d{2}$/.test(input.leaseStart)) {
    const [y, m, d] = input.leaseStart.split('-').map(Number);
    values.leaseStartYear = String(y - 1911);
    values.leaseStartMonth = String(m);
    values.leaseStartDay = String(d);
  }

  if (input.leaseEnd && /^\d{4}-\d{2}-\d{2}$/.test(input.leaseEnd)) {
    const [y, m, d] = input.leaseEnd.split('-').map(Number);
    values.leaseEndYear = String(y - 1911);
    values.leaseEndMonth = String(m);
    values.leaseEndDay = String(d);
  }

  if (values.leaseStartYear && values.leaseEndYear) {
    values.leaseDurationYears = String(
      Number(input.leaseEnd.slice(0, 4)) - Number(input.leaseStart.slice(0, 4)),
    );
  }

  return values;
}

async function getTemplatePreviewPdf(templateName, input = {}) {
  const values = buildPreviewValues(input);
  const templatePath = await resolveTemplatePath(templateName);
  const workDir = await createWorkDir();

  try {
    const extension = path.extname(templatePath);
    const copiedDocPath = path.join(workDir, `preview${extension}`);
    await fs.copyFile(templatePath, copiedDocPath);

    if (extension.toLowerCase() === '.docx') {
      const data = await fs.readFile(copiedDocPath);
      const zip = await JSZip.loadAsync(data);
      const xmlFiles = ['word/document.xml', 'word/header1.xml', 'word/footer1.xml'];

      for (const xmlFile of xmlFiles) {
        const entry = zip.file(xmlFile);
        if (!entry) continue;
        let xml = await entry.async('string');
        for (const [placeholder, key] of PLACEHOLDERS) {
          if (values[key] === undefined) continue;
          const pattern = buildPlaceholderPattern(placeholder);
          xml = xml.replace(pattern, () => values[key]);
        }
        zip.file(xmlFile, xml);
      }

      const out = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
      await fs.writeFile(copiedDocPath, out);
    }

    const pdfPath = await convertToPdf(copiedDocPath, workDir);
    return await fs.readFile(pdfPath);
  } finally {
    await removeWorkDir(workDir);
  }
}

const DEFAULTS_FILE = path.resolve(TEMPLATE_DIR, 'defaults.json');
const DEFAULT_CLEANING_FEE = '1200';

async function getTemplateDefaults(templateName) {
  try {
    const raw = await fs.readFile(DEFAULTS_FILE, 'utf-8');
    const all = JSON.parse(raw);
    return { cleaningFee: DEFAULT_CLEANING_FEE, ...all[templateName] };
  } catch {
    return { cleaningFee: DEFAULT_CLEANING_FEE };
  }
}

async function saveTemplateDefaults(templateName, values) {
  let all = {};
  try {
    const raw = await fs.readFile(DEFAULTS_FILE, 'utf-8');
    all = JSON.parse(raw);
  } catch {}
  all[templateName] = { ...all[templateName], ...values };
  await fs.mkdir(TEMPLATE_DIR, { recursive: true });
  await fs.writeFile(DEFAULTS_FILE, JSON.stringify(all, null, 2), 'utf-8');
}

async function saveTemplate(originalName, buffer) {
  const name = path.basename(String(originalName));
  if (!name) throwClientError('檔名不正確。');
  const ext = path.extname(name).toLowerCase();
  if (!SUPPORTED_TEMPLATE_EXTENSIONS.has(ext)) throwClientError('僅接受 .doc / .docx 檔案。');
  const dest = path.resolve(TEMPLATE_DIR, name);
  if (!isInsideDirectory(TEMPLATE_DIR, dest)) throwClientError('路徑不正確。');
  await fs.mkdir(TEMPLATE_DIR, { recursive: true });
  await fs.writeFile(dest, buffer);
  return name;
}

async function deleteTemplate(templateName) {
  const templatePath = await resolveTemplatePath(templateName);
  await fs.unlink(templatePath);
}

module.exports = {
  createContractPdf,
  listTemplates,
  getTemplatePreviewPdf,
  saveTemplate,
  deleteTemplate,
  getTemplateDefaults,
  saveTemplateDefaults,
};
