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

import type { NormalizedAgentEvent } from '../agent-events';
import { normalizeOpenCodeRunLine } from '../agent-events';
import { env } from '../env';
import type { AdapterFactory, TurnResult } from './agent-runtime';
import {
  buildMarkedCommand,
  killBoxProcessesByMarker,
  streamExecInContainer,
} from './docker';
import type { AdapterWorkspace } from './provider';
import { opencodeModel, providerEnvironment } from './provider';

// Normalize + pretty-print an auth JSON blob before writing it with owner-only
// permissions (mirrors the CodexAdapter helper).
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('OpenCode auth JSON is not valid JSON.');
  }
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, normalized, { mode: 0o600 });
};

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

  const prepareAuth = async (workspace: AdapterWorkspace) => {
    // API-key profiles authenticate purely via ANTHROPIC_API_KEY/OPENAI_API_KEY
    // in the environment; only `opencode_auth_json` profiles need auth.json on disk.
    const profile = workspace.claim.aiProviderProfile;
    if (profile?.authType !== 'opencode_auth_json') return;
    if (!profile.secret) {
      throw new Error('OpenCode auth profile is missing auth.json contents.');
    }
    const authPath = path.join(
      workspace.workdir,
      '.local',
      'share',
      'opencode',
      'auth.json',
    );
    await writeJsonFile(authPath, profile.secret);
  };

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

    // Flags verified against `opencode run --help` (opencode-ai@1.17.9):
    // `--format json`, `--model`, and `--session <id>` all exist.
    const argv = [
      'opencode',
      'run',
      '--format',
      'json',
      '--model',
      opencodeModel(workspace.claim),
      ...(workspace.opencodeSessionId
        ? ['--session', workspace.opencodeSessionId]
        : []),
      '--',
      prompt,
    ];
    const command = buildMarkedCommand(marker, argv);

    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;
    let finalMessage: 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 normalizeOpenCodeRunLine(line)) {
            if (event.kind === 'session') capturedSessionId = event.sessionId;
            if (event.kind === 'error') capturedError = event.message;
            if (event.kind === 'assistant_completed' && event.content) {
              finalMessage = event.content;
            }
            await onEvent(event);
          }
        },
        onStderrLine: async (line) => {
          if (!line.trim()) return;
          await onEvent({ kind: 'status', status: line });
        },
      });
    } finally {
      clearTimeout(killTimer);
    }

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

    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: 'opencode', prepareAuth, runTurn, abort };
};
