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

const projectRoot = process.cwd();
const jobsFile = path.resolve(projectRoot, process.argv[2]);
const sessionDir = path.resolve(projectRoot, process.argv[3]);
const outputDir = path.join(projectRoot, 'dist-release-final');
const buildTempDir = path.join(projectRoot, 'build', 'oem-batch-temp');
const logFile = path.join(sessionDir, 'worker.log');
const resultFile = path.join(sessionDir, 'result.json');
const buildOutputFile = path.join(sessionDir, 'build-output.log');
const stageStatusFile = path.join(sessionDir, 'stage-status.json');

let resultWritten = false;
let lastProgressLogAt = 0;
const HEARTBEAT_INTERVAL_MS = 30000;
const liveChildren = new Map();

function ensureSessionDir() {
  fs.mkdirSync(sessionDir, { recursive: true });
  fs.mkdirSync(buildTempDir, { recursive: true });
}

function formatError(error) {
  if (!error) return 'Unknown error';
  if (error.stack) return error.stack;
  if (error.message) return error.message;
  return String(error);
}

function writeLog(payload) {
  ensureSessionDir();
  fs.appendFileSync(logFile, JSON.stringify({
    time: new Date().toISOString(),
    ...payload,
  }) + '\n', 'utf8');
}

function appendBuildOutput(chunk) {
  ensureSessionDir();
  fs.appendFileSync(buildOutputFile, chunk, 'utf8');
}

function summarizeOutput(text) {
  const lines = String(text || '')
    .split(/\r?\n/)
    .map(line => line.trim())
    .filter(Boolean);
  if (lines.length === 0) return '';
  const lastLine = lines[lines.length - 1];
  return lastLine.length > 700 ? `${lastLine.slice(0, 700)}...` : lastLine;
}

function formatElapsed(ms) {
  const totalSeconds = Math.max(0, Math.floor(ms / 1000));
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
}

function stripAnsi(text) {
  return String(text || '').replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '');
}

function shouldMirrorOutput(text) {
  const value = stripAnsi(text);
  if (!value.trim()) return false;
  if (/^\s*(Compressing|Processing):\s+/m.test(value)) return false;
  if (/^\s*dist[\\/]/m.test(value)) return false;
  if (/^\s*dist-electron[\\/]/m.test(value)) return false;
  if (!/(error|failed|exception|eperm|eacces|ebusy|enoent|enotempty|cannot|unsafe)/i.test(value)) {
    if (value.includes('\\dist-release-final\\win-unpacked\\resources\\')) return false;
    if (value.includes('/dist-release-final/win-unpacked/resources/')) return false;
  }
  if (value.includes('[plugin:vite:reporter]')) return false;
  if (value.includes('dynamic import will not move module into another chunk')) return false;
  return true;
}

function writeCommandOutputLog(job, level, text) {
  appendBuildOutput(text);
  if (shouldMirrorOutput(text)) {
    writeLog({ jobId: job.id, level, message: text });
    return;
  }

  const now = Date.now();
  if (now - lastProgressLogAt < 5000) return;
  lastProgressLogAt = now;
  const summary = summarizeOutput(text);
  if (summary) {
    writeLog({ jobId: job.id, level: 'info', message: `[${level}] ${summary}` });
  }
}

function writeResult(payload) {
  ensureSessionDir();
  fs.writeFileSync(resultFile, JSON.stringify(payload, null, 2), 'utf8');
  resultWritten = true;
}

function writeStageStatus(payload) {
  ensureSessionDir();
  fs.writeFileSync(stageStatusFile, JSON.stringify({
    time: new Date().toISOString(),
    ...payload,
  }, null, 2), 'utf8');
}

function sanitizeIdentifier(value, fallback) {
  const normalized = String(value || '')
    .trim()
    .replace(/\s+/g, '-')
    .replace(/[^\w.-]/g, '');
  return normalized || fallback;
}

function jsString(value) {
  return JSON.stringify(String(value || ''));
}

function normalizeApiBaseUrl(value) {
  const raw = String(value || '').trim();
  if (!raw) return '';
  const normalized = raw
    .replace(/\/+$/, '')
    .replace(/\/+api$/i, '/api');
  return /\/api$/i.test(normalized) ? normalized : `${normalized}/api`;
}

function validateJob(job) {
  if (!job.brandName || !job.brandName.trim()) {
    throw new Error('Software name is required.');
  }
  job.apiBaseUrl = normalizeApiBaseUrl(job.apiBaseUrl);
  if (!job.apiBaseUrl) {
    throw new Error('API base URL is required.');
  }
  try {
    new URL(job.apiBaseUrl);
  } catch {
    throw new Error(`Invalid API base URL: ${job.apiBaseUrl}`);
  }
  if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(job.version || '')) {
    throw new Error(`Invalid version: ${job.version || '(empty)'}`);
  }

  const assets = job.assets || {};
  const required = [
    ['qrcodePath', 'QR code'],
    ['logoPath', 'Logo'],
    ['iconPath', 'Icon'],
    ['loginBgPath', 'Login background'],
  ];
  for (const [key, label] of required) {
    if (!assets[key] || !fs.existsSync(assets[key])) {
      throw new Error(`${label} file does not exist: ${assets[key] || '(empty)'}`);
    }
  }
}

function repairKnownMojibakePath(filePath) {
  return String(filePath || '')
    .replace(/cursor澶囦唤/g, 'cursor备份')
    .replace(/cursor锟斤拷锟斤拷/g, 'cursor备份');
}

function repairJobPaths(job) {
  if (!job.assets) return;
  for (const key of ['qrcodePath', 'logoPath', 'iconPath', 'loginBgPath']) {
    job.assets[key] = repairKnownMojibakePath(job.assets[key]);
  }
}

function copyAssets(job) {
  const assetsDir = path.join(projectRoot, 'oem-assets');
  fs.mkdirSync(assetsDir, { recursive: true });
  fs.copyFileSync(job.assets.qrcodePath, path.join(assetsDir, 'qrcode.png'));
  fs.copyFileSync(job.assets.logoPath, path.join(assetsDir, 'logo.png'));
  fs.copyFileSync(job.assets.iconPath, path.join(assetsDir, 'icon.png'));
  fs.copyFileSync(job.assets.loginBgPath, path.join(assetsDir, 'login-bg.png'));
}

function writeOemConfig(job) {
  const shortName = sanitizeIdentifier(job.brandShortName || job.brandName, 'OEMApp');
  const displayName = job.brandDisplayName && job.brandDisplayName.trim()
    ? job.brandDisplayName.trim()
    : job.brandName.trim();
  const slogan = job.brandSlogan && job.brandSlogan.trim() ? job.brandSlogan.trim() : '';

  const content = `module.exports = {
    appVersion: ${jsString(job.version)},

    brandName: ${jsString(job.brandName)},
    brandSlogan: ${jsString(slogan)},
    brandShortName: ${jsString(shortName)},
    brandDisplayName: ${jsString(displayName)},

    qrcodePath: "./oem-assets/qrcode.png",
    logoPath: "./oem-assets/logo.png",
    iconPath: "./oem-assets/icon.png",
    loginBgPath: "./oem-assets/login-bg.png",

    apiBaseUrl: ${jsString(job.apiBaseUrl)},

    website: "",
    websiteGithub: "",
    websiteGitee: "",
};
`;
  fs.writeFileSync(path.join(projectRoot, 'oem-config.cjs'), content, 'utf8');
}

function listArtifacts(afterMs) {
  if (!fs.existsSync(outputDir)) return [];
  const releaseAssetsDir = path.join(outputDir, 'release-assets');
  const searchRoot = fs.existsSync(releaseAssetsDir) ? releaseAssetsDir : outputDir;
  const files = [];
  const walk = (dir) => {
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
      const full = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        if (entry.name === 'win-unpacked') continue;
        walk(full);
      } else if (entry.isFile()) {
        files.push(full);
      }
    }
  };
  walk(searchRoot);
  return files
    .filter(filePath => {
      try {
        const stat = fs.statSync(filePath);
        return stat.isFile() && stat.mtimeMs >= afterMs - 3000;
      } catch {
        return false;
      }
    })
    .filter(filePath => /\.(exe|msi|dmg|zip|7z|blockmap|yml)$/i.test(filePath))
    .sort();
}

function resolveBuildCommand(command) {
  const trimmed = String(command || '').trim();
  const npmRunMatch = /^npm(?:\.cmd)?\s+run\s+([^\s]+)$/i.exec(trimmed);
  if (npmRunMatch) {
    return { file: 'cmd.exe', args: ['/d', '/s', '/c', `npm.cmd run ${npmRunMatch[1]}`], shell: false };
  }
  return { file: 'cmd.exe', args: ['/d', '/s', '/c', trimmed], shell: false };
}

function resolveExternalNsisDir() {
  const nsisDir = 'C:\\Program Files (x86)\\NSIS';
  if (fs.existsSync(path.join(nsisDir, 'Bin', 'makensis.exe'))) {
    return nsisDir;
  }
  return process.env.ELECTRON_BUILDER_NSIS_DIR || '';
}

function runCommand(command, job, label = command) {
  return new Promise((resolve, reject) => {
    const startMs = Date.now();
    lastProgressLogAt = 0;
    const resolved = resolveBuildCommand(command);
    writeStageStatus({ jobId: job.id, stage: label, command, status: 'running', startedAt: new Date().toISOString() });
    writeLog({ jobId: job.id, level: 'info', message: `Run build command: ${label}` });
    writeLog({ jobId: job.id, level: 'info', message: `Spawn: ${resolved.file} ${resolved.args.join(' ')}` });

    const child = spawn(resolved.file, resolved.args, {
      cwd: projectRoot,
      env: {
        ...process.env,
        APP_VERSION: job.version,
        OEM_VERSION: job.version,
        OEM_BATCH_BUILD: '1',
        EXTERNAL_ELECTRON_DEV: '1',
        TEMP: buildTempDir,
        TMP: buildTempDir,
        TMPDIR: buildTempDir,
        ELECTRON_BUILDER_NSIS_DIR: resolveExternalNsisDir(),
      },
      windowsHide: true,
      stdio: ['ignore', 'pipe', 'pipe'],
      shell: resolved.shell,
    });
    if (child.pid) liveChildren.set(child.pid, label);

    writeLog({ jobId: job.id, level: 'info', message: `Build process pid=${child.pid || 'unknown'}` });

    const heartbeatTimer = setInterval(() => {
      writeLog({
        jobId: job.id,
        level: 'info',
        message: `Still running: ${label} elapsed ${formatElapsed(Date.now() - startMs)}`,
      });
    }, HEARTBEAT_INTERVAL_MS);
    if (typeof heartbeatTimer.unref === 'function') heartbeatTimer.unref();

    child.stdout.on('data', data => {
      const text = data.toString();
      writeCommandOutputLog(job, 'stdout', text);
    });
    child.stderr.on('data', data => {
      const text = data.toString();
      writeCommandOutputLog(job, 'stderr', text);
    });
    child.on('error', error => {
      clearInterval(heartbeatTimer);
      if (child.pid) liveChildren.delete(child.pid);
      writeLog({ jobId: job.id, level: 'error', message: `Build process error: ${formatError(error)}` });
      writeStageStatus({ jobId: job.id, stage: label, command, status: 'error', error: formatError(error), finishedAt: new Date().toISOString() });
      reject(error);
    });
    child.on('exit', (code, signal) => {
      writeLog({ jobId: job.id, level: code === 0 ? 'info' : 'error', message: `Build process exit code=${code} signal=${signal || ''}` });
    });
    child.on('close', (code, signal) => {
      clearInterval(heartbeatTimer);
      if (child.pid) liveChildren.delete(child.pid);
      writeLog({ jobId: job.id, level: code === 0 ? 'info' : 'error', message: `Build process close code=${code} signal=${signal || ''}` });
      writeStageStatus({ jobId: job.id, stage: label, command, status: code === 0 ? 'success' : 'failed', code, signal, finishedAt: new Date().toISOString() });
      resolve({ code, signal, artifacts: listArtifacts(startMs) });
    });
  });
}

async function runBuildCommand(command, job) {
  const normalized = String(command || '').trim().toLowerCase();
  if (normalized === 'npm run build:win' || normalized === 'npm.cmd run build:win') {
    const steps = [
      ['clean oem package output', 'node scripts/clean-oem-package-output.cjs'],
      ['prepare-package', 'node apply-oem.cjs && py -3 scripts/export-runtime-system-templates.py && node scripts/prepare-package.cjs'],
      ['build renderer/main/preload', 'node scripts/clean-build-artifacts.cjs && node_modules\\.bin\\vite.cmd build'],
      ['build resource artifacts', 'node scripts/build-resource-artifacts.cjs'],
      ['make oem builder config', 'node scripts/make-oem-builder-config.cjs'],
      ['electron-builder win x64', 'node_modules\\.bin\\electron-builder.cmd --config tools\\oem-batch-builder\\electron-builder.oem.generated.json --win --x64'],
      ['hydrate oem portable bundles', 'node scripts/hydrate-oem-portable-bundles.cjs'],
      ['build full offline installer', 'node scripts/build-oem-inno-installer.cjs'],
      ['assemble release artifacts', 'node scripts/assemble-release-artifacts.cjs'],
    ];

    let artifacts = [];
    for (const [label, stepCommand] of steps) {
      const result = await runCommand(stepCommand, job, label);
      artifacts = result.artifacts || artifacts;
      if (result.code !== 0) {
        return { ...result, artifacts };
      }
    }
    return { code: 0, signal: null, artifacts: listArtifacts(Date.now() - 24 * 60 * 60 * 1000) };
  }

  return runCommand(command, job);
}

async function runOneJob(rawJob, index) {
  const job = {
    ...rawJob,
    id: rawJob.id || `oem-${Date.now()}-${index}`,
    buildCommand: rawJob.buildCommand || 'npm run build:win',
  };

  writeLog({ jobId: job.id, level: 'info', message: `Prepare ${job.brandName} v${job.version}` });
  repairJobPaths(job);
  validateJob(job);
  copyAssets(job);
  writeOemConfig(job);

  const { code, signal, artifacts } = await runBuildCommand(job.buildCommand, job);
  const success = code === 0;
  const result = {
    id: job.id,
    brandName: job.brandName,
    version: job.version,
    success,
    code,
    signal,
    outputDir,
    buildOutputFile,
    artifacts,
    message: success ? 'Build finished' : `Build failed, exit code ${code}${signal ? `, signal ${signal}` : ''}`,
  };
  writeLog({ jobId: job.id, level: success ? 'info' : 'error', message: result.message });
  if (artifacts.length > 0) {
    writeLog({ jobId: job.id, level: 'info', message: `Artifacts:\n${artifacts.join('\n')}` });
  }
  return result;
}

process.on('uncaughtException', error => {
  const message = formatError(error);
  try {
    writeLog({ level: 'error', message: `Worker uncaughtException: ${message}` });
    if (!resultWritten) {
      writeResult({
        success: false,
        results: [],
        outputDir,
        buildOutputFile,
        message: `Worker crashed: ${message}`,
      });
    }
  } finally {
    process.exit(1);
  }
});

process.on('unhandledRejection', error => {
  const message = formatError(error);
  writeLog({ level: 'error', message: `Worker unhandledRejection: ${message}` });
  if (!resultWritten) {
    writeResult({
      success: false,
      results: [],
      outputDir,
      buildOutputFile,
      message: `Worker rejected: ${message}`,
    });
  }
  process.exitCode = 1;
});

process.on('exit', code => {
  try {
    writeLog({ level: 'info', message: `Worker exit code=${code} resultWritten=${resultWritten} liveChildren=${JSON.stringify([...liveChildren.entries()])}` });
  } catch {
    // ignore exit logging failure
  }
  if (!resultWritten) {
    try {
      writeResult({
        success: false,
        results: [],
        outputDir,
        buildOutputFile,
        message: `Worker exited before writing a result. Exit code ${code}. See ${logFile}`,
      });
    } catch {
      // Exit handlers cannot reliably recover from filesystem failures.
    }
  }
});

process.on('beforeExit', code => {
  writeLog({ level: 'info', message: `Worker beforeExit code=${code} resultWritten=${resultWritten} liveChildren=${JSON.stringify([...liveChildren.entries()])}` });
});

process.on('SIGTERM', () => {
  writeLog({ level: 'error', message: `Worker received SIGTERM resultWritten=${resultWritten} liveChildren=${JSON.stringify([...liveChildren.entries()])}` });
});

process.on('SIGINT', () => {
  writeLog({ level: 'error', message: `Worker received SIGINT resultWritten=${resultWritten} liveChildren=${JSON.stringify([...liveChildren.entries()])}` });
});

(async () => {
  ensureSessionDir();
  const parsedJobs = JSON.parse(fs.readFileSync(jobsFile, 'utf8').replace(/^\uFEFF/, ''));
  const jobs = Array.isArray(parsedJobs) ? parsedJobs : [parsedJobs];
  const results = [];

  writeLog({ level: 'info', message: `Batch started, jobs=${jobs.length}` });
  for (let i = 0; i < jobs.length; i++) {
    try {
      results.push(await runOneJob(jobs[i], i));
    } catch (error) {
      const failed = {
        id: jobs[i] && jobs[i].id ? jobs[i].id : `oem-${i}`,
        brandName: jobs[i] && jobs[i].brandName ? jobs[i].brandName : `OEM ${i + 1}`,
        success: false,
        outputDir,
        buildOutputFile,
        artifacts: [],
        message: formatError(error),
      };
      results.push(failed);
      writeLog({ jobId: failed.id, level: 'error', message: failed.message });
    }
  }

  const payload = { success: results.every(item => item.success), results, outputDir, buildOutputFile };
  writeResult(payload);
  writeLog({ level: 'info', message: 'Batch finished' });
})().catch(error => {
  const payload = {
    success: false,
    results: [],
    outputDir,
    buildOutputFile,
    message: formatError(error),
  };
  writeResult(payload);
  writeLog({ level: 'error', message: payload.message });
  process.exitCode = 1;
});
