import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { randomUUID } from 'node:crypto';

import type { NormalizedAgentEvent } from '../agent-events';
import { normalizeCodexJsonLine } from '../agent-events';
import { prepareCodexWorkspaceFiles } from '../codex-runtime';
import { env } from '../env';
import type { AdapterFactory, TurnResult } from './agent-runtime';
import {
  buildMarkedCommand,
  killBoxProcessesByMarker,
  streamExecInContainer,
} from './docker';
import type { AdapterWorkspace } from './provider';
import {
  codexModelArgs,
  isCodexLoginProfile,
  providerEnvironment,
} from './provider';

// Reused verbatim from the worker: normalize + pretty-print an auth JSON blob
// before writing it with owner-only permissions.
const writeJsonFile = async (filePath: string, content: string) => {
  let normalized = content.trim();
  try {
    normalized = `${JSON.stringify(JSON.parse(normalized), null, 2)}\n`;
  } catch {
    throw new Error('Codex auth JSON is not valid JSON.');
  }
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, normalized, { mode: 0o600 });
};

const isNoise = (line: string) => {
  const trimmed = line.trim();
  return (
    !trimmed ||
    trimmed === 'Reading additional input from stdin...' ||
    trimmed.includes('`[features].codex_hooks` is deprecated')
  );
};

export const createCodexAdapter: AdapterFactory = (deps) => {
  const execStream = deps?.execStream ?? streamExecInContainer;

  const prepareAuth = async (workspace: AdapterWorkspace) => {
    // API-key profiles (e.g. OpenAI api_key) authenticate purely via
    // OPENAI_API_KEY from providerEnvironment; only ChatGPT-login/JSON-snapshot
    // profiles need a Codex `auth.json` on disk. Mirror the Claude adapter,
    // which early-returns for non-OAuth profiles.
    if (!isCodexLoginProfile(workspace.claim)) return;
    const secret = workspace.claim.aiProviderProfile?.secret;
    if (!secret) {
      throw new Error('Codex auth profile is missing auth.json contents.');
    }
    await prepareCodexWorkspaceFiles({
      workdir: workspace.workdir,
      repoDir: workspace.repoDir,
    });
    const codexAuthPath = path.join(workspace.workdir, '.codex', 'auth.json');
    await writeJsonFile(codexAuthPath, secret);

    // Also seed OpenCode's auth location with the saved JSON for forward
    // compatibility if this profile later runs through OpenCode directly.
    const openCodeAuthPath = path.join(
      workspace.workdir,
      '.local',
      'share',
      'opencode',
      'auth.json',
    );
    await writeJsonFile(openCodeAuthPath, secret);
  };

  const runTurn = async (
    workspace: AdapterWorkspace,
    prompt: string,
    onEvent: (event: NormalizedAgentEvent) => Promise<void>,
  ): Promise<TurnResult> => {
    workspace.runtime = 'codex';
    workspace.turnMarker = `spoon-turn-${randomUUID()}`;
    const marker = workspace.turnMarker;

    const outputFileName = `last-message-${workspace.claim.job._id}.txt`;
    const outputFileHostPath = path.join(
      workspace.workdir,
      '.codex',
      outputFileName,
    );
    const outputFileContainerPath = path.posix.join(
      workspace.containerHome,
      '.codex',
      outputFileName,
    );
    const codexArgv = workspace.codexSessionId
      ? [
          'codex',
          'exec',
          'resume',
          '--json',
          ...codexModelArgs(workspace.claim),
          '--dangerously-bypass-approvals-and-sandbox',
          '--output-last-message',
          outputFileContainerPath,
          workspace.codexSessionId,
          prompt,
        ]
      : [
          'codex',
          'exec',
          '--json',
          ...codexModelArgs(workspace.claim),
          '--dangerously-bypass-approvals-and-sandbox',
          '--output-last-message',
          outputFileContainerPath,
          '--cd',
          workspace.containerRepo,
          prompt,
        ];
    const command = buildMarkedCommand(marker, codexArgv);

    const aiEnv = providerEnvironment(workspace.claim, workspace.containerHome);
    const secretEnv = Object.fromEntries(
      workspace.claim.secrets.map((secret) => [secret.name, secret.value]),
    );

    let capturedSessionId: string | undefined;
    let capturedError: string | undefined;

    // execa's own `timeout` only kills the local docker-exec client, not the
    // in-box process group. Schedule an explicit marker kill on timeout and
    // clear it once the exec resolves.
    const killTimer = setTimeout(() => {
      void killBoxProcessesByMarker({
        containerName: workspace.boxName,
        marker,
      });
    }, env.jobTimeoutMs);

    let result;
    try {
      result = await execStream({
        containerName: workspace.boxName,
        containerCwd: workspace.containerRepo,
        command,
        environment: { ...aiEnv, ...secretEnv },
        redact: workspace.redact,
        timeoutMs: env.jobTimeoutMs,
        onStdoutLine: async (line) => {
          for (const event of normalizeCodexJsonLine(line)) {
            if (event.kind === 'session') capturedSessionId = event.sessionId;
            if (event.kind === 'error') capturedError = event.message;
            await onEvent(event);
          }
        },
        onStderrLine: async (line) => {
          if (isNoise(line)) return;
          await onEvent({ kind: 'status', status: line });
        },
      });
    } finally {
      clearTimeout(killTimer);
    }

    if (result.exitCode !== 0) {
      capturedError = result.output;
    }

    let finalMessage: string | undefined;
    try {
      const lastMessage = await readFile(outputFileHostPath, 'utf8');
      if (lastMessage.trim()) finalMessage = lastMessage.trim();
    } catch (error) {
      const code = error && typeof error === 'object' ? 'code' in error : false;
      if (!code || (error as { code?: string }).code !== 'ENOENT') {
        throw error;
      }
    }

    return { finalMessage, sessionId: capturedSessionId, error: capturedError };
  };

  const abort = async (workspace: AdapterWorkspace) => {
    if (!workspace.turnMarker) return;
    await killBoxProcessesByMarker({
      containerName: workspace.boxName,
      marker: workspace.turnMarker,
    });
  };

  return { name: 'codex', prepareAuth, runTurn, abort };
};
