import { spawn, spawnSync } from 'node:child_process';
import { existsSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';

import type {
  PromptTurnInput,
  PromptTurnProviderTelemetry,
  PromptTurnResult,
  PromptTurnRunner,
  PromptTurnRunnerCapabilities,
} from '../../application/ports/prompt-turn-runner.js';
import {
  appendProviderTelemetryBestEffort,
  normalizeCodexTelemetryFromJsonl,
} from './provider-telemetry.js';
// cspell:ignore xhigh

const DEFAULT_TIMEOUT_MS = 600_000;
const CODEX_TIMEOUT_MS_ENV = 'PROMPT_LANGUAGE_CODEX_TIMEOUT_MS';
const CODEX_REASONING_EFFORT_ENV = 'PROMPT_LANGUAGE_CODEX_REASONING_EFFORT';
const SKILL_WRAPPER_ENV = 'PROMPT_LANGUAGE_SKILL_PROMPT_WRAPPER';
const CODEX_SKILL_WRAPPER_ENV = 'PROMPT_LANGUAGE_CODEX_SKILL_PROMPT_WRAPPER';
const VALID_CODEX_REASONING_EFFORTS = new Set([
  'none',
  'minimal',
  'low',
  'medium',
  'high',
  'xhigh',
]);

function quotePowerShellArg(value: string): string {
  return `'${String(value).replace(/'/g, "''")}'`;
}

function buildCodexPowerShellCommand(args: readonly string[]): string {
  return `& codex ${args.map(quotePowerShellArg).join(' ')}`;
}

function readPositiveIntEnv(name: string): number | undefined {
  const value = process.env[name]?.trim();
  if (!value) return undefined;
  const parsed = Number.parseInt(value, 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}

function readCodexReasoningEffort(): string | undefined {
  const value = process.env[CODEX_REASONING_EFFORT_ENV]?.trim().toLowerCase();
  return value && VALID_CODEX_REASONING_EFFORTS.has(value) ? value : undefined;
}

function readBooleanEnv(name: string): boolean | undefined {
  const value = process.env[name]?.trim().toLowerCase();
  if (value == null || value === '') return undefined;
  if (value === '1' || value === 'true' || value === 'on') return true;
  if (value === '0' || value === 'false' || value === 'off') return false;
  return undefined;
}

function useSkillAwarePromptWrapper(): boolean {
  const harnessValue = readBooleanEnv(CODEX_SKILL_WRAPPER_ENV);
  if (harnessValue != null) {
    return harnessValue;
  }
  return readBooleanEnv(SKILL_WRAPPER_ENV) ?? true;
}

function resolveWindowsCodexCommandPrefix(): [string, string[]] {
  try {
    const result = spawnSync('where.exe', ['codex.cmd'], {
      encoding: 'utf-8',
      stdio: ['ignore', 'pipe', 'ignore'],
      windowsHide: true,
    });
    const shimPath = result.stdout
      .split(/\r?\n/)
      .map((line) => line.trim())
      .find(Boolean);
    if (result.status === 0 && shimPath) {
      const shimDir = dirname(shimPath);
      const bundledNode = join(shimDir, 'node.exe');
      const entrypoint = join(shimDir, 'node_modules', '@openai', 'codex', 'bin', 'codex.js');
      if (existsSync(entrypoint)) {
        return [existsSync(bundledNode) ? bundledNode : 'node', [entrypoint]];
      }
    }
  } catch {
    // Fall back to the PowerShell launcher below.
  }

  return ['powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command']];
}

function codexBinaryCommand(...args: string[]): [string, ...string[]] {
  if (process.platform !== 'win32') {
    return ['codex', ...args];
  }

  const [command, prefixArgs] = resolveWindowsCodexCommandPrefix();
  if (command === 'powershell') {
    return [command, ...prefixArgs, buildCodexPowerShellCommand(args)];
  }
  return [command, ...prefixArgs, ...args];
}

function terminateCodexProcessTree(child: ReturnType<typeof spawn>): void {
  try {
    child.kill();
  } catch {
    // ignore termination failures
  }

  if (process.platform === 'win32' && child.pid !== undefined) {
    try {
      spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
        stdio: 'ignore',
        windowsHide: true,
      });
    } catch {
      // ignore taskkill failures and rely on the direct kill attempt above
    }
  }

  child.stdin?.destroy();
  child.stdout?.destroy();
  child.stderr?.destroy();
}

function readAssistantText(outputFile: string, fallback?: string): string | undefined {
  try {
    return readFileSync(outputFile, 'utf-8').trim() || undefined;
  } catch {
    return fallback?.trim() ?? undefined;
  }
}

export function buildCodexPrompt(prompt: string): string {
  if (!useSkillAwarePromptWrapper()) {
    return prompt;
  }

  return [
    'You are executing a prompt-language flow step, not doing open-ended coding.',
    'Treat the text below as the active workflow state and current instruction.',
    'Rules:',
    '- Work directly in the current workspace when the instruction requires file or repository changes.',
    '- Do not stop after describing what you would do. Perform the requested edits, reads, and checks now.',
    '- If a relevant host or repo skill is already available, use it rather than reinventing the workflow.',
    '- If a skill or instruction includes prompt-language DSL, exact fixture strings, or literal file contents, preserve them exactly instead of paraphrasing them into prose.',
    '- Preserve exact fixture strings, filenames, and file contents from the workflow. Do not redact, mask, or paraphrase them.',
    '- If a step says a file must contain an exact value, write that exact value with no extra text.',
    '- Prefer minimal changes that satisfy the current step.',
    '- After completing the step, reply with one short line only.',
    '',
    prompt,
  ].join('\n');
}

function codexStatus(exitCode: number): PromptTurnProviderTelemetry['status'] {
  if (exitCode === 0) return 'ok';
  if (exitCode === 124) return 'timeout';
  return 'error';
}

export class CodexPromptTurnRunner implements PromptTurnRunner {
  readonly capabilities: PromptTurnRunnerCapabilities = {
    externalProcess: true,
    terminate: true,
    cwdOverride: true,
    modelPassThrough: true,
    stateDirPolling: true,
    inProcessExecution: false,
  };

  async run(input: PromptTurnInput): Promise<PromptTurnResult> {
    const outputFile = join(
      tmpdir(),
      `codex-last-message-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`,
    );
    const args = this.buildArgs(input, outputFile);
    const timeoutMs = readPositiveIntEnv(CODEX_TIMEOUT_MS_ENV) ?? DEFAULT_TIMEOUT_MS;
    const [command, ...commandArgs] = codexBinaryCommand(...args);
    const startedAt = Date.now();

    return await new Promise<PromptTurnResult>((resolve) => {
      const child = spawn(command, commandArgs, {
        cwd: input.cwd,
        windowsHide: true,
        stdio: ['pipe', 'pipe', 'pipe'],
      });

      let stdout = '';
      let stderr = '';
      let settled = false;
      let timedOut = false;

      const settle = (result: PromptTurnResult): void => {
        if (settled) return;
        settled = true;
        if (timer !== undefined) clearTimeout(timer);
        void (async () => {
          await appendProviderTelemetryBestEffort(input.cwd, {
            timestamp: new Date().toISOString(),
            ...result.providerTelemetry!,
          });
          resolve(result);
        })().finally(() => {
          try {
            rmSync(outputFile, { force: true });
          } catch {
            // ignore cleanup failures
          }
        });
      };

      const buildResult = (exitCode: number): PromptTurnResult => {
        const fallback = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
        const parsedTelemetry = normalizeCodexTelemetryFromJsonl(stdout);
        const providerTelemetry: PromptTurnProviderTelemetry = {
          provider: 'codex',
          requestedModel: input.model,
          actualModel: input.model,
          status: codexStatus(exitCode),
          exitCode,
          tokenUsage: parsedTelemetry.tokenUsage,
          duration: {
            totalMs: parsedTelemetry.duration?.totalMs ?? Date.now() - startedAt,
            firstTokenMs: parsedTelemetry.duration?.firstTokenMs,
          },
          retryCount: 0,
          estimatedCostUsd: null,
          metadata: parsedTelemetry.metadata,
        };
        return {
          exitCode,
          assistantText: readAssistantText(outputFile, fallback),
          providerTelemetry,
        };
      };

      const timer =
        timeoutMs > 0
          ? setTimeout(() => {
              timedOut = true;
              terminateCodexProcessTree(child);
            }, timeoutMs)
          : undefined;

      child.stdout?.setEncoding('utf8');
      child.stdout?.on('data', (chunk: string) => {
        stdout += chunk;
      });

      child.stderr?.setEncoding('utf8');
      child.stderr?.on('data', (chunk: string) => {
        stderr += chunk;
      });

      child.once('error', (error) => {
        stderr += error.message;
        settle(buildResult(1));
      });

      child.once('close', (code) => {
        if (timedOut) {
          const timedOutMessage = `Codex timed out after ${timeoutMs}ms waiting for output.`;
          const fallback = [stdout.trim(), stderr.trim(), timedOutMessage]
            .filter(Boolean)
            .join('\n');
          settle({
            exitCode: 124,
            assistantText: readAssistantText(outputFile, fallback),
            providerTelemetry: {
              provider: 'codex',
              requestedModel: input.model,
              actualModel: input.model,
              status: 'timeout',
              exitCode: 124,
              duration: {
                totalMs: Date.now() - startedAt,
              },
              retryCount: 0,
              estimatedCostUsd: null,
            },
          });
          return;
        }

        settle(buildResult(code ?? 1));
      });

      try {
        child.stdin?.end(buildCodexPrompt(input.prompt));
      } catch {
        child.stdin?.destroy();
      }
    });
  }

  buildArgs(input: PromptTurnInput, outputFile: string): string[] {
    const args = [
      'exec',
      '--json',
      '--dangerously-bypass-approvals-and-sandbox',
      '--skip-git-repo-check',
      '--output-last-message',
      outputFile,
      '-C',
      input.cwd,
    ];

    if (input.model != null) {
      args.push('--model', input.model);
    }

    const reasoningEffort = readCodexReasoningEffort();
    if (reasoningEffort !== undefined) {
      args.push('-c', `model_reasoning_effort="${reasoningEffort}"`);
    }

    args.push('-');
    return args;
  }
}
