const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const root = path.join(__dirname, '..');
const packageJson = require(path.join(root, 'package.json'));
const buildDir = path.join(root, 'dist-release-final');
const unpackedDir = path.join(buildDir, 'win-unpacked');
const bundleDir = path.join(buildDir, 'resource-bundles');
const manifestPath = path.join(buildDir, 'resource-manifest.json');
const tempDir = path.join(root, 'build', 'oem-batch-temp');

function findIscc() {
  const candidates = [
    process.env.INNO_SETUP_ISCC,
    'C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe',
    'C:\\Program Files\\Inno Setup 6\\ISCC.exe',
    'ISCC.exe',
  ].filter(Boolean);

  for (const candidate of candidates) {
    if (candidate.includes('\\') || candidate.includes('/')) {
      if (fs.existsSync(candidate)) return candidate;
      continue;
    }

    const result = spawnSync(candidate, ['/Qp'], {
      encoding: 'utf8',
      windowsHide: true,
    });
    if (result.status === 0 || result.status === 1) return candidate;
  }

  throw new Error('Inno Setup ISCC.exe not found.');
}

function getInnoInstallDir(isccPath) {
  if (isccPath.includes('\\') || isccPath.includes('/')) {
    return path.dirname(isccPath);
  }

  const candidates = [
    'C:\\Program Files (x86)\\Inno Setup 6',
    'C:\\Program Files\\Inno Setup 6',
  ];
  return candidates.find(candidate => fs.existsSync(path.join(candidate, 'ISCC.exe'))) || '';
}

function getLanguagesSection(isccPath) {
  const innoDir = getInnoInstallDir(isccPath);
  const chineseLanguageFile = innoDir ? path.join(innoDir, 'Languages', 'ChineseSimplified.isl') : '';
  if (chineseLanguageFile && fs.existsSync(chineseLanguageFile)) {
    return '[Languages]\nName: "chinesesimp"; MessagesFile: "compiler:Languages\\\\ChineseSimplified.isl"';
  }
  return '';
}

function escapeInnoString(value) {
  return String(value).replace(/"/g, '""');
}

function readJson(file, fallback) {
  if (!fs.existsSync(file)) return fallback;
  try {
    return JSON.parse(fs.readFileSync(file, 'utf8'));
  } catch {
    return fallback;
  }
}

function findAppExe() {
  if (!fs.existsSync(unpackedDir)) {
    throw new Error(`win-unpacked not found: ${unpackedDir}`);
  }

  const exeFiles = fs.readdirSync(unpackedDir)
    .filter(name => name.toLowerCase().endsWith('.exe'))
    .filter(name => !/^unins/i.test(name));

  if (exeFiles.length === 0) {
    throw new Error(`No app exe found in ${unpackedDir}`);
  }

  exeFiles.sort((a, b) => {
    const aStat = fs.statSync(path.join(unpackedDir, a));
    const bStat = fs.statSync(path.join(unpackedDir, b));
    return bStat.size - aStat.size;
  });

  return exeFiles[0];
}

function getCurrentResourceArchives() {
  const manifest = readJson(manifestPath, { bundles: {} });
  const archives = Object.values(manifest.bundles || {})
    .map(bundle => bundle && bundle.archive)
    .filter(Boolean);

  const archivePaths = Array.from(new Set(archives))
    .map(archive => path.join(bundleDir, archive))
    .sort();
  const missing = archivePaths.filter(archivePath => !fs.existsSync(archivePath));

  if (archivePaths.length === 0) {
    throw new Error(`No resource archives found in manifest: ${manifestPath}`);
  }
  if (missing.length > 0) {
    throw new Error(`Missing resource archives:\n${missing.join('\n')}`);
  }

  return archivePaths;
}

function getCurrentResourceArchiveEntries() {
  const manifest = readJson(manifestPath, { bundles: {} });
  return Object.entries(manifest.bundles || {})
    .map(([name, bundle]) => ({
      name,
      archive: bundle && bundle.archive,
      archivePath: bundle && bundle.archive ? path.join(bundleDir, bundle.archive) : '',
    }))
    .filter(entry => entry.archive && fs.existsSync(entry.archivePath))
    .sort((a, b) => a.name.localeCompare(b.name));
}

function ensureOfflineBundlesHydrated() {
  const scriptPath = path.join(__dirname, 'hydrate-oem-portable-bundles.cjs');
  const result = spawnSync(process.execPath, [scriptPath], {
    cwd: root,
    encoding: 'utf8',
    stdio: 'inherit',
    windowsHide: true,
  });
  if (result.status !== 0) {
    throw new Error(`hydrate-oem-portable-bundles failed with exit code ${result.status}`);
  }
}

function dirSizeBytes(dir) {
  let total = 0;
  const walk = current => {
    for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
      const full = path.join(current, entry.name);
      if (entry.isDirectory()) {
        walk(full);
      } else if (entry.isFile()) {
        total += fs.statSync(full).size;
      }
    }
  };
  walk(dir);
  return total;
}

function main() {
  const appExe = findAppExe();
  ensureOfflineBundlesHydrated();
  const resourceArchives = getCurrentResourceArchives();
  const appName = path.basename(appExe, '.exe');
  const version = process.env.OEM_VERSION || process.env.APP_VERSION || packageJson.version || '1.0.0';
  const outputBaseName = `${appName}-${version}-win-setup-x64`;
  const issPath = path.join(tempDir, 'oem-full-offline-installer.iss');
  const iscc = findIscc();
  const languagesSection = getLanguagesSection(iscc);

  fs.mkdirSync(tempDir, { recursive: true });
  fs.mkdirSync(buildDir, { recursive: true });

  const previousOutput = path.join(buildDir, `${outputBaseName}.exe`);
  if (fs.existsSync(previousOutput)) {
    fs.rmSync(previousOutput, { force: true });
  }

  const iss = `
[Setup]
AppId={{68912956-3e71-5e51-8eba-ab7cdc795c8c}
AppName=${escapeInnoString(appName)}
AppVersion=${escapeInnoString(version)}
AppPublisher=ModStartLib
DefaultDirName={autopf}\\${escapeInnoString(appName)}
DefaultGroupName=${escapeInnoString(appName)}
DisableDirPage=no
DisableProgramGroupPage=yes
UsePreviousAppDir=no
OutputDir=${escapeInnoString(buildDir)}
OutputBaseFilename=${escapeInnoString(outputBaseName)}
SetupIconFile=${escapeInnoString(path.join(root, 'electron', 'resources', 'build', 'logo.ico'))}
UninstallDisplayIcon={app}\\${escapeInnoString(appExe)}
Compression=lzma2/max
SolidCompression=no
LZMAUseSeparateProcess=yes
LZMADictionarySize=65536
DiskSpanning=no
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
WizardStyle=modern
PrivilegesRequired=lowest
CloseApplications=yes
RestartApplications=no

${languagesSection}

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"

[Files]
Source: "${escapeInnoString(path.join(unpackedDir, '*'))}"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "${escapeInnoString(manifestPath)}"; DestDir: "{app}\\resources"; Flags: ignoreversion
[Icons]
Name: "{group}\\${escapeInnoString(appName)}"; Filename: "{app}\\${escapeInnoString(appExe)}"; WorkingDir: "{app}"
Name: "{autodesktop}\\${escapeInnoString(appName)}"; Filename: "{app}\\${escapeInnoString(appExe)}"; WorkingDir: "{app}"; Tasks: desktopicon

[Run]
Filename: "{app}\\${escapeInnoString(appExe)}"; Description: "{cm:LaunchProgram,${escapeInnoString(appName)}}"; Flags: nowait postinstall skipifsilent
`.trimStart();

  fs.writeFileSync(issPath, iss, 'utf8');

  const unpackedGb = dirSizeBytes(unpackedDir) / 1024 / 1024 / 1024;
  const archivesMb = resourceArchives.reduce((total, archivePath) => total + fs.statSync(archivePath).size, 0) / 1024 / 1024;
  console.log(`[inno] source: ${unpackedDir} (${unpackedGb.toFixed(2)} GB)`);
  console.log(`[inno] validated resource archives: ${resourceArchives.length} (${archivesMb.toFixed(2)} MB)`);
  console.log('[inno] resource payload mode: full offline resources copied from win-unpacked');
  console.log(`[inno] output: ${previousOutput}`);

  const result = spawnSync(iscc, [issPath], {
    cwd: root,
    encoding: 'utf8',
    stdio: 'inherit',
    windowsHide: true,
  });

  if (result.status !== 0) {
    throw new Error(`ISCC failed with exit code ${result.status}`);
  }

  const stat = fs.statSync(previousOutput);
  console.log(`[inno] built: ${previousOutput} (${(stat.size / 1024 / 1024).toFixed(2)} MB)`);
}

main();
