import { spawn, execSync } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { getConfig } from '../config.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const AGENT_ROOT = join(__dirname, '..');

/**
 * Resolve the active workspace directory.
 * Checks config for workspaceRoot + activeWorkspace, falls back to workspaces/default.
 */
function getActiveWorkspaceDir() {
  try {
    const config = getConfig();
    const root = config.workspaceRoot || join(AGENT_ROOT, 'workspaces');
    const activeId = config.activeWorkspace || 'default';
    return join(root, activeId);
  } catch {
    return join(AGENT_ROOT, 'workspaces', 'default');
  }
}

export const name = 'codex';

export const modelMap = {
  fast: 'gpt-5-codex-mini',
  balanced: 'gpt-5.2-codex',
  powerful: 'gpt-5.3-codex'
};

function resolveModel(model) {
  const key = String(model || 'balanced').trim().toLowerCase();
  if (key === 'fast' || key === 'balanced' || key === 'powerful') {
    return modelMap[key];
  }
  return String(model || modelMap.balanced);
}

// Resolved full path to the CLI binary (set by isAvailable)
let resolvedBin = 'codex';

function spawnCodex(args, options = {}) {
  if (process.platform === 'win32') {
    return spawn('cmd.exe', ['/d', '/s', '/c', resolvedBin, ...args], {
      ...options,
      shell: false
    });
  }
  return spawn(resolvedBin, args, {
    ...options,
    shell: false
  });
}

function appendTextFromContent(content, textParts) {
  if (!content) return;

  if (typeof content === 'string') {
    textParts.push(content);
    return;
  }

  if (Array.isArray(content)) {
    for (const block of content) {
      if (typeof block === 'string') {
        textParts.push(block);
      } else if (block && typeof block === 'object') {
        if (typeof block.text === 'string') textParts.push(block.text);
        if (typeof block.output_text === 'string') textParts.push(block.output_text);
      }
    }
  }
}

/**
 * Known event types that contain internal reasoning/thinking, not user-facing output.
 * These should be skipped to avoid leaking Codex's chain-of-thought into agent replies.
 *
 * Codex CLI event types observed:
 *   task_started              — metadata, no text
 *   agent_reasoning           — internal chain-of-thought (SKIP)
 *   agent_reasoning_section_break — section divider in reasoning (SKIP)
 *   agent_message             — actual assistant response (EXTRACT)
 *   token_count               — usage stats, no text
 *   item.completed            — completed items (check sub-type)
 *   response.completed        — final response
 *   turn.completed            — final turn
 */
const SKIP_EVENT_TYPES = new Set([
  // Codex-specific reasoning
  'agent_reasoning', 'agent_reasoning_section_break',
  // Generic reasoning
  'reasoning', 'thinking', 'response.thinking',
  'response.reasoning', 'content_block_thinking',
  // Metadata-only events
  'task_started', 'token_count', 'message_delta'
]);

function extractTextAndError(raw) {
  const event = raw?.msg || raw;
  const textParts = [];
  let error = null;

  if (typeof event === 'string') {
    return { text: event, error: null };
  }

  if (!event || typeof event !== 'object') {
    return { text: '', error: null };
  }

  const evtType = String(event.type || '').toLowerCase();

  // Skip events that are purely internal reasoning/thinking/metadata
  if (SKIP_EVENT_TYPES.has(evtType)) {
    return { text: '', error: null };
  }

  // ── agent_message: Codex's primary response event ──
  if (evtType === 'agent_message') {
    if (typeof event.message === 'string') textParts.push(event.message);
    if (typeof event.text === 'string') textParts.push(event.text);
    if (typeof event.content === 'string') textParts.push(event.content);
    appendTextFromContent(event.content, textParts);
    appendTextFromContent(event.message?.content, textParts);
  }

  // ── result / output_text: direct result fields ──
  if (typeof event.result === 'string') textParts.push(event.result);
  if (typeof event.output_text === 'string') textParts.push(event.output_text);

  // ── response.output and response.content: authoritative output fields ──
  appendTextFromContent(event.response?.output, textParts);
  appendTextFromContent(event.response?.content, textParts);

  // ── item.completed: extract only non-reasoning items ──
  if (evtType === 'item.completed' && event.item && typeof event.item === 'object') {
    const itemType = String(event.item.type || '').toLowerCase();
    if (itemType !== 'reasoning' && itemType !== 'thinking') {
      if (typeof event.item.text === 'string') textParts.push(event.item.text);
      if (typeof event.item.output_text === 'string') textParts.push(event.item.output_text);
      appendTextFromContent(event.item.content, textParts);
    }
  }

  // ── response.completed / turn.completed: final output ──
  if (evtType === 'response.completed' || evtType === 'turn.completed') {
    appendTextFromContent(event.output?.content, textParts);
    appendTextFromContent(event.output, textParts);
    appendTextFromContent(event.result?.content, textParts);
    if (typeof event.text === 'string') textParts.push(event.text);
  }

  // ── Fallback: for any other event type not explicitly handled ──
  // Only extract if we haven't found text yet, and only from safe fields
  if (textParts.length === 0) {
    if (evtType === 'message' || evtType === 'text' || evtType === 'output' || evtType === 'assistant') {
      if (typeof event.message === 'string') textParts.push(event.message);
      if (typeof event.text === 'string') textParts.push(event.text);
      appendTextFromContent(event.content, textParts);
      appendTextFromContent(event.message?.content, textParts);
    }
  }

  // ── Errors ──
  if (evtType === 'error' || evtType === 'turn.failed') {
    error = event.error?.message || event.message || event.error || 'Codex turn failed';
  }

  return { text: textParts.join(''), error };
}

export function isAvailable() {
  try {
    if (process.platform === 'win32') {
      resolvedBin = execSync('where codex.cmd 2>nul || where codex', { encoding: 'utf-8' }).trim().split(/\r?\n/)[0];
    } else {
      resolvedBin = execSync('which codex', { encoding: 'utf-8' }).trim();
    }
    return true;
  } catch {
    return false;
  }
}

/**
 * Run Codex CLI in non-interactive (exec) mode and return the result.
 *
 * @param {string} prompt
 * @param {object} opts
 * @param {string} opts.model
 * @param {number} opts.timeout - ms before killing
 * @param {string} opts.sandbox - sandbox level: 'read-only' | 'workspace-write' | 'danger-full-access' (default: 'workspace-write')
 * @param {boolean} opts.sandboxed - legacy flag; false → 'danger-full-access' (overrides sandbox)
 * @param {string} opts.cwd - working directory for Codex process (default: AGENT_ROOT)
 * @param {function} opts.onStream - callback(partialText) for streaming updates
 */
export async function ask(prompt, { model = 'balanced', timeout = 600_000, sandbox = 'workspace-write', sandboxed, cwd, onStream } = {}) {
  return new Promise((resolve, reject) => {
    const selectedModel = resolveModel(model);

    // Resolve sandbox level: legacy sandboxed=false means full access
    let sandboxLevel = sandbox;
    if (sandboxed === false) sandboxLevel = 'danger-full-access';

    // Pass prompt via stdin (using '-') to avoid shell escaping issues on Windows
    const args = [
      'exec',
      '--json',
      '--sandbox', sandboxLevel,
      '--model', selectedModel,
      '-'
    ];

    const proc = spawnCodex(args, {
      // Use the active workspace directory as cwd so any files Codex writes land
      // in the correct workspace, not the repo root.
      cwd: cwd || getActiveWorkspaceDir(),
      env: { ...process.env },
      stdio: ['pipe', 'pipe', 'pipe']
    });

    let fullResult = '';
    let accumulatedText = '';
    let lastEventError = '';
    const seenEventTypes = new Set();
    let buffer = '';
    let killed = false;

    const timer = setTimeout(() => {
      killed = true;
      proc.kill('SIGTERM');
      setTimeout(() => proc.kill('SIGKILL'), 5000);
    }, timeout);

    proc.stdout.on('data', (chunk) => {
      buffer += chunk.toString();
      const lines = buffer.split('\n');
      buffer = lines.pop();

      for (const line of lines) {
        if (!line.trim()) continue;
        try {
          const parsed = JSON.parse(line);
          const evt = parsed?.msg || parsed;
          if (evt && typeof evt === 'object' && typeof evt.type === 'string') {
            seenEventTypes.add(evt.type);
          }
          const { text, error } = extractTextAndError(parsed);
          if (text) {
            accumulatedText += text;
            if (onStream) onStream(accumulatedText);
          }
          if (error) {
            lastEventError = String(error);
          }
        } catch {
          // Non-JSON line — may be plain text result
          if (line.trim()) {
            accumulatedText += line;
            if (onStream) onStream(accumulatedText);
          }
        }
      }
    });

    let stderr = '';
    proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });

    proc.on('close', (code) => {
      clearTimeout(timer);

      // Process any remaining buffer
      if (buffer.trim()) {
        try {
          const parsed = JSON.parse(buffer);
          const evt = parsed?.msg || parsed;
          if (evt && typeof evt === 'object' && typeof evt.type === 'string') {
            seenEventTypes.add(evt.type);
          }
          const { text, error } = extractTextAndError(parsed);
          if (text) accumulatedText += text;
          if (error) lastEventError = String(error);
        } catch {
          if (buffer.trim()) accumulatedText += buffer.trim();
        }
      }

      fullResult = accumulatedText.trim();

      if (killed) {
        reject(new Error(`Codex CLI timed out after ${timeout}ms`));
      } else if (!fullResult && lastEventError) {
        reject(new Error(`Codex CLI failed: ${lastEventError}\n${stderr}`.trim()));
      } else if (code !== 0 && !fullResult) {
        reject(new Error(`Codex CLI exited with code ${code}\n${stderr}`));
      } else if (code === 0 && !fullResult) {
        const typeSummary = seenEventTypes.size > 0 ? `\nEvent types: ${[...seenEventTypes].join(', ')}` : '';
        reject(new Error(`Codex CLI returned empty output${typeSummary}\n${stderr}`.trim()));
      } else {
        resolve(fullResult);
      }
    });

    proc.on('error', (err) => {
      clearTimeout(timer);
      reject(new Error(`Codex CLI spawn error: ${err.message}`));
    });

    // Write prompt to stdin and close
    proc.stdin.write(prompt);
    proc.stdin.end();
  });
}

/**
 * Chat-mode completion with controlled tool access.
 *
 * Chat mode (allowedTools set): uses read-only sandbox so Codex can read
 * files and search code but cannot write, execute, or deploy.
 *
 * Raw completion mode (allowedTools null): same read-only sandbox,
 * returns clean output for skills.
 *
 * @param {string} prompt
 * @param {object} opts
 * @param {string} opts.model
 * @param {number} opts.timeout
 * @param {function} opts.onStream
 * @param {string[]|null} opts.allowedTools - accepted for interface consistency
 */
export async function streamChat(prompt, { model = 'balanced', timeout = 300_000, onStream, allowedTools = null } = {}) {
  return ask(prompt, { model, timeout, sandbox: 'read-only', onStream });
}

/**
 * Spawn Codex in interactive mode (for meta-develop).
 * Returns the spawned child process.
 */
export function spawnInteractive({ cwd, model = 'balanced' } = {}) {
  const selectedModel = resolveModel(model);
  return spawnCodex([
    '--model', selectedModel,
    '--ask-for-approval', 'on-request',
    '--sandbox', 'workspace-write'
  ], {
    cwd: cwd || getActiveWorkspaceDir(),
    env: { ...process.env },
    stdio: ['pipe', 'pipe', 'pipe']
  });
}
