#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(__dirname, '..');
const runtimeRoot = path.join(root, '.runtime', 'portal');
const distDir = path.join(root, 'dist');
const publicDir = path.join(root, 'public');
const nodeModulesDir = path.join(root, 'node_modules');
const schemaFile = path.join(root, 'schema.sql');
const esbuildBin = path.join(root, 'node_modules', '.pnpm', 'node_modules', '.bin', 'esbuild');
const skipBuild = process.argv.includes('--skip-build');
const skipNodeModules = process.argv.includes('--skip-node-modules');

const externalPackages = [
  '@node-rs/argon2',
  '@resvg/resvg-js',
  'debug',
  'express',
  'http-proxy-middleware',
  'jsonrepair',
  'mysql2',
  'mysql2/promise',
  'qrcode',
  'redis',
  'undici',
];

function run(command, args, options = {}) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      cwd: root,
      stdio: 'inherit',
      env: process.env,
      ...options,
    });
    child.on('exit', (code) => {
      if (code === 0) resolve();
      else reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 1}`));
    });
    child.on('error', reject);
  });
}

async function exists(targetPath) {
  try {
    await fs.access(targetPath);
    return true;
  } catch {
    return false;
  }
}

async function remove(targetPath) {
  await fs.rm(targetPath, { recursive: true, force: true });
}

async function copyDir(source, target) {
  await fs.mkdir(path.dirname(target), { recursive: true });
  await fs.cp(source, target, { recursive: true, dereference: false });
}

async function writeFile(targetPath, content) {
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
  await fs.writeFile(targetPath, content, 'utf8');
}

async function buildFrontend() {
  if (skipBuild) return;
  console.log('==> 构建 Portal 前端 dist');
  await run('npm', ['run', 'build']);
}

async function bundleServer() {
  if (!(await exists(esbuildBin))) {
    throw new Error(`未找到 esbuild: ${esbuildBin}`);
  }
  console.log('==> 打包 Portal 后端为单文件 runtime');
  const args = [
    'server.mjs',
    '--bundle',
    '--platform=node',
    '--format=esm',
    '--target=node24',
    '--outfile=.runtime/portal/server.mjs',
    '--banner:js=import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
  ];
  for (const pkg of externalPackages) {
    args.push(`--external:${pkg}`);
  }
  await run(esbuildBin, args);
}

async function bundleSandboxMcp() {
  if (!(await exists(esbuildBin))) {
    throw new Error(`未找到 esbuild: ${esbuildBin}`);
  }
  console.log('==> 打包 sandbox MCP 为单文件 runtime');
  const args = [
    'mindspace-sandbox-mcp.mjs',
    '--bundle',
    '--platform=node',
    '--format=esm',
    '--target=node24',
    '--outfile=.runtime/portal/mindspace-sandbox-mcp.mjs',
    '--banner:js=import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
  ];
  for (const pkg of externalPackages) {
    args.push(`--external:${pkg}`);
  }
  await run(esbuildBin, args);
}

async function copyRuntimeAssets() {
  console.log('==> 拷贝运行时静态资源');
  await remove(runtimeRoot);
  await fs.mkdir(runtimeRoot, { recursive: true });

  if (!(await exists(distDir))) {
    throw new Error(`缺少 dist 目录: ${distDir}`);
  }
  await copyDir(distDir, path.join(runtimeRoot, 'dist'));

  if (await exists(publicDir)) {
    await copyDir(publicDir, path.join(runtimeRoot, 'public'));
  }

  if (await exists(schemaFile)) {
    await fs.copyFile(schemaFile, path.join(runtimeRoot, 'schema.sql'));
  }
}

async function copyNodeModules() {
  if (skipNodeModules) return;
  if (!(await exists(nodeModulesDir))) {
    throw new Error(`缺少 node_modules 目录: ${nodeModulesDir}`);
  }
  console.log('==> 拷贝生产运行依赖 node_modules');
  await copyDir(nodeModulesDir, path.join(runtimeRoot, 'node_modules'));
  await rewriteNodeModulesSymlinks(path.join(runtimeRoot, 'node_modules'));
}

async function rewriteNodeModulesSymlinks(runtimeNodeModulesDir) {
  console.log('==> 重写 node_modules 顶层符号链接为 runtime 内相对路径');
  const localNodeModulesRoot = `${nodeModulesDir}${path.sep}`;

  async function visit(dir) {
    const entries = await fs.readdir(dir);
    for (const entryName of entries) {
      const fullPath = path.join(dir, entryName);
      const stats = await fs.lstat(fullPath);
      if (stats.isSymbolicLink()) {
        const linkTarget = await fs.readlink(fullPath);
        const normalizedTarget = path.normalize(linkTarget);
        if (!normalizedTarget.startsWith(localNodeModulesRoot)) continue;
        const relativeInsideNodeModules = normalizedTarget.slice(localNodeModulesRoot.length);
        const runtimeTarget = path.join(runtimeNodeModulesDir, relativeInsideNodeModules);
        const replacement = path.relative(path.dirname(fullPath), runtimeTarget);
        await fs.unlink(fullPath);
        await fs.symlink(replacement, fullPath);
        continue;
      }
      if (stats.isDirectory()) {
        await visit(fullPath);
      }
    }
  }

  await visit(runtimeNodeModulesDir);
}

async function writeMetadata() {
  const packageJson = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
  const runtimePackageJson = {
    name: `${packageJson.name}-portal-runtime`,
    private: true,
    type: 'module',
  };
  await writeFile(path.join(runtimeRoot, 'package.json'), `${JSON.stringify(runtimePackageJson, null, 2)}\n`);
  await writeFile(
    path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'),
    [
      '#!/usr/bin/env bash',
      'set -euo pipefail',
      '',
      'ROOT="$(cd "$(dirname "$0")/.." && pwd)"',
      'cd "$ROOT"',
      '',
      'if [[ -f "${ROOT}/.env" ]]; then',
      '  set -a',
      '  # shellcheck disable=SC1091',
      '  source "${ROOT}/.env"',
      '  set +a',
      'fi',
      '',
      'export NODE_ENV=production',
      'export H5_PORT="${H5_PORT:-8081}"',
      'export H5_PUBLIC_BASE_URL="${H5_PUBLIC_BASE_URL:-https://m.tkmind.cn}"',
      'export TKMIND_API_TARGET="${TKMIND_API_TARGET:-https://127.0.0.1:18006}"',
      'export TKMIND_API_TARGET_1="${TKMIND_API_TARGET_1:-https://127.0.0.1:18007}"',
      '',
      'NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"',
      'if [[ ! -x "${NODE_BIN}" ]]; then',
      '  NODE_BIN="$(command -v node)"',
      'fi',
      '',
      'exec "${NODE_BIN}" "${ROOT}/server.mjs"',
      '',
    ].join('\n'),
  );
  await writeFile(
    path.join(runtimeRoot, 'RUNBOOK.txt'),
    [
      'Portal runtime artifact',
      '',
      'This directory is meant to run on production without the source tree.',
      'Required persisted items to inherit on the host:',
      '  .env',
      '  MindSpace/',
      '  data/',
      '  users/',
      '  .tailscale/',
      '  public/plaza-covers/',
      '  logs/',
      '',
      'Bundled alongside server.mjs (required for sandbox-fs MCP):',
      '  mindspace-sandbox-mcp.mjs  (esbuild bundle; includes schedule-service deps)',
      '',
      'Key runtime differences must stay in .env, not in the artifact:',
      '  DATABASE_URL / MYSQL_*',
      '  H5_PUBLIC_BASE_URL',
      '  TKMIND_API_TARGET / TKMIND_API_TARGET_1',
      '  H5_USERS_ROOT / MINDSPACE_STORAGE_ROOT / MEMIND_SHARED_PUBLISH_ROOT',
      '',
      'Deployment and operations transport:',
      '  105 fixed IP: 120.26.184.105',
      '  103 / Studio fixed IP: 58.38.22.103',
      '  Do not switch back to 10.10.* LAN paths unless explicitly required.',
      '',
    ].join('\n'),
  );
}

async function main() {
  await buildFrontend();
  await copyRuntimeAssets();
  await bundleServer();
  await bundleSandboxMcp();
  await copyNodeModules();
  await writeMetadata();
  await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), 0o755);
  console.log('');
  console.log(`Portal runtime 已生成: ${runtimeRoot}`);
}

main().catch((error) => {
  console.error(error instanceof Error ? error.message : error);
  process.exit(1);
});
