/**
 * Shell Executor Utilities
 *
 * Cross-platform shell execution utilities for CCS.
 */

import { spawn, spawnSync, ChildProcess, type SpawnOptions } from 'child_process';
import { ErrorManager } from './error-manager';
import { getWebSearchHookEnv } from './websearch-manager';
import { wireChildProcessSignals } from './signal-forwarder';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import SharedManager from '../management/shared-manager';

/**
 * Strip ANTHROPIC_* env vars from an environment object.
 * Used for account/default profiles to prevent stale proxy config from
 * interfering with native Claude API routing.
 */
export function stripAnthropicEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
  const result: NodeJS.ProcessEnv = {};
  for (const key of Object.keys(env)) {
    if (!key.startsWith('ANTHROPIC_')) {
      result[key] = env[key];
    }
  }
  return result;
}

const ANTHROPIC_ROUTING_ENV_KEYS = [
  'ANTHROPIC_BASE_URL',
  'ANTHROPIC_AUTH_TOKEN',
  'ANTHROPIC_API_KEY',
];
const ANTHROPIC_ROUTING_ENV_KEY_SET = new Set(ANTHROPIC_ROUTING_ENV_KEYS);
const ANTHROPIC_MODEL_ENV_KEYS = [
  'ANTHROPIC_MODEL',
  'ANTHROPIC_DEFAULT_OPUS_MODEL',
  'ANTHROPIC_DEFAULT_SONNET_MODEL',
  'ANTHROPIC_DEFAULT_HAIKU_MODEL',
  'ANTHROPIC_SMALL_FAST_MODEL',
];
const TMUX_SYNC_ENV_KEYS = [
  'CLAUDE_CONFIG_DIR',
  'CCS_PROFILE_TYPE',
  'CCS_WEBSEARCH_SKIP',
  'CCS_STRIP_INHERITED_ANTHROPIC_ENV',
  'CLAUDE_CODE_MAX_OUTPUT_TOKENS',
  ...ANTHROPIC_MODEL_ENV_KEYS,
  ...ANTHROPIC_ROUTING_ENV_KEYS,
];

/**
 * Strip inherited Anthropic routing/auth env while preserving model intent.
 * Used for nested settings-profile Claude launches where `--settings` already
 * defines the provider transport and the parent process should only lend model
 * defaults or effort hints.
 */
export function stripAnthropicRoutingEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
  const result: NodeJS.ProcessEnv = {};
  for (const key of Object.keys(env)) {
    if (!ANTHROPIC_ROUTING_ENV_KEY_SET.has(key.toUpperCase())) {
      result[key] = env[key];
    }
  }
  return result;
}

function syncTmuxNestedSessionEnv(env: NodeJS.ProcessEnv, profileType: string | undefined): void {
  if (!process.env.TMUX) {
    return;
  }

  const nestedSessionEnv =
    profileType === 'account' || profileType === 'default'
      ? stripAnthropicEnv(env)
      : profileType === 'settings'
        ? stripAnthropicRoutingEnv(env)
        : env;

  for (const key of TMUX_SYNC_ENV_KEYS) {
    try {
      const value = nestedSessionEnv[key];
      if (value !== undefined) {
        spawnSync('tmux', ['setenv', key, value], { stdio: 'ignore' });
      } else {
        spawnSync('tmux', ['setenv', '-u', key], { stdio: 'ignore' });
      }
    } catch {
      // tmux setenv can fail if not in a tmux session; safe to ignore
    }
  }
}

/**
 * Strip inherited browser attach/runtime env vars from a process environment.
 *
 * Browser capability is opt-in and launch-scoped. Stale CCS_BROWSER_* values
 * from the parent process must never bleed into a browser-off child launch.
 */
export function stripBrowserEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
  const result: NodeJS.ProcessEnv = {};
  for (const key of Object.keys(env)) {
    if (!key.toUpperCase().startsWith('CCS_BROWSER_')) {
      result[key] = env[key];
    }
  }
  return result;
}

/**
 * Strip Claude Code nested-session guard env var from a process environment.
 *
 * Note: Windows env keys are case-insensitive, so remove case-insensitively
 * to avoid missing variants like `claudecode`.
 */
export function stripClaudeCodeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
  const result: NodeJS.ProcessEnv = {};
  for (const key of Object.keys(env)) {
    if (key.toUpperCase() !== 'CLAUDECODE') {
      result[key] = env[key];
    }
  }
  return result;
}

/**
 * Strip Codex session-scoped env vars before launching a nested Codex process.
 *
 * Keep real user config such as CODEX_HOME intact. Only remove the known
 * session/runtime metadata exported by the current Codex host process.
 */
export function stripCodexSessionEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
  const sessionKeys = new Set(['CODEX_CI', 'CODEX_MANAGED_BY_BUN', 'CODEX_THREAD_ID']);
  const result: NodeJS.ProcessEnv = {};
  for (const key of Object.keys(env)) {
    const upperKey = key.toUpperCase();
    if (sessionKeys.has(upperKey)) {
      continue;
    }
    result[key] = env[key];
  }
  return result;
}

/**
 * Resolve CCS-managed environment overrides for Claude launch.
 * - preferences.auto_update: false -> DISABLE_AUTOUPDATER=1
 */
export function getClaudeLaunchEnvOverrides(): NodeJS.ProcessEnv {
  try {
    const config = loadOrCreateUnifiedConfig();
    if (config.preferences?.auto_update === false) {
      return { DISABLE_AUTOUPDATER: '1' };
    }
  } catch {
    // Config read errors should never block Claude launch.
  }
  return {};
}

/**
 * Escape arguments for shell execution (cross-platform)
 *
 * IMPORTANT: On Windows, spawn({ shell: true }) uses cmd.exe by default,
 * NOT PowerShell. cmd.exe does NOT recognize single quotes as string delimiters.
 * We must use double quotes for cmd.exe compatibility.
 */
export function escapeShellArg(arg: string): string {
  const isWindows = process.platform === 'win32';

  if (isWindows) {
    // cmd.exe: Use double quotes, escape inner double quotes by doubling them
    // cmd.exe interprets "" as escaped double quote inside quoted string
    // Strip newlines/tabs that can break cmd.exe parsing
    return (
      '"' +
      String(arg)
        .replace(/[\r\n\t]/g, ' ') // Replace newlines/tabs with space
        .replace(/%/g, '%%') // Escape percent signs
        .replace(/\^/g, '^^') // Escape carets
        .replace(/!/g, '^^!') // Escape exclamation marks (delayed expansion)
        .replace(/"/g, '""') + // Escape quotes
      '"'
    );
  } else {
    // Unix/macOS: Double quotes with escaped inner quotes
    return '"' + String(arg).replace(/"/g, '\\"') + '"';
  }
}

/**
 * Return the shell that matches escapeShellArg() quoting semantics.
 *
 * On Windows, prefer ComSpec over a bare `cmd.exe` so escaped wrapper launches
 * keep the same shell contract without depending on PATH lookup.
 */
export function getWindowsEscapedCommandShell(): SpawnOptions['shell'] {
  if (process.platform !== 'win32') {
    return true;
  }

  return process.env.ComSpec || process.env.COMSPEC || 'cmd.exe';
}

/**
 * Execute Claude CLI with unified spawn logic
 */
export function execClaude(
  claudeCli: string,
  args: string[],
  envVars: NodeJS.ProcessEnv | null = null
): void {
  const isWindows = process.platform === 'win32';
  const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli);
  const needsShell = isWindows && /\.(cmd|bat)$/i.test(claudeCli);

  // Get WebSearch hook config env vars
  const webSearchEnv = getWebSearchHookEnv();
  const claudeLaunchEnv = getClaudeLaunchEnvOverrides();

  // Strip inherited ANTHROPIC_* when the launch should not reuse parent routing.
  // Account/default profiles need full isolation from prior proxy sessions.
  // Settings profiles can selectively strip only routing/auth when `--settings`
  // already carries the provider source of truth but the parent model intent
  // should still flow into nested Team/subagent launches.
  const profileType = envVars?.CCS_PROFILE_TYPE;
  const stripInheritedAnthropicEnv = profileType === 'account' || profileType === 'default';
  const stripInheritedAnthropicRoutingEnv = envVars?.CCS_STRIP_INHERITED_ANTHROPIC_ENV === '1';
  const inheritedEnv = stripInheritedAnthropicEnv
    ? stripAnthropicEnv(process.env)
    : stripInheritedAnthropicRoutingEnv
      ? stripAnthropicRoutingEnv(process.env)
      : process.env;
  const baseEnv = stripBrowserEnv(inheritedEnv);

  // Prepare environment (merge with base env if envVars provided)
  const mergedEnv = envVars
    ? { ...baseEnv, ...claudeLaunchEnv, ...envVars, ...webSearchEnv }
    : { ...baseEnv, ...claudeLaunchEnv, ...webSearchEnv };
  const effectiveMergedEnv = stripInheritedAnthropicRoutingEnv
    ? stripAnthropicRoutingEnv(mergedEnv)
    : mergedEnv;

  // Strip Claude Code nested session guard env var to allow CCS delegation
  // (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions)
  const env = stripClaudeCodeEnv(effectiveMergedEnv);

  if (profileType !== 'account') {
    try {
      new SharedManager().normalizeSharedPluginMetadataPathsLocked(env.CLAUDE_CONFIG_DIR);
    } catch {
      // Best-effort normalization should never block Claude launch.
    }
  }

  // Keep tmux teammate panes aligned with the nested-safe Claude runtime env
  // rather than the tmux server's original shell environment.
  syncTmuxNestedSessionEnv(env, profileType);

  let child: ChildProcess;
  if (isPowerShellScript) {
    child = spawn(
      'powershell.exe',
      ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', claudeCli, ...args],
      {
        stdio: 'inherit',
        windowsHide: true,
        env,
      }
    );
  } else if (needsShell) {
    // When shell needed: concatenate into string to avoid DEP0190 warning
    const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
    child = spawn(cmdString, {
      stdio: 'inherit',
      windowsHide: true,
      shell: getWindowsEscapedCommandShell(),
      env,
    });
  } else {
    // When no shell needed: use array form (faster, no shell overhead)
    child = spawn(claudeCli, args, {
      stdio: 'inherit',
      windowsHide: true,
      env,
    });
  }

  wireChildProcessSignals(child, async (err: NodeJS.ErrnoException) => {
    if (err.code === 'EACCES') {
      console.error(`[X] Claude CLI is not executable: ${claudeCli}`);
      console.error('    Check file permissions and executable bit.');
    } else if (err.code === 'ENOENT') {
      if (isPowerShellScript) {
        console.error('[X] PowerShell executable not found (required for .ps1 wrapper launch).');
        console.error('    Ensure powershell.exe is available in PATH.');
      } else if (needsShell) {
        console.error('[X] Windows command shell not found for Claude wrapper launch.');
        console.error('    Ensure cmd.exe is available and accessible.');
      } else {
        await ErrorManager.showClaudeNotFound();
      }
    } else {
      console.error(`[X] Failed to start Claude CLI (${claudeCli}): ${err.message}`);
    }
    process.exit(1);
  });
}
