import { createServer } from 'node:http';
import { execFile, spawn } from 'node:child_process';
import { access, chmod, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '..');
const host = process.env.SERVER_COMMAND_RUNNER_HOST?.trim() || '0.0.0.0';
const port = Number(process.env.SERVER_COMMAND_RUNNER_PORT?.trim() || '3211');
const accessToken = process.env.SERVER_COMMAND_RUNNER_ACCESS_TOKEN?.trim() || 'local-server-command-runner';
const runnerLogFile = process.env.SERVER_COMMAND_RUNNER_LOG_FILE?.trim() || '/tmp/server-command-runner.log';
const heartbeatFile =
  process.env.SERVER_COMMAND_RUNNER_HEARTBEAT_FILE?.trim() || path.join(projectRoot, '.server-command-runner-heartbeat.json');
const startedAt = new Date().toISOString();
const runnerLogMaxBytes = Math.max(256 * 1024, Number(process.env.SERVER_COMMAND_RUNNER_LOG_MAX_BYTES?.trim() || `${5 * 1024 * 1024}`));
const runnerLogTrimToBytes = Math.max(
  128 * 1024,
  Math.min(
    runnerLogMaxBytes,
    Number(process.env.SERVER_COMMAND_RUNNER_LOG_TRIM_TO_BYTES?.trim() || `${2 * 1024 * 1024}`),
  ),
);
const runnerLogTrimIntervalMs = Math.max(
  15_000,
  Number(process.env.SERVER_COMMAND_RUNNER_LOG_TRIM_INTERVAL_MS?.trim() || '60000'),
);
const STREAM_CAPTURE_LIMIT = 256 * 1024;
const CODEX_LIVE_IDLE_TIMEOUT_MS = Math.max(
  30_000,
  Number(process.env.SERVER_COMMAND_RUNNER_CODEX_IDLE_TIMEOUT_MS?.trim() || '180000'),
);
const CODEX_LIVE_MAX_EXECUTION_MS = Math.max(
  CODEX_LIVE_IDLE_TIMEOUT_MS,
  Number(process.env.SERVER_COMMAND_RUNNER_CODEX_MAX_EXECUTION_MS?.trim() || `${10 * 60 * 1000}`),
);
const CODEX_LIVE_MODEL =
  process.env.SERVER_COMMAND_RUNNER_CODEX_MODEL?.trim() ||
  process.env.PLAN_CODEX_MODEL?.trim() ||
  'gpt-5.4';
const CODEX_HOME_RUNTIME_PATHS = [
  'auth.json',
  'config.toml',
  'rules',
  'skills',
  'vendor_imports',
  'models_cache.json',
  'version.json',
];
const CHAT_SESSION_RESOURCE_DIR_MODE = 0o777;
const CODEX_LIVE_EVENT_HISTORY_LIMIT = 400;
const CODEX_LIVE_FINISHED_RETENTION_MS = Math.max(
  60_000,
  Number(process.env.SERVER_COMMAND_RUNNER_CODEX_FINISHED_RETENTION_MS?.trim() || `${10 * 60 * 1000}`),
);
const activeCodexExecutions = new Map();
const recentCodexExecutions = new Map();
let runnerShutdownSignal = null;

function logRunner(message) {
  process.stdout.write("[server-command-runner] " + new Date().toISOString() + " " + message + "\n");
}

function summarizeActiveExecutionIds(limit = 8) {
  const requestIds = Array.from(activeCodexExecutions.keys()).slice(0, limit);
  const suffix = activeCodexExecutions.size > requestIds.length ? " +" + (activeCodexExecutions.size - requestIds.length) + " more" : "";
  return requestIds.length > 0 ? requestIds.join(", ") + suffix : "none";
}

function resolveSignalExitCode(signal) {
  switch (signal) {
    case "SIGINT":
      return 130;
    case "SIGHUP":
      return 129;
    case "SIGTERM":
      return 143;
    default:
      return 1;
  }
}

function shutdownRunnerFromSignal(signal) {
  if (runnerShutdownSignal) {
    return;
  }

  runnerShutdownSignal = signal;
  logRunner("received " + signal + "; activeExecutions=" + activeCodexExecutions.size + "; requestIds=" + summarizeActiveExecutionIds());
  process.exit(resolveSignalExitCode(signal));
}

process.once("SIGTERM", () => shutdownRunnerFromSignal("SIGTERM"));
process.once("SIGINT", () => shutdownRunnerFromSignal("SIGINT"));
process.once("SIGHUP", () => shutdownRunnerFromSignal("SIGHUP"));
process.on("exit", (code) => {
  const shutdownLabel = runnerShutdownSignal === null ? "none" : runnerShutdownSignal;
  logRunner("exiting with code " + code + "; shutdownSignal=" + shutdownLabel + "; activeExecutions=" + activeCodexExecutions.size);
});

function resolveCodexLiveModel(value) {
  const normalized = String(value ?? '').trim();
  return /^[A-Za-z0-9._:-]+$/u.test(normalized) ? normalized : CODEX_LIVE_MODEL;
}

function createCodexExecutionRecord({ requestId, child, tempDir }) {
  return {
    requestId,
    child,
    tempDir,
    subscribers: new Set(),
    history: [],
    completed: false,
    cleanupTimer: null,
  };
}

function registerSubscriberClose(record, response) {
  response.on('close', () => {
    record.subscribers.delete(response);
  });
}

function attachCodexExecutionSubscriber(record, response) {
  response.writeHead(200, {
    'content-type': 'application/x-ndjson; charset=utf-8',
    'cache-control': 'no-store',
  });
  record.subscribers.add(response);
  registerSubscriberClose(record, response);

  for (const payload of record.history) {
    sendJsonLine(response, payload);
  }

  if (record.completed) {
    response.end();
    record.subscribers.delete(response);
  }
}

function broadcastCodexExecutionEvent(record, payload) {
  record.history.push(payload);
  if (record.history.length > CODEX_LIVE_EVENT_HISTORY_LIMIT) {
    record.history.splice(0, record.history.length - CODEX_LIVE_EVENT_HISTORY_LIMIT);
  }

  for (const response of record.subscribers) {
    try {
      sendJsonLine(response, payload);
    } catch {
      record.subscribers.delete(response);
    }
  }
}

function closeCodexExecutionSubscribers(record) {
  for (const response of record.subscribers) {
    try {
      response.end();
    } catch {
      // noop
    }
  }

  record.subscribers.clear();
}

function scheduleCodexExecutionCleanup(record) {
  if (record.cleanupTimer) {
    clearTimeout(record.cleanupTimer);
  }

  record.cleanupTimer = setTimeout(async () => {
    recentCodexExecutions.delete(record.requestId);
    await rm(record.tempDir, { recursive: true, force: true }).catch(() => undefined);
  }, CODEX_LIVE_FINISHED_RETENTION_MS);
  record.cleanupTimer.unref?.();
}

function normalizeCodexUsageMetricValue(value) {
  if (typeof value === 'number' && Number.isFinite(value)) {
    return Math.max(0, Math.round(value));
  }

  if (typeof value === 'string' && value.trim()) {
    const normalized = Number(value);
    if (Number.isFinite(normalized)) {
      return Math.max(0, Math.round(normalized));
    }
  }

  return null;
}

function extractCodexUsageSnapshot(parsed) {
  if (!parsed || typeof parsed !== 'object') {
    return null;
  }

  const usage =
    parsed.usage && typeof parsed.usage === 'object'
      ? parsed.usage
      : parsed.response &&
          typeof parsed.response === 'object' &&
          parsed.response !== null &&
          parsed.response.usage &&
          typeof parsed.response.usage === 'object'
        ? parsed.response.usage
        : null;

  if (!usage) {
    return null;
  }

  const input = normalizeCodexUsageMetricValue(usage.input_tokens);
  const output = normalizeCodexUsageMetricValue(usage.output_tokens);
  const cached = normalizeCodexUsageMetricValue(usage.cached_input_tokens);
  const reasoning = normalizeCodexUsageMetricValue(usage.reasoning_output_tokens ?? usage.reasoning_tokens);
  const total =
    normalizeCodexUsageMetricValue(usage.total_tokens) ??
    normalizeCodexUsageMetricValue(usage.totalTokens) ??
    [input ?? 0, output ?? 0].reduce((sum, value) => sum + value, 0);

  if (input === null && output === null && cached === null && reasoning === null && total === null) {
    return null;
  }

  return {
    tokenTotals: {
      total: total ?? 0,
      input: input ?? 0,
      output: output ?? 0,
      cached: cached ?? 0,
      reasoning: reasoning ?? 0,
    },
    totalTokens: total ?? 0,
  };
}

function extractCodexUsageSnapshotFromText(output) {
  const text = String(output ?? '');

  if (!text.trim()) {
    return null;
  }

  const lines = text
    .split('\n')
    .map((line) => line.trim())
    .filter(Boolean);

  for (let index = lines.length - 1; index >= 0; index -= 1) {
    const line = lines[index] ?? '';

    if (!line.startsWith('{')) {
      continue;
    }

    try {
      const usageSnapshot = extractCodexUsageSnapshot(JSON.parse(line));

      if (usageSnapshot) {
        return usageSnapshot;
      }
    } catch {
      // ignore malformed JSON lines during fallback scan
    }
  }

  return null;
}

function finalizeCodexExecution(record) {
  if (record.completed) {
    return;
  }

  record.completed = true;
  activeCodexExecutions.delete(record.requestId);
  recentCodexExecutions.set(record.requestId, record);
  closeCodexExecutionSubscribers(record);
  scheduleCodexExecutionCleanup(record);
}

async function ensureWorldWritableDirectory(absolutePath) {
  await mkdir(absolutePath, { recursive: true, mode: CHAT_SESSION_RESOURCE_DIR_MODE });
  await chmod(absolutePath, CHAT_SESSION_RESOURCE_DIR_MODE).catch(() => {});
}

async function ensureWritableChatSessionDirectories(repoPath, sessionId) {
  const sessionRoot = path.join(repoPath, 'public', '.codex_chat', sessionId);
  const resourceRoot = path.join(sessionRoot, 'resource');
  const uploadRoot = path.join(resourceRoot, 'uploads');

  await ensureWorldWritableDirectory(sessionRoot);
  await ensureWorldWritableDirectory(resourceRoot);
  await ensureWorldWritableDirectory(uploadRoot);

  return {
    resourceRoot,
    uploadRoot,
  };
}

const commandDefinitions = {
  test: {
    label: 'TEST',
    scriptPath: path.join(projectRoot, 'etc', 'commands', 'server-command', 'restart-test.sh'),
    workingDirectory: projectRoot,
    env: {
      MAIN_PROJECT_ROOT: projectRoot,
      SERVER_COMMAND_COMPOSE_FILE: path.join(projectRoot, 'docker-compose.yml'),
      SERVER_COMMAND_SERVICE: process.env.SERVER_COMMAND_TEST_SERVICE?.trim() || 'app',
      SERVER_COMMAND_CONTAINER_NAME: process.env.SERVER_COMMAND_TEST_CONTAINER_NAME?.trim() || 'ai-code-app-app-1',
    },
    restartStrategy: 'deferred',
  },
  rel: {
    label: 'REL',
    scriptPath: path.join(projectRoot, 'etc', 'commands', 'server-command', 'restart-rel.sh'),
    workingDirectory: projectRoot,
    env: {
      MAIN_PROJECT_ROOT: projectRoot,
      SERVER_COMMAND_COMPOSE_FILE: path.join(projectRoot, 'docker-compose.yml'),
      SERVER_COMMAND_SERVICE: process.env.SERVER_COMMAND_REL_SERVICE?.trim() || 'release-app',
    },
    restartStrategy: 'deferred',
  },
  prod: {
    label: 'PROD',
    scriptPath: path.join(projectRoot, 'etc', 'commands', 'server-command', 'restart-prod.sh'),
    workingDirectory: projectRoot,
    env: {
      MAIN_PROJECT_ROOT: projectRoot,
      SERVER_COMMAND_COMPOSE_FILE: path.join(projectRoot, 'docker-compose.yml'),
      SERVER_COMMAND_SERVICE: process.env.SERVER_COMMAND_PROD_SERVICE?.trim() || 'prod-app',
      SERVER_COMMAND_CONTAINER_NAME: process.env.SERVER_COMMAND_PROD_CONTAINER_NAME?.trim() || 'ai-code-app-prod',
    },
    restartStrategy: 'deferred',
  },
  'work-server': {
    label: 'WORK-SERVER',
    scriptPath: path.join(projectRoot, 'etc', 'commands', 'server-command', 'restart-work-server.sh'),
    workingDirectory: projectRoot,
    env: {
      REPO_ROOT: projectRoot,
    },
    restartStrategy: 'deferred',
  },
  'command-runner': {
    label: 'COMMAND-RUNNER',
    scriptPath: path.join(projectRoot, 'etc', 'commands', 'server-command', 'restart-server-command-runner.sh'),
    workingDirectory: projectRoot,
    env: {
      PROJECT_ROOT: projectRoot,
      SERVER_COMMAND_RUNNER_NODE_BIN:
        process.env.SERVER_COMMAND_RUNNER_NODE_BIN?.trim() || 'node',
      SERVER_COMMAND_RUNNER_HOST: process.env.SERVER_COMMAND_RUNNER_HOST?.trim() || '127.0.0.1',
      SERVER_COMMAND_RUNNER_PORT: process.env.SERVER_COMMAND_RUNNER_PORT?.trim() || '3211',
      SERVER_COMMAND_RUNNER_ACCESS_TOKEN: process.env.SERVER_COMMAND_RUNNER_ACCESS_TOKEN?.trim() || accessToken,
    },
    restartStrategy: 'deferred',
  },
};

function normalizeRepoPathCandidate(value) {
  const normalized = String(value ?? '').trim();
  return normalized ? path.resolve(normalized) : '';
}

function translateWorkspacePathToHost(inputPath) {
  const normalizedInput = normalizeRepoPathCandidate(inputPath);

  if (!normalizedInput) {
    return normalizedInput;
  }

  const replacements = [
    ['/workspace/main-project', projectRoot],
    ['/workspace/auto_codex/repo', projectRoot],
  ];

  for (const [workspacePrefix, hostPrefix] of replacements) {
    if (normalizedInput === workspacePrefix) {
      return hostPrefix;
    }

    if (normalizedInput.startsWith(`${workspacePrefix}${path.sep}`)) {
      return path.join(hostPrefix, normalizedInput.slice(workspacePrefix.length + 1));
    }
  }

  return normalizedInput;
}

function trimOutput(value, maxLength = 400) {
  const normalized = value.replace(/\s+/g, ' ').trim();
  if (!normalized) {
    return null;
  }

  return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 3)}...` : normalized;
}

function sendJson(response, statusCode, payload) {
  response.writeHead(statusCode, {
    'content-type': 'application/json; charset=utf-8',
    'cache-control': 'no-store',
  });
  response.end(JSON.stringify(payload));
}

async function trimRunnerLogIfNeeded() {
  try {
    const logStat = await stat(runnerLogFile);

    if (!logStat.isFile() || logStat.size <= runnerLogMaxBytes) {
      return;
    }

    const content = await readFile(runnerLogFile, 'utf8');
    const trimmedContent = content.slice(-runnerLogTrimToBytes);
    const boundaryIndex = trimmedContent.indexOf('\n');
    const nextContent = boundaryIndex >= 0 ? trimmedContent.slice(boundaryIndex + 1) : trimmedContent;

    await writeFile(runnerLogFile, `[log-trimmed ${new Date().toISOString()}]\n${nextContent}`, 'utf8');
  } catch {
    // ignore log trim failures so the runner itself keeps serving requests
  }
}

async function writeHeartbeat() {
  await mkdir(path.dirname(heartbeatFile), { recursive: true });
  await writeFile(
    heartbeatFile,
    JSON.stringify(
      {
        ok: true,
        service: 'server-command-runner',
        pid: process.pid,
        host,
        port,
        cwd: projectRoot,
        startedAt,
        updatedAt: new Date().toISOString(),
      },
      null,
      2,
    ),
    'utf8',
  );
}

void trimRunnerLogIfNeeded();
setInterval(() => {
  void trimRunnerLogIfNeeded();
}, runnerLogTrimIntervalMs).unref();

function sendJsonLine(response, payload) {
  response.write(`${JSON.stringify(payload)}\n`);
}

function normalizeNumericSeconds(value) {
  if (typeof value === 'number' && Number.isFinite(value)) {
    return value;
  }

  if (typeof value === 'string') {
    const trimmed = value.trim();

    if (!trimmed) {
      return null;
    }

    const parsed = Number(trimmed);
    return Number.isFinite(parsed) ? parsed : null;
  }

  return null;
}

function readRequestBody(request, maxBytes = 4 * 1024 * 1024) {
  return new Promise((resolve, reject) => {
    let total = 0;
    const chunks = [];

    request.on('data', (chunk) => {
      total += chunk.length;

      if (total > maxBytes) {
        reject(new Error('요청 본문이 너무 큽니다.'));
        request.destroy();
        return;
      }

      chunks.push(chunk);
    });

    request.on('end', () => {
      resolve(Buffer.concat(chunks).toString('utf8'));
    });

    request.on('error', reject);
  });
}

function summarizeCodexOutput(output) {
  const normalized = String(output ?? '').trim();

  if (!normalized) {
    return 'Codex 실행 결과가 비어 있습니다.';
  }

  return normalized
    .split('\n')
    .map((line) => line.trimEnd())
    .filter(Boolean)
    .slice(-12)
    .join('\n');
}

function summarizeCommand(command, limit = 180) {
  const normalized = String(command ?? '').replace(/\s+/g, ' ').trim();
  return normalized.length > limit ? `${normalized.slice(0, limit - 1).trimEnd()}...` : normalized;
}

function summarizeCommandOutput(output, maxLines = 3, maxLength = 220) {
  const lines = String(output ?? '')
    .replace(/\r/g, '')
    .split('\n')
    .map((line) => line.trim())
    .filter(Boolean);

  if (lines.length === 0) {
    return '';
  }

  const joined = lines.slice(0, maxLines).join(' / ');
  return joined.length > maxLength ? `${joined.slice(0, maxLength - 1).trimEnd()}...` : joined;
}

function inferCommandReason(command) {
  const normalized = command.toLowerCase();

  if (normalized.includes('rg ') || normalized.includes('ripgrep')) {
    return '관련 파일이나 텍스트를 빠르게 찾기 위해 검색했습니다.';
  }

  if (normalized.includes('sed -n') || normalized.includes('cat ') || normalized.includes('less ')) {
    return '해당 파일 내용을 직접 읽어 문맥을 확인했습니다.';
  }

  if (normalized.includes('ls ') || normalized === 'ls' || normalized.includes('rg --files')) {
    return '디렉터리 구조와 대상 파일 위치를 확인했습니다.';
  }

  if (normalized.includes('git status')) {
    return '작업 트리 상태와 변경 범위를 점검했습니다.';
  }

  if (normalized.includes('tsc ') || normalized.includes('npm test') || normalized.includes('node --test')) {
    return '수정 후 타입이나 동작 검증을 진행했습니다.';
  }

  if (normalized.includes('curl ') || normalized.includes('fetch')) {
    return '실제 응답이나 연결 상태를 확인했습니다.';
  }

  return '현재 요청을 해결하기 위한 다음 단계를 확인했습니다.';
}

function isIgnorableCodexDiagnosticLine(line) {
  const normalized = String(line ?? '').trim();

  if (!normalized) {
    return true;
  }

  return (
    /^\d{4}-\d{2}-\d{2}T\S+\s+WARN\b/.test(normalized) ||
    normalized.includes('ignoring interface.defaultPrompt') ||
    normalized.includes('failed to open state db') ||
    normalized.includes('state db discrepancy') ||
    normalized.includes('Failed to kill MCP process group') ||
    normalized.includes('Failed to delete shell snapshot') ||
    normalized.includes('failed to unwatch ')
  );
}

function collectCodexTextFragments(value) {
  if (typeof value === 'string') {
    const normalized = value.trim();
    return normalized ? [normalized] : [];
  }

  if (Array.isArray(value)) {
    return value.flatMap((item) => collectCodexTextFragments(item));
  }

  if (!value || typeof value !== 'object') {
    return [];
  }

  const record = value;
  const directTextKeys = ['text', 'delta', 'output_text', 'content', 'message'];

  for (const key of directTextKeys) {
    const fragments = collectCodexTextFragments(record[key]);

    if (fragments.length > 0) {
      return fragments;
    }
  }

  if (typeof record.type === 'string' && record.type.includes('output_text')) {
    const fragments = collectCodexTextFragments(record.text ?? record.delta);

    if (fragments.length > 0) {
      return fragments;
    }
  }

  return Object.values(record).flatMap((item) => collectCodexTextFragments(item));
}

function extractCompletedAgentMessageText(item) {
  if (!item || typeof item !== 'object') {
    return '';
  }

  if (item.type !== 'agent_message') {
    return '';
  }

  return collectCodexTextFragments(item.text ?? item.content ?? item.message).join('');
}

function extractCodexStreamText(parsed) {
  const type = typeof parsed.type === 'string' ? parsed.type : '';

  if (type === 'item.completed') {
    return {
      completedText: extractCompletedAgentMessageText(parsed.item),
      deltaText: '',
    };
  }

  if (type === 'item.delta' || type === 'response.output_text.delta') {
    return {
      completedText: '',
      deltaText:
        type === 'response.output_text.delta'
          ? collectCodexTextFragments(parsed.delta ?? parsed.text).join('')
          : collectCodexTextFragments(parsed.delta).join(''),
    };
  }

  if (type === 'response.completed') {
    return {
      completedText: collectCodexTextFragments(parsed.response).join(''),
      deltaText: '',
    };
  }

  return {
    completedText: '',
    deltaText: '',
  };
}

function extractCodexActivityLog(parsed) {
  const type = typeof parsed.type === 'string' ? parsed.type : '';
  const item = parsed.item && typeof parsed.item === 'object' ? parsed.item : null;
  const itemType = typeof item?.type === 'string' ? item.type : '';

  if (!item || itemType !== 'command_execution') {
    return '';
  }

  const command = summarizeCommand(typeof item.command === 'string' ? item.command : '');

  if (!command) {
    return '';
  }

  const reason = inferCommandReason(command);

  if (type === 'item.started') {
    return `# 이유: ${reason}\n$ ${command}`;
  }

  if (type === 'item.completed') {
    const exitCode = typeof item.exit_code === 'number' && Number.isFinite(item.exit_code) ? Math.round(item.exit_code) : null;
    const outputSummary = summarizeCommandOutput(typeof item.aggregated_output === 'string' ? item.aggregated_output : '');
    const statusLabel = exitCode === null ? '# 결과: 완료' : exitCode === 0 ? '# 결과: 완료(0)' : `# 결과: 종료(${exitCode})`;

    return outputSummary ? `${statusLabel}\n# 출력: ${outputSummary}` : statusLabel;
  }

  return '';
}

async function prepareWritableCodexHome(tempDir) {
  const writableCodexHome = path.join(tempDir, '.codex');
  const sourceCodexHome =
    process.env.CODEX_HOME_TEMPLATE?.trim() ||
    process.env.CODEX_HOME?.trim() ||
    path.join(process.env.HOME ?? '/root', '.codex');

  await mkdir(writableCodexHome, { recursive: true });

  for (const relativePath of CODEX_HOME_RUNTIME_PATHS) {
    const sourcePath = path.join(sourceCodexHome, relativePath);
    const targetPath = path.join(writableCodexHome, relativePath);

    try {
      await access(sourcePath);
    } catch {
      continue;
    }

    await mkdir(path.dirname(targetPath), { recursive: true });
    await cp(sourcePath, targetPath, { recursive: true, force: true });
  }

  return writableCodexHome;
}

async function validateCodexExecutionRuntime(repoPath, codexBin) {
  const issues = [];

  try {
    const repoStat = await stat(repoPath);

    if (!repoStat.isDirectory()) {
      issues.push(`repoPath 경로가 디렉터리가 아닙니다: ${repoPath}`);
    }
  } catch {
    issues.push(`repoPath 경로를 찾지 못했습니다: ${repoPath}`);
  }

  const trimmedCodexBin = String(codexBin ?? '').trim();
  const pathCandidates =
    trimmedCodexBin && !trimmedCodexBin.includes(path.sep)
      ? (process.env.PATH ?? '')
          .split(path.delimiter)
          .map((entry) => entry.trim())
          .filter(Boolean)
          .map((entry) => path.join(entry, trimmedCodexBin))
      : [trimmedCodexBin];

  let codexResolved = false;

  for (const candidate of pathCandidates) {
    try {
      await access(candidate);
      codexResolved = true;
      break;
    } catch {
      // try next candidate
    }
  }

  if (!codexResolved) {
    issues.push(`Codex 실행 파일을 찾지 못했습니다: ${codexBin}`);
  }

  if (issues.length > 0) {
    throw new Error(issues.join(' / '));
  }
}

async function runCodexLiveExecution(payload, response) {
  const requestId = String(payload?.requestId ?? '').trim();
  const sessionId = String(payload?.sessionId ?? '').trim();
  const repoPath = projectRoot;
  const prompt = String(payload?.prompt ?? '');
  const resourceDir = path.join(repoPath, 'public', '.codex_chat', sessionId, 'resource');
  const uploadDir = path.join(resourceDir, 'uploads');
  const codexBin = process.env.SERVER_COMMAND_RUNNER_CODEX_BIN?.trim() || process.env.PLAN_CODEX_BIN?.trim() || 'codex';
  const codexModel = resolveCodexLiveModel(payload?.model);
  const configuredIdleTimeoutMs = resolveCodexLiveIdleTimeoutMs(payload?.idleTimeoutSeconds);
  const configuredMaxExecutionMs = resolveCodexLiveMaxExecutionMs(payload?.maxExecutionSeconds, configuredIdleTimeoutMs);

  if (!requestId || !sessionId || !prompt.trim()) {
    sendJson(response, 400, {
      message: 'requestId, sessionId, prompt는 필수입니다.',
    });
    return;
  }

  const existingExecution = activeCodexExecutions.get(requestId) ?? recentCodexExecutions.get(requestId);

  if (existingExecution) {
    attachCodexExecutionSubscriber(existingExecution, response);
    broadcastCodexExecutionEvent(existingExecution, {
      type: 'attached',
      requestId,
      completed: existingExecution.completed,
    });
    return;
  }

  await validateCodexExecutionRuntime(repoPath, codexBin);
  await ensureWritableChatSessionDirectories(repoPath, sessionId);

  const tempDir = await mkdtemp(path.join(tmpdir(), 'command-runner-codex-'));
  const writableCodexHome = await prepareWritableCodexHome(tempDir);
  let stdoutTail = '';
  let stderrTail = '';
  let jsonLineBuffer = '';
  let completedText = '';
  let streamedUsageSnapshot = null;
  let idleTimer = null;
  let executionTimer = null;
  let terminationRequested = false;

  const child = spawn(
    codexBin,
    ['exec', '--model', codexModel, '--json', '--dangerously-bypass-approvals-and-sandbox', '-C', repoPath, '-'],
    {
      cwd: repoPath,
      stdio: ['pipe', 'pipe', 'pipe'],
      env: {
        ...process.env,
        CODEX_HOME: writableCodexHome,
        CODEX_LIVE_CHAT_SESSION_ID: sessionId,
        CODEX_LIVE_CHAT_RESOURCE_DIR: resourceDir,
        CODEX_LIVE_CHAT_UPLOAD_DIR: uploadDir,
      },
    },
  );

  const executionRecord = createCodexExecutionRecord({
    requestId,
    child,
    tempDir,
  });
  logRunner("spawned Codex child pid=" + (child.pid ?? "unknown") + " requestId=" + requestId + " sessionId=" + sessionId + " model=" + codexModel + " idleTimeoutMs=" + configuredIdleTimeoutMs + " maxExecutionMs=" + configuredMaxExecutionMs);
  activeCodexExecutions.set(requestId, executionRecord);
  attachCodexExecutionSubscriber(executionRecord, response);
  broadcastCodexExecutionEvent(executionRecord, {
    type: 'started',
    pid: child.pid ?? null,
    model: codexModel,
    configuredIdleTimeoutSeconds: Math.round(configuredIdleTimeoutMs / 1000),
    configuredMaxExecutionSeconds: Math.round(configuredMaxExecutionMs / 1000),
  });

  const clearExecutionTimers = () => {
    if (idleTimer) {
      clearTimeout(idleTimer);
      idleTimer = null;
    }

    if (executionTimer) {
      clearTimeout(executionTimer);
      executionTimer = null;
    }
  };

  const requestTermination = (message, reason = 'runner-termination') => {
    if (terminationRequested) {
      return;
    }

    terminationRequested = true;
    clearExecutionTimers();

    broadcastCodexExecutionEvent(executionRecord, {
      type: 'error',
      message,
    });

    logRunner("terminating Codex child pid=" + (child.pid ?? "unknown") + " requestId=" + requestId + " reason=" + reason + " message=" + message);
    child.kill('SIGTERM');
    setTimeout(() => {
      if (!child.killed) {
        child.kill('SIGKILL');
      }
    }, 3000).unref?.();
  };

  const refreshIdleTimer = () => {
    if (terminationRequested) {
      return;
    }

    if (idleTimer) {
      clearTimeout(idleTimer);
    }

    idleTimer = setTimeout(() => {
      requestTermination(
        `Codex Live 실행이 ${Math.round(configuredIdleTimeoutMs / 1000)}초 동안 출력이 없어 중단되었습니다.`,
        'idle-timeout',
      );
    }, configuredIdleTimeoutMs);
    idleTimer.unref?.();
  };

  executionTimer = setTimeout(() => {
    requestTermination(
      `Codex Live 실행이 ${Math.round(configuredMaxExecutionMs / 1000)}초를 넘어 중단되었습니다.`,
      'max-execution-timeout',
    );
  }, configuredMaxExecutionMs);
  executionTimer.unref?.();
  refreshIdleTimer();

  const handleCodexJsonLine = (line) => {
    let parsed;

    try {
      parsed = JSON.parse(line);
    } catch {
      return false;
    }

    const activityLog = extractCodexActivityLog(parsed);

    if (activityLog) {
      refreshIdleTimer();
      broadcastCodexExecutionEvent(executionRecord, {
        type: 'activity',
        line: activityLog,
      });
    }

    const { completedText: nextCompletedText, deltaText } = extractCodexStreamText(parsed);
    const usageSnapshot = extractCodexUsageSnapshot(parsed);

    if (deltaText) {
      refreshIdleTimer();
      broadcastCodexExecutionEvent(executionRecord, {
        type: 'delta',
        text: deltaText,
      });
    }

    if (usageSnapshot) {
      streamedUsageSnapshot = usageSnapshot;
      refreshIdleTimer();
      broadcastCodexExecutionEvent(executionRecord, {
        type: 'usage',
        usageSnapshot,
      });
    }

    if (nextCompletedText) {
      refreshIdleTimer();
      completedText = nextCompletedText;
      broadcastCodexExecutionEvent(executionRecord, {
        type: 'completed',
        text: nextCompletedText,
        usageSnapshot,
      });
    }

    return Boolean(deltaText || usageSnapshot || nextCompletedText || activityLog);
  };

  child.stdout?.on('data', (chunk) => {
    refreshIdleTimer();
    const text = String(chunk);
    stdoutTail = (stdoutTail + text).slice(-STREAM_CAPTURE_LIMIT);
    jsonLineBuffer += text;
    const lines = jsonLineBuffer.split('\n');
    jsonLineBuffer = lines.pop() ?? '';

    for (const rawLine of lines) {
      const line = rawLine.trim();

      if (!line) {
        continue;
      }

      if (handleCodexJsonLine(line)) {
        continue;
      }

      if (!line.startsWith('{') && !isIgnorableCodexDiagnosticLine(line)) {
        broadcastCodexExecutionEvent(executionRecord, {
          type: 'stdout',
          line,
        });
      }
    }
  });

  child.stderr?.on('data', (chunk) => {
    refreshIdleTimer();
    const text = String(chunk);
    stderrTail = (stderrTail + text).slice(-STREAM_CAPTURE_LIMIT);
    text
      .split('\n')
      .map((line) => line.trim())
      .filter(Boolean)
      .forEach((line) => {
        if (isIgnorableCodexDiagnosticLine(line)) {
          return;
        }

        broadcastCodexExecutionEvent(executionRecord, {
          type: 'stderr',
          line,
        });
      });
  });

  child.on('error', async (error) => {
    clearExecutionTimers();
    logRunner("Codex child process error requestId=" + requestId + " pid=" + (child.pid ?? "unknown") + " message=" + (error instanceof Error ? error.message : String(error)));
    broadcastCodexExecutionEvent(executionRecord, {
      type: 'error',
      message: error instanceof Error ? error.message : String(error),
    });
    broadcastCodexExecutionEvent(executionRecord, {
      type: 'finished',
      exitCode: null,
    });
    finalizeCodexExecution(executionRecord);
  });

  child.on('close', async (code, signal) => {
    clearExecutionTimers();
    logRunner("Codex child closed requestId=" + requestId + " pid=" + (child.pid ?? "unknown") + " exitCode=" + (code ?? "null") + " signal=" + (signal ?? "none") + " terminationRequested=" + terminationRequested);
    const trailingLine = jsonLineBuffer.trim();
    if (trailingLine) {
      handleCodexJsonLine(trailingLine);
    }

    if (!streamedUsageSnapshot) {
      const fallbackUsageSnapshot = extractCodexUsageSnapshotFromText([stdoutTail, trailingLine].filter(Boolean).join('\n'));

      if (fallbackUsageSnapshot) {
        streamedUsageSnapshot = fallbackUsageSnapshot;
        broadcastCodexExecutionEvent(executionRecord, {
          type: 'usage',
          usageSnapshot: fallbackUsageSnapshot,
        });
      }
    }

    if (code !== 0) {
      broadcastCodexExecutionEvent(executionRecord, {
        type: 'error',
        message: summarizeCodexOutput(`${stderrTail}\n${completedText}\n${stdoutTail}`),
      });
    }

    broadcastCodexExecutionEvent(executionRecord, {
      type: 'finished',
      exitCode: code ?? null,
    });
    finalizeCodexExecution(executionRecord);
  });

  child.stdin?.end(prompt);
}

function resolveCodexLiveMaxExecutionMs(value, minimumMs = CODEX_LIVE_IDLE_TIMEOUT_MS) {
  const normalizedValue = normalizeNumericSeconds(value);

  if (normalizedValue == null) {
    return Math.max(minimumMs, CODEX_LIVE_MAX_EXECUTION_MS);
  }

  return Math.max(minimumMs, Math.min(7_200_000, Math.max(60_000, Math.round(normalizedValue * 1000))));
}

function resolveCodexLiveIdleTimeoutMs(value) {
  const normalizedValue = normalizeNumericSeconds(value);

  if (normalizedValue == null) {
    return CODEX_LIVE_IDLE_TIMEOUT_MS;
  }

  return Math.min(3_600_000, Math.max(30_000, Math.round(normalizedValue * 1000)));
}

function isAuthorized(request) {
  const token = String(request.headers['x-access-token'] ?? '').trim();
  return token.length > 0 && token === accessToken;
}

async function runRestartCommand(key) {
  const definition = commandDefinitions[key];
  if (!definition) {
    return {
      statusCode: 404,
      payload: {
        message: '지원하지 않는 서버입니다.',
      },
    };
  }

  if (definition.restartStrategy === 'deferred') {
    await new Promise((resolve, reject) => {
      const child = spawn(
        'sh',
        ['-c', `sleep 1 && exec sh "${definition.scriptPath}" >/tmp/${key}-restart.log 2>&1`],
        {
          cwd: definition.workingDirectory,
          detached: true,
          stdio: 'ignore',
          env: {
            ...process.env,
            ...definition.env,
          },
        },
      );

      child.once('error', reject);
      child.once('spawn', () => {
        child.unref();
        resolve();
      });
    });

    return {
      statusCode: 202,
      payload: {
        ok: true,
        restartState: 'accepted',
        commandOutput: `${definition.label} 재기동 요청을 접수했습니다. 잠시 후 상태를 다시 확인해 주세요.`,
      },
    };
  }

  try {
    const commandResult = await execFileAsync('sh', [definition.scriptPath], {
      cwd: definition.workingDirectory,
      timeout: 30000,
      maxBuffer: 1024 * 1024,
      env: {
        ...process.env,
        ...definition.env,
      },
    });

    return {
      statusCode: 200,
      payload: {
        ok: true,
        restartState: 'completed',
        commandOutput: trimOutput([commandResult.stdout, commandResult.stderr].filter(Boolean).join('\n')),
      },
    };
  } catch (error) {
    const failure = error instanceof Error ? error : new Error(String(error));
    const detail = trimOutput(
      [
        failure instanceof Error && 'stderr' in failure ? String(failure.stderr ?? '') : '',
        failure instanceof Error && 'stdout' in failure ? String(failure.stdout ?? '') : '',
        failure.message,
      ]
        .filter(Boolean)
        .join('\n'),
    );
    const code =
      failure instanceof Error && 'code' in failure && (typeof failure.code === 'number' || typeof failure.code === 'string')
        ? String(failure.code)
        : null;

    return {
      statusCode: 500,
      payload: {
        message: `${definition.label} 재기동에 실패했습니다.${code ? ` exit:${code}` : ''}${detail ? ` ${detail}` : ''}`,
      },
    };
  }
}

const server = createServer(async (request, response) => {
  const requestUrl = new URL(request.url || '/', `http://${request.headers.host || `${host}:${port}`}`);

  if (request.method === 'GET' && requestUrl.pathname === '/health') {
    sendJson(response, 200, {
      ok: true,
      service: 'server-command-runner',
      cwd: projectRoot,
    });
    return;
  }

  if (!isAuthorized(request)) {
    sendJson(response, 403, {
      message: '권한 토큰이 필요합니다.',
    });
    return;
  }

  const matchedPath = requestUrl.pathname.match(/^\/api\/server-commands\/([^/]+)\/actions\/restart$/);
  if (request.method === 'POST' && matchedPath) {
    const key = decodeURIComponent(matchedPath[1]);
    const result = await runRestartCommand(key);
    sendJson(response, result.statusCode, result.payload);
    return;
  }

  if (request.method === 'POST' && requestUrl.pathname === '/api/codex-live/execute') {
    try {
      const rawBody = await readRequestBody(request);
      const payload = rawBody ? JSON.parse(rawBody) : {};
      await runCodexLiveExecution(payload, response);
    } catch (error) {
      sendJson(response, 500, {
        message: error instanceof Error ? error.message : 'Codex 실행 요청을 처리하지 못했습니다.',
      });
    }
    return;
  }

  const attachMatch = requestUrl.pathname.match(/^\/api\/codex-live\/jobs\/([^/]+)\/attach$/);
  if (request.method === 'POST' && attachMatch) {
    const requestId = decodeURIComponent(attachMatch[1]);
    const execution = activeCodexExecutions.get(requestId) ?? recentCodexExecutions.get(requestId);

    if (!execution) {
      sendJson(response, 404, {
        attached: false,
        message: '재부착할 Codex 작업을 찾지 못했습니다.',
      });
      return;
    }

    attachCodexExecutionSubscriber(execution, response);
    broadcastCodexExecutionEvent(execution, {
      type: 'attached',
      requestId,
      completed: execution.completed,
    });
    return;
  }

  const cancelMatch = requestUrl.pathname.match(/^\/api\/codex-live\/jobs\/([^/]+)\/cancel$/);
  if (request.method === 'POST' && cancelMatch) {
    const requestId = decodeURIComponent(cancelMatch[1]);
    const activeExecution = activeCodexExecutions.get(requestId);

    if (!activeExecution) {
      sendJson(response, 404, {
        cancelled: false,
        message: '실행 중인 Codex 작업을 찾지 못했습니다.',
      });
      return;
    }

    try {
      logRunner("received cancel request for requestId=" + requestId + "; forwarding SIGTERM to child pid=" + (activeExecution.child.pid ?? "unknown"));
      activeExecution.child.kill('SIGTERM');
      setTimeout(() => {
        const current = activeCodexExecutions.get(requestId);

        if (current?.child && !current.child.killed) {
          current.child.kill('SIGKILL');
        }
      }, 3000).unref?.();
      sendJson(response, 200, {
        cancelled: true,
      });
    } catch (error) {
      sendJson(response, 500, {
        cancelled: false,
        message: error instanceof Error ? error.message : 'Codex 작업 취소에 실패했습니다.',
      });
    }
    return;
  }

  sendJson(response, 404, {
    message: '지원하지 않는 경로입니다.',
  });
});

server.listen(port, host, () => {
  void writeHeartbeat().catch(() => {
    // noop
  });
  const heartbeatTimer = setInterval(() => {
    void writeHeartbeat().catch(() => {
      // noop
    });
  }, 10_000);
  heartbeatTimer.unref();
  logRunner("listening on http://" + host + ":" + port + "; pid=" + process.pid + "; ppid=" + process.ppid + "; startedAt=" + startedAt + "; logFile=" + runnerLogFile);
});
