import { execSync, spawnSync } from 'child_process';
import { app } from 'electron';
import { chmodSync, existsSync, mkdirSync, readdirSync, realpathSync, statSync, writeFileSync } from 'fs';
import { delimiter, dirname, join } from 'path';

import { buildSessionTitleFromInput } from '../../common/sessionTitle';
import { buildEnvForConfig, getCurrentApiConfig, resolveCurrentApiConfig, resolveRawApiConfig } from './claudeSettings';
import { coworkLog } from './coworkLogger';
import {
  buildAnthropicMessagesUrl,
  buildGeminiGenerateContentUrl,
  CoworkModelProtocol,
  extractApiErrorSnippet,
} from './coworkModelApi';
import type { OpenAICompatProxyTarget } from './coworkOpenAICompatProxy';
import { appendPythonRuntimeToEnv } from './pythonRuntime';
import { isSystemProxyEnabled, resolveSystemProxyUrlForTargets } from './systemProxy';

function appendEnvPath(current: string | undefined, additions: string[]): string | undefined {
  const items = new Set<string>();

  for (const entry of additions) {
    if (entry) {
      items.add(entry);
    }
  }

  if (current) {
    for (const entry of current.split(delimiter)) {
      if (entry) {
        items.add(entry);
      }
    }
  }

  return items.size > 0 ? Array.from(items).join(delimiter) : current;
}

function hasCommandInEnv(command: string, env: Record<string, string | undefined>): boolean {
  const whichCmd = process.platform === 'win32' ? 'where' : 'which';
  try {
    const result = spawnSync(whichCmd, [command], {
      env: { ...env } as NodeJS.ProcessEnv,
      encoding: 'utf-8',
      timeout: 5000,
      windowsHide: process.platform === 'win32',
    });
    return result.status === 0;
  } catch {
    return false;
  }
}

let cachedElectronNodeRuntimePath: string | null = null;

function resolveElectronNodeRuntimePath(): string {
  if (!app.isPackaged || process.platform !== 'darwin') {
    return process.execPath;
  }

  try {
    const appName = app.getName();
    const frameworksDir = join(process.resourcesPath, '..', 'Frameworks');
    if (!existsSync(frameworksDir)) {
      return process.execPath;
    }

    const helperApps = readdirSync(frameworksDir)
      .filter((entry) => entry.startsWith(`${appName} Helper`) && entry.endsWith('.app'))
      .sort((a, b) => {
        const score = (name: string): number => {
          if (name === `${appName} Helper.app`) return 0;
          if (name === `${appName} Helper (Renderer).app`) return 1;
          if (name === `${appName} Helper (Plugin).app`) return 2;
          if (name === `${appName} Helper (GPU).app`) return 3;
          return 10;
        };
        return score(a) - score(b);
      });

    for (const helperApp of helperApps) {
      const helperExeName = helperApp.replace(/\.app$/, '');
      const helperExePath = join(frameworksDir, helperApp, 'Contents', 'MacOS', helperExeName);
      if (existsSync(helperExePath)) {
        coworkLog('INFO', 'resolveNodeShim', `Using Electron helper runtime for node shim: ${helperExePath}`);
        return helperExePath;
      }
    }
  } catch (error) {
    coworkLog('WARN', 'resolveNodeShim', `Failed to resolve Electron helper runtime: ${error instanceof Error ? error.message : String(error)}`);
  }

  return process.execPath;
}

export function getElectronNodeRuntimePath(): string {
  if (!cachedElectronNodeRuntimePath) {
    cachedElectronNodeRuntimePath = resolveElectronNodeRuntimePath();
  }
  return cachedElectronNodeRuntimePath;
}

/**
 * Cached user shell PATH. Resolved once and reused across calls.
 */
let cachedUserShellPath: string | null | undefined;

/**
 * Resolve the user's login shell PATH on macOS/Linux.
 * Packaged Electron apps on macOS don't inherit the user's shell profile,
 * so node/npm and other tools won't be in PATH unless we resolve it.
 */
function resolveUserShellPath(): string | null {
  if (cachedUserShellPath !== undefined) return cachedUserShellPath;

  if (process.platform === 'win32') {
    cachedUserShellPath = null;
    return null;
  }

  try {
    const shell = process.env.SHELL || '/bin/bash';
    // Prefer non-interactive login shell first to avoid potential side effects
    // from interactive startup scripts (which may launch extra GUI processes).
    const pathProbes = [
      `${shell} -lc 'echo __PATH__=$PATH'`,
    ];

    let resolved: string | null = null;
    for (const probe of pathProbes) {
      try {
        const result = execSync(probe, {
          encoding: 'utf-8',
          timeout: 5000,
          env: { ...process.env },
        });
        const match = result.match(/__PATH__=(.+)/);
        if (match?.[1]) {
          resolved = match[1].trim();
          break;
        }
      } catch {
        // Try next probe.
      }
    }
    cachedUserShellPath = resolved;
  } catch (error) {
    console.warn('[coworkUtil] Failed to resolve user shell PATH:', error);
    cachedUserShellPath = null;
  }

  return cachedUserShellPath;
}

/**
 * Cached Windows registry PATH. Resolved once and reused.
 */
let cachedWindowsRegistryPath: string | null | undefined;

function readWindowsRegistryPathValue(registryKey: string): string {
  try {
    const output = execSync(`reg query "${registryKey}" /v Path`, {
      encoding: 'utf-8',
      timeout: 8000,
      windowsHide: true,
    });

    for (const line of output.split(/\r?\n/)) {
      const match = line.match(/^\s*Path\s+REG_\w+\s+(.+)$/i);
      if (match?.[1]) {
        return match[1].trim();
      }
    }
  } catch {
    // Ignore missing keys or access-denied errors.
  }

  return '';
}

/**
 * Resolve the latest PATH from the Windows registry (Machine + User).
 *
 * When a packaged Electron app is launched from the Start Menu, desktop shortcut,
 * or Explorer, its `process.env.PATH` is inherited from the Explorer shell process.
 * If the user installed tools (Python, Node.js, npm, etc.) after Explorer started
 * — or without restarting Explorer — those new PATH entries won't be in
 * `process.env.PATH`. This causes commands like `python`, `npm`, `pip` to be
 * missing from the cowork session even though they work fine in a freshly opened
 * terminal (which reads the latest registry values).
 *
 * This function reads the current Machine and User PATH directly from the registry
 * to get the most up-to-date values, similar to how `resolveUserShellPath()` works
 * for macOS/Linux.
 */
function resolveWindowsRegistryPath(): string | null {
  if (cachedWindowsRegistryPath !== undefined) return cachedWindowsRegistryPath;

  if (process.platform !== 'win32') {
    cachedWindowsRegistryPath = null;
    return null;
  }

  try {
    const machinePath = readWindowsRegistryPathValue('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment');
    const userPath = readWindowsRegistryPathValue('HKCU\\Environment');
    const registryPath = [machinePath, userPath].filter(Boolean).join(';');
    if (registryPath.trim()) {
      // Deduplicate and remove empty entries
      const entries = registryPath
        .split(';')
        .map((entry) => entry.trim())
        .filter(Boolean);
      const unique = Array.from(new Set(entries));
      cachedWindowsRegistryPath = unique.join(';');
      coworkLog('INFO', 'resolveWindowsRegistryPath', `Resolved ${unique.length} PATH entries from Windows registry`);
    } else {
      cachedWindowsRegistryPath = null;
    }
  } catch (error) {
    coworkLog('WARN', 'resolveWindowsRegistryPath', `Failed to read PATH from Windows registry: ${error instanceof Error ? error.message : String(error)}`);
    cachedWindowsRegistryPath = null;
  }

  return cachedWindowsRegistryPath;
}

/**
 * Merge the current process PATH with registry-resolved PATH on Windows.
 *
 * This ensures that any PATH entries the user has added (e.g. Python, Node.js,
 * npm, pip) are available even if the Electron app inherited a stale PATH from
 * Explorer. The registry PATH entries are appended after the current entries
 * so that any overrides already in the env (like Git toolchain, shims) take priority.
 */
function ensureWindowsRegistryPathEntries(env: Record<string, string | undefined>): void {
  const registryPath = resolveWindowsRegistryPath();
  if (!registryPath) return;

  const currentPath = env.PATH || '';
  const currentEntriesLower = new Set(
    currentPath.split(delimiter).map((entry) => entry.toLowerCase().replace(/\\$/, ''))
  );

  const missingEntries: string[] = [];
  for (const entry of registryPath.split(';')) {
    const trimmed = entry.trim();
    if (!trimmed) continue;
    // Normalize: remove trailing backslash for comparison
    const normalizedLower = trimmed.toLowerCase().replace(/\\$/, '');
    if (!currentEntriesLower.has(normalizedLower)) {
      missingEntries.push(trimmed);
      currentEntriesLower.add(normalizedLower); // prevent duplicates within registry entries
    }
  }

  if (missingEntries.length > 0) {
    // Append registry entries at the END so existing overrides (Git, shims) take priority
    env.PATH = currentPath ? `${currentPath}${delimiter}${missingEntries.join(delimiter)}` : missingEntries.join(delimiter);
    coworkLog('INFO', 'ensureWindowsRegistryPathEntries', `Appended ${missingEntries.length} missing PATH entries from Windows registry: ${missingEntries.join(', ')}`);
  }
}

/**
 * Cached git-bash path on Windows. Resolved once and reused.
 */
let cachedGitBashPath: string | null | undefined;
let cachedGitBashResolutionError: string | null | undefined;

function normalizeWindowsPath(input: string | undefined): string | null {
  if (!input) return null;
  const trimmed = input.trim().replace(/\r/g, '');
  if (!trimmed) return null;

  const unquoted = trimmed.replace(/^["']+|["']+$/g, '');
  if (!unquoted) return null;

  return unquoted.replace(/\//g, '\\');
}

function listWindowsCommandPaths(command: string): string[] {
  try {
    const output = execSync(command, { encoding: 'utf-8', timeout: 5000 });
    const parsed = output
      .split(/\r?\n/)
      .map((line) => normalizeWindowsPath(line))
      .filter((line): line is string => Boolean(line && existsSync(line)));
    return Array.from(new Set(parsed));
  } catch {
    return [];
  }
}

function listGitInstallPathsFromRegistry(): string[] {
  const registryKeys = [
    'HKCU\\Software\\GitForWindows',
    'HKLM\\Software\\GitForWindows',
    'HKLM\\Software\\WOW6432Node\\GitForWindows',
  ];

  const installRoots: string[] = [];

  for (const key of registryKeys) {
    try {
      const output = execSync(`reg query "${key}" /v InstallPath`, { encoding: 'utf-8', timeout: 5000 });
      for (const line of output.split(/\r?\n/)) {
        const match = line.match(/InstallPath\s+REG_\w+\s+(.+)$/i);
        const root = normalizeWindowsPath(match?.[1]);
        if (root) {
          installRoots.push(root);
        }
      }
    } catch {
      // registry key might not exist
    }
  }

  return Array.from(new Set(installRoots));
}

function getBundledGitBashCandidates(): string[] {
  const bundledRoots = app.isPackaged
    ? [join(process.resourcesPath, 'mingit')]
    : [
      join(__dirname, '..', '..', 'resources', 'mingit'),
      join(process.cwd(), 'resources', 'mingit'),
    ];

  const candidates: string[] = [];
  for (const root of bundledRoots) {
    // Prefer bin/bash.exe on Windows; invoking usr/bin/bash.exe directly may miss Git toolchain PATH.
    candidates.push(join(root, 'bin', 'bash.exe'));
    candidates.push(join(root, 'usr', 'bin', 'bash.exe'));
  }

  return candidates;
}

function checkWindowsGitBashHealth(bashPath: string): { ok: boolean; reason?: string } {
  try {
    if (!existsSync(bashPath)) {
      return { ok: false, reason: 'path does not exist' };
    }

    // Use a minimal env for the health check to avoid interference from
    // BASH_ENV, MSYS2_PATH_TYPE, or other env vars that could slow startup.
    // Only pass PATH + SYSTEMROOT (required for Windows DLL loading) + HOME.
    const healthEnv: Record<string, string> = {
      PATH: process.env.PATH || '',
      SYSTEMROOT: process.env.SYSTEMROOT || process.env.SystemRoot || 'C:\\Windows',
      HOME: process.env.HOME || process.env.USERPROFILE || '',
    };

    // Try non-login shell first (-c instead of -lc) for speed.
    // Login shells source /etc/profile which can be slow on some systems.
    // cygpath is a standalone binary and does not require a login shell.
    const fastResult = spawnSync(
      bashPath,
      ['-c', 'cygpath -u "C:\\\\Windows"'],
      {
        encoding: 'utf-8',
        timeout: 5000,
        windowsHide: true,
        env: healthEnv,
      }
    );

    const result = (fastResult.error || (typeof fastResult.status === 'number' && fastResult.status !== 0))
      // Non-login shell failed — retry with login shell and a longer timeout.
      // Some Git Bash builds require login shell to set up PATH for cygpath.
      ? spawnSync(
        bashPath,
        ['-lc', 'cygpath -u "C:\\\\Windows"'],
        {
          encoding: 'utf-8',
          timeout: 15000,
          windowsHide: true,
          env: healthEnv,
        }
      )
      : fastResult;

    if (result.error) {
      return { ok: false, reason: result.error.message };
    }

    if (typeof result.status === 'number' && result.status !== 0) {
      const stderr = (result.stderr || '').trim();
      const stdout = (result.stdout || '').trim();
      return {
        ok: false,
        reason: `exit ${result.status}${stderr ? `, stderr: ${stderr}` : ''}${stdout ? `, stdout: ${stdout}` : ''}`,
      };
    }

    const stdout = (result.stdout || '').trim();
    const stderr = (result.stderr || '').trim();
    const lines = stdout
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter(Boolean);
    const lastNonEmptyLine = lines.length > 0 ? lines[lines.length - 1] : '';

    // Some Git Bash builds may print runtime warnings before the actual cygpath
    // output (for example, missing /dev/shm or /dev/mqueue directories).
    // Accept the check when the final non-empty line is a valid POSIX path.
    if (!/^\/[a-zA-Z]\//.test(lastNonEmptyLine)) {
      const diagnosticStdout = truncateDiagnostic(stdout || '(empty)');
      const diagnosticStderr = stderr ? `, stderr: ${truncateDiagnostic(stderr)}` : '';
      return { ok: false, reason: `unexpected cygpath output: ${diagnosticStdout}${diagnosticStderr}` };
    }

    return { ok: true };
  } catch (error) {
    return { ok: false, reason: error instanceof Error ? error.message : String(error) };
  }
}

function truncateDiagnostic(message: string, maxLength = 500): string {
  if (message.length <= maxLength) return message;
  return `${message.slice(0, maxLength - 3)}...`;
}

function getWindowsGitToolDirs(bashPath: string): string[] {
  const normalized = bashPath.replace(/\//g, '\\');
  const lower = normalized.toLowerCase();
  let gitRoot: string | null = null;

  if (lower.endsWith('\\usr\\bin\\bash.exe')) {
    gitRoot = normalized.slice(0, -'\\usr\\bin\\bash.exe'.length);
  } else if (lower.endsWith('\\bin\\bash.exe')) {
    gitRoot = normalized.slice(0, -'\\bin\\bash.exe'.length);
  }

  if (!gitRoot) {
    const bashDir = dirname(normalized);
    return [bashDir].filter((dir) => existsSync(dir));
  }

  const candidates = [
    join(gitRoot, 'cmd'),
    join(gitRoot, 'mingw64', 'bin'),
    join(gitRoot, 'usr', 'bin'),
    join(gitRoot, 'bin'),
  ];

  return candidates.filter((dir) => existsSync(dir));
}

export function ensureElectronNodeShim(electronPath: string, npmBinDir?: string): string | null {
  try {
    const shimDir = join(app.getPath('userData'), 'cowork', 'bin');
    mkdirSync(shimDir, { recursive: true });
    coworkLog('INFO', 'resolveNodeShim', `Shim directory: ${shimDir}, electronPath: ${electronPath}, npmBinDir: ${npmBinDir || '(none)'}`);

    // --- node shim ---
    // Shell script (macOS/Linux/Windows git-bash)
    const nodeSh = join(shimDir, 'node');
    const nodeShContent = [
      '#!/usr/bin/env bash',
      'if [ -z "${LOBSTERAI_ELECTRON_PATH:-}" ]; then',
      '  echo "LOBSTERAI_ELECTRON_PATH is not set" >&2',
      '  exit 127',
      'fi',
      'exec env ELECTRON_RUN_AS_NODE=1 "${LOBSTERAI_ELECTRON_PATH}" "$@"',
      '',
    ].join('\n');

    writeFileSync(nodeSh, nodeShContent, 'utf8');
    try {
      chmodSync(nodeSh, 0o755);
    } catch {
      // Ignore chmod errors on file systems that do not support POSIX modes.
    }
    coworkLog('INFO', 'resolveNodeShim', `Created node bash shim: ${nodeSh}`);

    // Windows .cmd wrapper (only needed on Windows)
    if (process.platform === 'win32') {
      const nodeCmd = join(shimDir, 'node.cmd');
      const nodeCmdContent = [
        '@echo off',
        'if "%LOBSTERAI_ELECTRON_PATH%"=="" (',
        '  echo LOBSTERAI_ELECTRON_PATH is not set 1>&2',
        '  exit /b 127',
        ')',
        'set ELECTRON_RUN_AS_NODE=1',
        '"%LOBSTERAI_ELECTRON_PATH%" %*',
        '',
      ].join('\r\n');
      writeFileSync(nodeCmd, nodeCmdContent, 'utf8');
      coworkLog('INFO', 'resolveNodeShim', `Created node.cmd shim: ${nodeCmd}`);
    }

    // --- npx / npm shims ---
    // Create shims that invoke npx-cli.js / npm-cli.js from the bundled npm
    // package via the node shim above. This avoids relying on symlinks in
    // node_modules/.bin which do not work on Windows cross-platform builds.
    if (npmBinDir && existsSync(npmBinDir)) {
      const npxCliJs = join(npmBinDir, 'npx-cli.js');
      const npmCliJs = join(npmBinDir, 'npm-cli.js');

      // Convert to POSIX path for bash scripts on Windows (git-bash)
      const npxCliJsPosix = npxCliJs.replace(/\\/g, '/');
      const npmCliJsPosix = npmCliJs.replace(/\\/g, '/');

      coworkLog('INFO', 'resolveNodeShim', `npmBinDir exists: true, npx-cli.js exists: ${existsSync(npxCliJs)}, npm-cli.js exists: ${existsSync(npmCliJs)}`);

      if (existsSync(npxCliJs)) {
        // npx bash shim
        const npxSh = join(shimDir, 'npx');
        const npxShContent = [
          '#!/usr/bin/env bash',
          'SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"',
          `exec "$SCRIPT_DIR/node" "${npxCliJsPosix}" "$@"`,
          '',
        ].join('\n');
        writeFileSync(npxSh, npxShContent, 'utf8');
        try { chmodSync(npxSh, 0o755); } catch { /* ignore */ }
        coworkLog('INFO', 'resolveNodeShim', `Created npx bash shim: ${npxSh} -> ${npxCliJsPosix}`);

        // npx.cmd for Windows — uses %LOBSTERAI_NPM_BIN_DIR% env var to avoid
        // hardcoding paths that may contain non-ASCII chars (breaks GBK cmd.exe).
        if (process.platform === 'win32') {
          const npxCmd = join(shimDir, 'npx.cmd');
          const npxCmdContent = [
            '@echo off',
            '"%~dp0node.cmd" "%LOBSTERAI_NPM_BIN_DIR%\\npx-cli.js" %*',
            '',
          ].join('\r\n');
          writeFileSync(npxCmd, npxCmdContent, 'utf8');
          coworkLog('INFO', 'resolveNodeShim', `Created npx.cmd shim: ${npxCmd} (using env var LOBSTERAI_NPM_BIN_DIR)`);
        }
      } else {
        coworkLog('WARN', 'resolveNodeShim', `npx-cli.js not found at: ${npxCliJs}`);
      }

      if (existsSync(npmCliJs)) {
        // npm bash shim
        const npmSh = join(shimDir, 'npm');
        const npmShContent = [
          '#!/usr/bin/env bash',
          'SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"',
          `exec "$SCRIPT_DIR/node" "${npmCliJsPosix}" "$@"`,
          '',
        ].join('\n');
        writeFileSync(npmSh, npmShContent, 'utf8');
        try { chmodSync(npmSh, 0o755); } catch { /* ignore */ }
        coworkLog('INFO', 'resolveNodeShim', `Created npm bash shim: ${npmSh} -> ${npmCliJsPosix}`);

        // npm.cmd for Windows — uses %LOBSTERAI_NPM_BIN_DIR% env var to avoid
        // hardcoding paths that may contain non-ASCII chars (breaks GBK cmd.exe).
        if (process.platform === 'win32') {
          const npmCmd = join(shimDir, 'npm.cmd');
          const npmCmdContent = [
            '@echo off',
            '"%~dp0node.cmd" "%LOBSTERAI_NPM_BIN_DIR%\\npm-cli.js" %*',
            '',
          ].join('\r\n');
          writeFileSync(npmCmd, npmCmdContent, 'utf8');
          coworkLog('INFO', 'resolveNodeShim', `Created npm.cmd shim: ${npmCmd} (using env var LOBSTERAI_NPM_BIN_DIR)`);
        }
      } else {
        coworkLog('WARN', 'resolveNodeShim', `npm-cli.js not found at: ${npmCliJs}`);
      }

      coworkLog('INFO', 'resolveNodeShim', `Created npx/npm shims pointing to: ${npmBinDir}`);
    } else {
      coworkLog('WARN', 'resolveNodeShim', `npmBinDir not available: ${npmBinDir || '(not provided)'}, exists: ${npmBinDir ? existsSync(npmBinDir) : 'N/A'}`);
    }

    // Verify shim files exist and are executable
    const shimFiles = ['node', 'npx', 'npm'];
    for (const name of shimFiles) {
      const shimPath = join(shimDir, name);
      const exists = existsSync(shimPath);
      if (exists) {
        try {
          const stat = statSync(shimPath);
          coworkLog('INFO', 'resolveNodeShim', `Shim verify: ${name} exists, mode=0o${stat.mode.toString(8)}, size=${stat.size}`);
        } catch (e) {
          coworkLog('WARN', 'resolveNodeShim', `Shim verify: ${name} exists but stat failed: ${e instanceof Error ? e.message : String(e)}`);
        }
      } else {
        coworkLog('WARN', 'resolveNodeShim', `Shim verify: ${name} NOT found at ${shimPath}`);
      }
    }

    return shimDir;
  } catch (error) {
    coworkLog('WARN', 'resolveNodeShim', `Failed to prepare Electron Node shim: ${error instanceof Error ? error.message : String(error)}`);
    return null;
  }
}

/**
 * Resolve git-bash path on Windows.
 * Claude Code CLI requires git-bash for shell tool execution.
 * Priority: env var override > bundled PortableGit > installed Git > PATH lookup.
 * Every candidate must pass a health check (`cygpath -u`) before use.
 */
function resolveWindowsGitBashPath(): string | null {
  if (cachedGitBashPath !== undefined) return cachedGitBashPath;

  if (process.platform !== 'win32') {
    cachedGitBashPath = null;
    cachedGitBashResolutionError = null;
    return null;
  }

  const candidates: Array<{ path: string; source: string }> = [];
  const seen = new Set<string>();
  const failedCandidates: string[] = [];

  const pushCandidate = (candidatePath: string | null, source: string): void => {
    if (!candidatePath) return;
    const normalized = normalizeWindowsPath(candidatePath);
    if (!normalized) return;
    const key = normalized.toLowerCase();
    if (seen.has(key)) return;
    seen.add(key);
    candidates.push({ path: normalized, source });
  };

  // 1. Explicit env var (user override)
  pushCandidate(process.env.CLAUDE_CODE_GIT_BASH_PATH ?? null, 'env:CLAUDE_CODE_GIT_BASH_PATH');

  // 2. Bundled PortableGit (preferred default in LobsterAI package)
  for (const bundledCandidate of getBundledGitBashCandidates()) {
    pushCandidate(bundledCandidate, 'bundled:resources/mingit');
  }

  // 3. Common Git for Windows installation paths
  const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
  const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
  const localAppData = process.env.LOCALAPPDATA || '';
  const userProfile = process.env.USERPROFILE || '';

  const installCandidates = [
    join(programFiles, 'Git', 'bin', 'bash.exe'),
    join(programFiles, 'Git', 'usr', 'bin', 'bash.exe'),
    join(programFilesX86, 'Git', 'bin', 'bash.exe'),
    join(programFilesX86, 'Git', 'usr', 'bin', 'bash.exe'),
    join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'),
    join(localAppData, 'Programs', 'Git', 'usr', 'bin', 'bash.exe'),
    join(userProfile, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'),
    join(userProfile, 'scoop', 'apps', 'git', 'current', 'usr', 'bin', 'bash.exe'),
    'C:\\Git\\bin\\bash.exe',
    'C:\\Git\\usr\\bin\\bash.exe',
  ];

  for (const installCandidate of installCandidates) {
    pushCandidate(installCandidate, 'installed:common-paths');
  }

  // 4. Query Git for Windows install root from registry
  const registryInstallRoots = listGitInstallPathsFromRegistry();
  for (const installRoot of registryInstallRoots) {
    const registryCandidates = [
      join(installRoot, 'bin', 'bash.exe'),
      join(installRoot, 'usr', 'bin', 'bash.exe'),
    ];
    for (const registryCandidate of registryCandidates) {
      pushCandidate(registryCandidate, `registry:${installRoot}`);
    }
  }

  // 5. Try `where bash`
  const bashPaths = listWindowsCommandPaths('where bash');
  for (const bashPath of bashPaths) {
    if (bashPath.toLowerCase().endsWith('\\bash.exe')) {
      pushCandidate(bashPath, 'path:where bash');
    }
  }

  // 6. Try `where git` and derive bash from git location
  const gitPaths = listWindowsCommandPaths('where git');
  for (const gitPath of gitPaths) {
    const gitRoot = dirname(dirname(gitPath));
    const bashCandidates = [
      join(gitRoot, 'bin', 'bash.exe'),
      join(gitRoot, 'usr', 'bin', 'bash.exe'),
    ];
    for (const bashCandidate of bashCandidates) {
      pushCandidate(bashCandidate, `path:where git (${gitPath})`);
    }
  }

  for (const candidate of candidates) {
    if (!existsSync(candidate.path)) {
      continue;
    }

    const health = checkWindowsGitBashHealth(candidate.path);
    if (health.ok) {
      coworkLog('INFO', 'resolveGitBash', `Selected git-bash (${candidate.source}): ${candidate.path}`);
      cachedGitBashPath = candidate.path;
      cachedGitBashResolutionError = null;
      return candidate.path;
    }

    const failure = `${candidate.path} [${candidate.source}] failed health check (${health.reason || 'unknown reason'})`;
    failedCandidates.push(failure);
    coworkLog('WARN', 'resolveGitBash', failure);
  }

  const diagnostic = failedCandidates.length > 0
    ? `No healthy git-bash found. Failures: ${failedCandidates.join('; ')}`
    : 'No git-bash candidates found on this system';
  coworkLog('WARN', 'resolveGitBash', diagnostic);
  cachedGitBashPath = null;
  cachedGitBashResolutionError = truncateDiagnostic(diagnostic);
  return null;
}

/**
 * Windows system directories that must be in PATH for built-in commands
 * (ipconfig, systeminfo, netstat, ping, nslookup, etc.) to work.
 */
const WINDOWS_SYSTEM_PATH_ENTRIES = [
  'System32',
  'System32\\Wbem',
  'System32\\WindowsPowerShell\\v1.0',
  'System32\\OpenSSH',
];

/**
 * Critical Windows environment variables that some system commands and DLLs depend on.
 * Without these, commands like ipconfig may fail even if System32 is in PATH.
 */
const WINDOWS_CRITICAL_ENV_VARS: Record<string, () => string | undefined> = {
  SystemRoot: () => process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\windows',
  windir: () => process.env.windir || process.env.WINDIR || process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\windows',
  COMSPEC: () => process.env.COMSPEC || process.env.comspec || 'C:\\windows\\system32\\cmd.exe',
  SYSTEMDRIVE: () => process.env.SYSTEMDRIVE || process.env.SystemDrive || 'C:',
};

/**
 * Ensure critical Windows system environment variables are present in the env object.
 *
 * Packaged Electron apps or certain launch contexts may strip environment variables
 * like SystemRoot, windir, COMSPEC, and SYSTEMDRIVE. Many Windows system commands
 * and DLLs depend on these variables to locate system resources.
 *
 * Additionally, the Claude Agent SDK's shell snapshot mechanism runs `echo $PATH`
 * via `shell: true`, which on Windows uses cmd.exe. The captured PATH is then
 * baked into the snapshot file. If these critical variables are missing, the shell
 * environment may be broken and commands fail silently.
 */
function ensureWindowsSystemEnvVars(env: Record<string, string | undefined>): void {
  const injected: string[] = [];

  for (const [key, resolver] of Object.entries(WINDOWS_CRITICAL_ENV_VARS)) {
    // Check both the exact case and common variants (Windows env vars are case-insensitive
    // but Node.js process.env on Windows normalizes to the original casing)
    if (!env[key]) {
      const value = resolver();
      if (value) {
        env[key] = value;
        injected.push(`${key}=${value}`);
      }
    }
  }

  if (injected.length > 0) {
    coworkLog('INFO', 'ensureWindowsSystemEnvVars', `Injected missing Windows system env vars: ${injected.join(', ')}`);
  }
}

/**
 * Ensure Windows system directories (System32, etc.) are present in PATH.
 *
 * When the Electron app launches, process.env.PATH normally includes System32.
 * However, the Claude Agent SDK creates a "shell snapshot" by running git-bash
 * with `-c -l` (login shell). The git-bash `/etc/profile` rebuilds PATH based on
 * MSYS2_PATH_TYPE (default: "inherit"), which preserves ORIGINAL_PATH from the
 * inherited environment. If System32 entries are somehow missing from the inherited
 * PATH, they won't appear in the snapshot either.
 *
 * This function ensures that essential Windows system directories are always
 * present in PATH before the environment is handed to the SDK.
 */
function ensureWindowsSystemPathEntries(env: Record<string, string | undefined>): void {
  const systemRoot = env.SystemRoot || env.SYSTEMROOT || 'C:\\windows';
  const currentPath = env.PATH || '';
  const currentEntries = currentPath.split(delimiter).map((entry) => entry.toLowerCase());

  const missingDirs: string[] = [];
  for (const relDir of WINDOWS_SYSTEM_PATH_ENTRIES) {
    const fullDir = join(systemRoot, relDir);
    if (!currentEntries.includes(fullDir.toLowerCase()) && existsSync(fullDir)) {
      missingDirs.push(fullDir);
    }
  }

  // Also ensure the systemRoot itself (e.g. C:\windows) is in PATH
  if (!currentEntries.includes(systemRoot.toLowerCase()) && existsSync(systemRoot)) {
    missingDirs.push(systemRoot);
  }

  if (missingDirs.length > 0) {
    // Append system dirs at the END so they don't override user tools
    env.PATH = currentPath ? `${currentPath}${delimiter}${missingDirs.join(delimiter)}` : missingDirs.join(delimiter);
    coworkLog('INFO', 'ensureWindowsSystemPathEntries', `Appended missing Windows system PATH entries: ${missingDirs.join(', ')}`);
  }
}

/**
 * Ensure non-login git-bash invocations can resolve core MSYS commands.
 *
 * Claude Agent SDK invokes `cygpath` during Windows path normalization via
 * `execSync(..., { shell: bash.exe })`, which does NOT always run a login shell.
 * In that code path, bash may inherit Windows-format PATH directly, and command
 * lookup for `cygpath` can fail because PATH is semicolon-delimited.
 *
 * Prefixing PATH with `/usr/bin:/bin` keeps Windows PATH semantics (semicolon
 * delimiter) while giving bash a valid colon-delimited segment at the beginning.
 * This prevents errors like: `/bin/bash: line 1: cygpath: command not found`.
 */
function ensureWindowsBashBootstrapPath(env: Record<string, string | undefined>): void {
  const currentPath = env.PATH || '';
  if (!currentPath) return;

  const bootstrapToken = '/usr/bin:/bin';
  const entries = currentPath.split(delimiter).map((entry) => entry.trim()).filter(Boolean);
  if (entries.some((entry) => entry === bootstrapToken)) {
    return;
  }

  env.PATH = `${bootstrapToken}${delimiter}${currentPath}`;
  coworkLog('INFO', 'ensureWindowsBashBootstrapPath', `Prepended bash bootstrap PATH token: ${bootstrapToken}`);
}

/**
 * Convert a single Windows path to MSYS2/POSIX format.
 *
 * When the Windows path contains non-ASCII characters (e.g. Chinese usernames
 * like C:\Users\中文用户\...), MSYS2's automatic Windows→POSIX conversion may
 * corrupt the path if it runs before LANG=C.UTF-8 takes effect. Pre-converting
 * to POSIX format (/c/Users/中文用户/...) bypasses this problematic conversion
 * because MSYS2 recognises the value as already POSIX and passes it through
 * directly to its internal wide-char file APIs.
 */
function singleWindowsPathToPosix(windowsPath: string): string {
  if (!windowsPath) return windowsPath;
  const driveMatch = windowsPath.match(/^([A-Za-z]):[/\\](.*)/);
  if (driveMatch) {
    const driveLetter = driveMatch[1].toLowerCase();
    const rest = driveMatch[2].replace(/\\/g, '/').replace(/\/+$/, '');
    return `/${driveLetter}${rest ? '/' + rest : ''}`;
  }
  return windowsPath.replace(/\\/g, '/');
}

/**
 * Convert a Windows-format PATH string to MSYS2/POSIX format for git-bash.
 *
 * Windows PATH uses semicolons (;) as delimiters and backslash paths (C:\...),
 * while MSYS2 bash expects colons (:) and forward-slash POSIX paths (/c/...).
 *
 * When Node.js passes env vars to a forked process, PATH stays in Windows format.
 * If the CLI later spawns git-bash, the /etc/profile uses ORIGINAL_PATH="${PATH}"
 * and appends it to the new PATH with a colon. But since the Windows PATH still
 * has semicolons inside, it becomes one giant invalid path entry.
 *
 * This function converts each semicolon-separated Windows path entry to its
 * POSIX equivalent so that git-bash can correctly parse all entries.
 */
function convertWindowsPathToMsys(windowsPath: string): string {
  if (!windowsPath) return windowsPath;

  const entries = windowsPath.split(';').filter(Boolean);
  const converted: string[] = [];

  for (const entry of entries) {
    const trimmed = entry.trim();
    if (!trimmed) continue;

    // Convert Windows path to POSIX: C:\foo\bar → /c/foo/bar
    // Drive letter pattern: X:\ or X:/
    const driveMatch = trimmed.match(/^([A-Za-z]):[/\\](.*)/);
    if (driveMatch) {
      const driveLetter = driveMatch[1].toLowerCase();
      const rest = driveMatch[2].replace(/\\/g, '/').replace(/\/+$/, '');
      converted.push(`/${driveLetter}${rest ? '/' + rest : ''}`);
    } else if (trimmed.startsWith('/')) {
      // Already POSIX-style
      converted.push(trimmed);
    } else {
      // Relative path or unknown format, convert backslashes
      converted.push(trimmed.replace(/\\/g, '/'));
    }
  }

  return converted.join(':');
}

/**
 * Set ORIGINAL_PATH with POSIX-converted PATH for git-bash to inherit.
 *
 * Git-bash's /etc/profile (with MSYS2_PATH_TYPE=inherit) reads ORIGINAL_PATH
 * and appends it to the MSYS2 PATH. However, if ORIGINAL_PATH contains
 * Windows-format paths (semicolons, backslashes), bash treats them as a single
 * invalid entry because it uses colons as the PATH delimiter.
 *
 * By pre-setting ORIGINAL_PATH to the POSIX-converted version of our PATH,
 * we ensure that /etc/profile appends properly formatted, colon-separated
 * paths that bash can actually use.
 */
function ensureWindowsOriginalPath(env: Record<string, string | undefined>): void {
  const currentPath = env.PATH || '';
  if (!currentPath) return;

  const posixPath = convertWindowsPathToMsys(currentPath);
  env.ORIGINAL_PATH = posixPath;
  coworkLog('INFO', 'ensureWindowsOriginalPath', `Set ORIGINAL_PATH with ${posixPath.split(':').length} POSIX-format entries`);
}

/**
 * Create a bash init script that sets the Windows console code page to UTF-8 (65001).
 *
 * On Chinese Windows, the default console code page is GBK (936). When git-bash
 * executes Windows native commands (dir, ipconfig, systeminfo, net, type, etc.),
 * they output text encoded in the active console code page. If the code page is GBK,
 * the output contains GBK-encoded bytes, but the Claude Agent SDK reads them as UTF-8,
 * producing garbled characters (mojibake).
 *
 * By setting BASH_ENV to this script, every non-interactive bash session spawned by
 * the Claude Agent SDK will automatically switch the console code page to UTF-8
 * before executing any commands.
 */
function ensureWindowsBashUtf8InitScript(): string | null {
  try {
    const initDir = join(app.getPath('userData'), 'cowork', 'bin');
    mkdirSync(initDir, { recursive: true });

    const initScript = join(initDir, 'bash_utf8_init.sh');
    const content = [
      '#!/usr/bin/env bash',
      '# Auto-generated by LobsterAI – switch Windows console code page to UTF-8',
      '# to prevent garbled output from Windows native commands.',
      'if command -v chcp.com >/dev/null 2>&1; then',
      '  chcp.com 65001 >/dev/null 2>&1',
      'fi',
      '',
    ].join('\n');

    writeFileSync(initScript, content, 'utf8');
    try {
      chmodSync(initScript, 0o755);
    } catch {
      // Ignore chmod errors on file systems that do not support POSIX modes.
    }

    return initScript;
  } catch (error) {
    coworkLog('WARN', 'ensureWindowsBashUtf8InitScript', `Failed to create bash UTF-8 init script: ${error instanceof Error ? error.message : String(error)}`);
    return null;
  }
}

function applyPackagedEnvOverrides(env: Record<string, string | undefined>): void {
  const electronNodeRuntimePath = getElectronNodeRuntimePath();

  if (app.isPackaged && !env.LOBSTERAI_ELECTRON_PATH) {
    env.LOBSTERAI_ELECTRON_PATH = electronNodeRuntimePath;
  }

  // On Windows, resolve git-bash and ensure Git toolchain directories are available in PATH.
  if (process.platform === 'win32') {
    env.LOBSTERAI_ELECTRON_PATH = electronNodeRuntimePath;

    // Force UTF-8 encoding for MSYS2/git-bash.
    //
    // On Chinese (and other non-Latin) Windows systems, the default system locale
    // uses GBK (code page 936) or similar legacy encodings. Without explicit locale
    // settings, MSYS2 tools and the git-bash environment may output text in the
    // system's legacy encoding, which the Claude Agent SDK misinterprets as UTF-8,
    // producing garbled characters.
    //
    // Setting LANG and LC_ALL to C.UTF-8 tells the MSYS2 runtime to use UTF-8 for
    // all text I/O, including output from coreutils (ls, cat, grep, etc.).
    if (!env.LANG) {
      env.LANG = 'C.UTF-8';
    }
    if (!env.LC_ALL) {
      env.LC_ALL = 'C.UTF-8';
    }

    // Force Python to use UTF-8 mode (PEP 540, Python 3.7+).
    // Without this, Python on Chinese Windows defaults to GBK for stdin/stdout/stderr
    // and file I/O, causing garbled output when the SDK reads it as UTF-8.
    if (!env.PYTHONUTF8) {
      env.PYTHONUTF8 = '1';
    }
    if (!env.PYTHONIOENCODING) {
      env.PYTHONIOENCODING = 'utf-8';
    }

    // Force `less` and `git` pager output to use UTF-8.
    if (!env.LESSCHARSET) {
      env.LESSCHARSET = 'utf-8';
    }

    // Create a bash init script that switches the Windows console code page to
    // UTF-8 (65001). By setting BASH_ENV, every non-interactive bash session
    // spawned by the Claude Agent SDK will source this script before executing
    // commands, ensuring Windows native commands (dir, ipconfig, systeminfo,
    // type, etc.) output UTF-8 instead of GBK.
    if (!env.BASH_ENV) {
      const initScript = ensureWindowsBashUtf8InitScript();
      if (initScript) {
        // Convert to MSYS2 POSIX format to avoid encoding issues when the
        // path contains non-ASCII characters (e.g. Chinese Windows username).
        // MSYS2's automatic Windows→POSIX conversion can corrupt non-ASCII
        // chars if it runs before LANG=C.UTF-8 takes effect during DLL init.
        env.BASH_ENV = singleWindowsPathToPosix(initScript);
        coworkLog('INFO', 'applyPackagedEnvOverrides', `Set BASH_ENV for UTF-8 console code page: ${env.BASH_ENV}`);
      }
    }

    // Ensure critical Windows system environment variables are always present.
    // Packaged Electron apps or certain launch contexts may lack these variables,
    // which causes Windows built-in commands (ipconfig, systeminfo, netstat, etc.)
    // to fail when executed inside git-bash via the Claude Agent SDK.
    ensureWindowsSystemEnvVars(env);

    // Ensure Windows system directories (System32, etc.) are always in PATH.
    // The Claude Agent SDK's shell snapshot mechanism captures PATH and may lose
    // system directories if they were missing from the inherited environment.
    ensureWindowsSystemPathEntries(env);

    // Merge the latest PATH entries from the Windows registry (Machine + User).
    // When the Electron app is launched from Explorer/Start Menu, process.env.PATH
    // may be stale and missing tools installed after Explorer started (e.g. Python,
    // Node.js, npm). Reading from the registry ensures we get the latest values,
    // similar to how a freshly opened terminal would.
    ensureWindowsRegistryPathEntries(env);

    const configuredBashPath = normalizeWindowsPath(env.CLAUDE_CODE_GIT_BASH_PATH);
    let bashPath = configuredBashPath && existsSync(configuredBashPath)
      ? configuredBashPath
      : resolveWindowsGitBashPath();

    if (configuredBashPath && bashPath === configuredBashPath) {
      const configuredHealth = checkWindowsGitBashHealth(configuredBashPath);
      if (!configuredHealth.ok) {
        const fallbackPath = resolveWindowsGitBashPath();
        if (fallbackPath && fallbackPath !== configuredBashPath) {
          coworkLog(
            'WARN',
            'resolveGitBash',
            `Configured bash is unhealthy (${configuredBashPath}): ${configuredHealth.reason || 'unknown reason'}. Falling back to: ${fallbackPath}`
          );
          bashPath = fallbackPath;
        } else {
          const diagnostic = truncateDiagnostic(
            `Configured bash is unhealthy (${configuredBashPath}): ${configuredHealth.reason || 'unknown reason'}`
          );
          env.LOBSTERAI_GIT_BASH_RESOLUTION_ERROR = diagnostic;
          coworkLog('WARN', 'resolveGitBash', diagnostic);
          bashPath = null;
        }
      }
    }

    if (bashPath) {
      env.CLAUDE_CODE_GIT_BASH_PATH = bashPath;
      delete env.LOBSTERAI_GIT_BASH_RESOLUTION_ERROR;
      coworkLog('INFO', 'resolveGitBash', `Using Windows git-bash: ${bashPath}`);
      const gitToolDirs = getWindowsGitToolDirs(bashPath);
      env.PATH = appendEnvPath(env.PATH, gitToolDirs);
      coworkLog('INFO', 'resolveGitBash', `Injected Windows Git toolchain PATH entries: ${gitToolDirs.join(', ')}`);
      ensureWindowsBashBootstrapPath(env);
    } else {
      const diagnostic = cachedGitBashResolutionError || 'git-bash not found or failed health checks';
      env.LOBSTERAI_GIT_BASH_RESOLUTION_ERROR = truncateDiagnostic(diagnostic);
    }

    appendPythonRuntimeToEnv(env);

    // Tell git-bash to inherit the PATH from the parent process instead of
    // rebuilding it from scratch. Without this, git-bash's /etc/profile (login
    // shell) defaults to constructing a minimal PATH containing only Windows
    // system directories + MSYS2 tools, discarding user-installed tool paths
    // like Python, Node.js, npm, pip, etc. Setting MSYS2_PATH_TYPE=inherit
    // makes git-bash preserve the full PATH we've carefully constructed above.
    if (!env.MSYS2_PATH_TYPE) {
      env.MSYS2_PATH_TYPE = 'inherit';
      coworkLog('INFO', 'applyPackagedEnvOverrides', 'Set MSYS2_PATH_TYPE=inherit to preserve PATH in git-bash');
    }

    // Pre-set ORIGINAL_PATH in POSIX format so git-bash's /etc/profile can use it.
    //
    // ROOT CAUSE: Node.js env PATH on Windows uses semicolons (;) and backslash
    // paths (C:\...). When the Claude Agent SDK's CLI spawns git-bash with this env,
    // /etc/profile reads ORIGINAL_PATH="${ORIGINAL_PATH:-${PATH}}" and appends it
    // with a colon. But the semicolons in the Windows PATH are NOT converted to
    // colons, so "C:\nodejs;C:\python" becomes one giant invalid entry instead of
    // two separate paths. This causes `npm`, `python`, `pip` etc. to be unfindable.
    //
    // By pre-setting ORIGINAL_PATH to the POSIX-converted version (/c/nodejs:/c/python),
    // /etc/profile uses it directly and bash can correctly parse all PATH entries.
    // This MUST be done AFTER all PATH modifications above so the full PATH is captured.
    ensureWindowsOriginalPath(env);
  }

  if (!app.isPackaged) {
    // In dev mode, prepend project's node_modules/.bin to PATH so bundled
    // npx/npm are found even if the user has no global Node.js installation.
    const devBinDir = join(app.getAppPath(), 'node_modules', '.bin');
    if (existsSync(devBinDir)) {
      env.PATH = [devBinDir, env.PATH].filter(Boolean).join(delimiter);
      coworkLog('INFO', 'applyPackagedEnvOverrides', `Dev mode: prepended node_modules/.bin to PATH: ${devBinDir}`);
    }

    // In dev mode, add openclaw runtime's node_modules to NODE_PATH so exec tool
    // can access shared packages like sharp.
    const devRuntimeNodeModules = (() => {
      const candidates = [
        join(app.getAppPath(), 'vendor', 'openclaw-runtime', 'current', 'node_modules'),
        join(process.cwd(), 'vendor', 'openclaw-runtime', 'current', 'node_modules'),
      ];
      for (const c of candidates) {
        try {
          const resolved = realpathSync(c);
          if (existsSync(resolved)) return resolved;
        } catch { /* symlink target missing — skip */ }
      }
      return null;
    })();
    if (devRuntimeNodeModules) {
      env.NODE_PATH = appendEnvPath(env.NODE_PATH, [devRuntimeNodeModules]);
      coworkLog('INFO', 'applyPackagedEnvOverrides', `Dev mode: added openclaw runtime node_modules to NODE_PATH: ${devRuntimeNodeModules}`);
    }
    return;
  }

  if (!env.HOME) {
    env.HOME = app.getPath('home');
  }

  // Resolve user's shell PATH so that node, npm, and other tools are findable
  const userPath = resolveUserShellPath();
  if (userPath) {
    env.PATH = userPath;
    coworkLog('INFO', 'applyPackagedEnvOverrides', `Resolved user shell PATH (${userPath.split(delimiter).length} entries)`);
    for (const entry of userPath.split(delimiter)) {
      coworkLog('INFO', 'applyPackagedEnvOverrides', `  PATH entry: ${entry} (exists: ${existsSync(entry)})`);
    }
  } else {
    // Fallback: append common node installation paths
    const home = env.HOME || app.getPath('home');
    const commonPaths = [
      '/usr/local/bin',
      '/opt/homebrew/bin',
      `${home}/.nvm/current/bin`,
      `${home}/.volta/bin`,
      `${home}/.fnm/current/bin`,
    ];
    env.PATH = [env.PATH, ...commonPaths].filter(Boolean).join(delimiter);
    coworkLog('WARN', 'applyPackagedEnvOverrides', `Failed to resolve user shell PATH, using fallback common paths`);
  }

  const resourcesPath = process.resourcesPath;
  coworkLog('INFO', 'applyPackagedEnvOverrides', `Packaged mode: resourcesPath=${resourcesPath}`);

  // Create node/npx/npm shims that wrap Electron as a Node.js runtime via
  // ELECTRON_RUN_AS_NODE=1 and point npx/npm to the bundled npm package.
  // This avoids relying on node_modules/.bin symlinks which don't work on
  // Windows cross-platform builds.
  const npmBinDir = join(resourcesPath, 'app.asar.unpacked', 'node_modules', 'npm', 'bin');
  coworkLog('INFO', 'applyPackagedEnvOverrides', `npmBinDir=${npmBinDir}, exists=${existsSync(npmBinDir)}`);

  // Set env var so .cmd shims can reference npmBinDir without hardcoding
  // non-ASCII characters (which break on Windows when cmd.exe uses GBK code page).
  env.LOBSTERAI_NPM_BIN_DIR = npmBinDir;

  const hasSystemNode = hasCommandInEnv('node', env);
  const hasSystemNpx = hasCommandInEnv('npx', env);
  const hasSystemNpm = hasCommandInEnv('npm', env);
  const shouldForcePackagedDarwinShim = app.isPackaged && process.platform === 'darwin';
  const shouldInjectShim = shouldForcePackagedDarwinShim
    || process.platform === 'win32'
    || !(hasSystemNode && hasSystemNpx && hasSystemNpm);
  if (shouldInjectShim) {
    const shimDir = ensureElectronNodeShim(electronNodeRuntimePath, npmBinDir);
    if (shimDir) {
      env.PATH = [shimDir, env.PATH].filter(Boolean).join(delimiter);
      env.LOBSTERAI_NODE_SHIM_ACTIVE = '1';
      coworkLog('INFO', 'resolveNodeShim', `Injected Electron Node/npx/npm shim PATH entry: ${shimDir}`);
      if (shouldForcePackagedDarwinShim) {
        coworkLog('INFO', 'resolveNodeShim', 'Packaged macOS build: forcing bundled Electron node/npx/npm shims to avoid stale system Node versions');
      }

      // Re-compute ORIGINAL_PATH after shim injection so that git-bash
      // also sees the bundled node/npx/npm in its PATH.
      if (process.platform === 'win32') {
        ensureWindowsOriginalPath(env);
      }
    }
  } else {
    delete env.LOBSTERAI_NODE_SHIM_ACTIVE;
    coworkLog('INFO', 'resolveNodeShim', 'System node/npx/npm detected; skipped Electron node shim injection');
  }

  const nodePaths = [
    join(resourcesPath, 'app.asar', 'node_modules'),
    join(resourcesPath, 'app.asar.unpacked', 'node_modules'),
    join(resourcesPath, 'cfmind', 'node_modules'),
  ].filter((nodePath) => existsSync(nodePath));

  if (nodePaths.length > 0) {
    env.NODE_PATH = appendEnvPath(env.NODE_PATH, nodePaths);
  }

  // Verify node/npx resolution in the constructed environment
  verifyNodeEnvironment(env);
}

/**
 * Verify that node/npx/npm can be resolved from the constructed environment PATH.
 * Logs diagnostic info for debugging MCP server startup issues on macOS.
 */
function verifyNodeEnvironment(env: Record<string, string | undefined>): void {
  const tag = 'verifyNodeEnv';
  const pathValue = env.PATH || '';

  // Log final PATH entries
  const pathEntries = pathValue.split(delimiter);
  coworkLog('INFO', tag, `Final PATH has ${pathEntries.length} entries:`);
  for (let i = 0; i < pathEntries.length; i++) {
    const entry = pathEntries[i];
    const exists = entry ? existsSync(entry) : false;
    coworkLog('INFO', tag, `  [${i}] ${entry} (exists: ${exists})`);
  }

  // Try to resolve node, npx, npm using 'which' (macOS/Linux) or 'where' (Windows)
  const whichCmd = process.platform === 'win32' ? 'where' : 'which';
  for (const tool of ['node', 'npx', 'npm']) {
    try {
      const result = spawnSync(whichCmd, [tool], {
        env: { ...env } as NodeJS.ProcessEnv,
        encoding: 'utf-8',
        timeout: 5000,
        windowsHide: process.platform === 'win32',
      });
      if (result.status === 0 && result.stdout) {
        const resolved = result.stdout.trim();
        coworkLog('INFO', tag, `${whichCmd} ${tool} => ${resolved}`);
        const resolvedCandidates = resolved
          .split(/\r?\n/)
          .map((line) => line.trim())
          .filter(Boolean);
        const resolvedForExec = process.platform === 'win32'
          ? resolvedCandidates.find((candidate) => /\.(cmd|exe|bat)$/i.test(candidate)) || resolvedCandidates[0]
          : resolvedCandidates[0];

        // Try to get version
        if (tool === 'node' && resolvedForExec) {
          try {
            let execTarget = resolvedForExec;
            if (process.platform === 'win32' && /\.cmd$/i.test(resolvedForExec)) {
              execTarget = env.LOBSTERAI_ELECTRON_PATH || process.execPath;
            }
            const versionResult = spawnSync(execTarget, ['--version'], {
              env: { ...env, ELECTRON_RUN_AS_NODE: '1' } as NodeJS.ProcessEnv,
              encoding: 'utf-8',
              timeout: 5000,
              windowsHide: process.platform === 'win32',
            });
            coworkLog('INFO', tag, `node --version (${execTarget}) => ${(versionResult.stdout || '').trim()} (exit: ${versionResult.status})`);
            if (versionResult.error) {
              coworkLog('WARN', tag, `node --version spawn error: ${versionResult.error.message}`);
            }
            if (versionResult.stderr) {
              coworkLog('WARN', tag, `node --version stderr: ${versionResult.stderr.trim()}`);
            }
          } catch (e) {
            coworkLog('WARN', tag, `node --version failed: ${e instanceof Error ? e.message : String(e)}`);
          }
        }
      } else {
        coworkLog('WARN', tag, `${whichCmd} ${tool} => NOT FOUND (exit: ${result.status}, stderr: ${(result.stderr || '').trim()})`);
      }
    } catch (e) {
      coworkLog('WARN', tag, `${whichCmd} ${tool} threw: ${e instanceof Error ? e.message : String(e)}`);
    }
  }

  // Log key env vars
  coworkLog('INFO', tag, `NODE_PATH=${env.NODE_PATH || '(not set)'}`);
  coworkLog('INFO', tag, `LOBSTERAI_ELECTRON_PATH=${env.LOBSTERAI_ELECTRON_PATH || '(not set)'}`);
  coworkLog('INFO', tag, `LOBSTERAI_NPM_BIN_DIR=${env.LOBSTERAI_NPM_BIN_DIR || '(not set)'}`);
  coworkLog('INFO', tag, `HOME=${env.HOME || '(not set)'}`);
}

/**
 * Get SKILLs directory path (handles both development and production)
 */
export function getSkillsRoot(): string {
  if (app.isPackaged) {
    // In production, SKILLs are copied to userData
    return join(app.getPath('userData'), 'SKILLs');
  }

  // In development, __dirname can vary with bundling output (e.g. dist-electron/ or dist-electron/libs/).
  // Resolve from several stable anchors and pick the first existing SKILLs directory.
  const envRoots = [process.env.LOBSTERAI_SKILLS_ROOT, process.env.SKILLS_ROOT]
    .map((value) => value?.trim())
    .filter((value): value is string => Boolean(value));
  const candidates = [
    ...envRoots,
    join(app.getAppPath(), 'SKILLs'),
    join(process.cwd(), 'SKILLs'),
    join(__dirname, '..', 'SKILLs'),
    join(__dirname, '..', '..', 'SKILLs'),
  ];

  for (const candidate of candidates) {
    if (existsSync(candidate)) {
      return candidate;
    }
  }

  // Final fallback for first-run dev environments where SKILLs may not exist yet.
  return join(app.getAppPath(), 'SKILLs');
}

/**
 * Get enhanced environment variables (including proxy configuration)
 * Async function to fetch system proxy and inject into environment variables
 */
export async function getEnhancedEnv(target: OpenAICompatProxyTarget = 'local'): Promise<Record<string, string | undefined>> {
  const config = getCurrentApiConfig(target);
  const env = config
    ? buildEnvForConfig(config)
    : { ...process.env };

  applyPackagedEnvOverrides(env);

  // Inject SKILLs directory path for skill scripts.
  // On Windows, normalise backslashes to forward slashes so the value is usable
  // in both Node.js (which accepts forward slashes) and bash (which treats
  // backslashes as escape characters).
  const skillsRoot = getSkillsRoot().replace(/\\/g, '/');
  env.SKILLS_ROOT = skillsRoot;
  env.LOBSTERAI_SKILLS_ROOT = skillsRoot; // Alternative name for clarity
  if (process.platform === 'win32' || env.LOBSTERAI_NODE_SHIM_ACTIVE === '1') {
    env.LOBSTERAI_ELECTRON_PATH = getElectronNodeRuntimePath().replace(/\\/g, '/');
  } else {
    delete env.LOBSTERAI_ELECTRON_PATH;
  }

  // Skip system proxy resolution if proxy env vars already exist
  if (env.http_proxy || env.HTTP_PROXY || env.https_proxy || env.HTTPS_PROXY) {
    return env;
  }

  // User can disable system proxy from settings.
  if (!isSystemProxyEnabled()) {
    return env;
  }

  // Resolve proxy from system settings
  const { proxyUrl, targetUrl } = await resolveSystemProxyUrlForTargets();
  if (proxyUrl) {
    env.http_proxy = proxyUrl;
    env.https_proxy = proxyUrl;
    env.HTTP_PROXY = proxyUrl;
    env.HTTPS_PROXY = proxyUrl;
    console.log(`[CoworkUtil] Injected system proxy for subprocess via ${targetUrl}:`, proxyUrl);
  }

  return env;
}

/**
 * Ensure the cowork temp directory exists in the given working directory
 * @param cwd Working directory path
 * @returns Path to the temp directory
 */
export function ensureCoworkTempDir(cwd: string): string {
  const tempDir = join(cwd, '.cowork-temp');
  if (!existsSync(tempDir)) {
    try {
      mkdirSync(tempDir, { recursive: true });
      console.log('Created cowork temp directory:', tempDir);
    } catch (error) {
      console.error('Failed to create cowork temp directory:', error);
      // Fall back to cwd if we can't create the temp dir
      return cwd;
    }
  }
  return tempDir;
}

/**
 * Get enhanced environment variables with TMPDIR set to the cowork temp directory
 * This ensures Claude Agent SDK creates temporary files in the user's working directory
 * @param cwd Working directory path
 */
export async function getEnhancedEnvWithTmpdir(
  cwd: string,
  target: OpenAICompatProxyTarget = 'local'
): Promise<Record<string, string | undefined>> {
  const env = await getEnhancedEnv(target);
  const tempDir = ensureCoworkTempDir(cwd);

  // Set temp directory environment variables for all platforms
  env.TMPDIR = tempDir;  // macOS, Linux
  env.TMP = tempDir;     // Windows
  env.TEMP = tempDir;    // Windows

  return env;
}

const SESSION_TITLE_FALLBACK = 'New Chat';
const COWORK_MODEL_PROBE_TIMEOUT_MS = 20000;

type SessionTitleApiConfig =
  | {
      protocol: typeof CoworkModelProtocol.Anthropic;
      apiKey: string;
      baseURL: string;
      model: string;
    }
  | {
      protocol: typeof CoworkModelProtocol.GeminiNative;
      apiKey: string;
      baseURL: string;
      model: string;
    };

function resolveSessionTitleApiConfig(): { config: SessionTitleApiConfig | null; error?: string } {
  const rawResolution = resolveRawApiConfig();
  if (rawResolution.config && rawResolution.providerMetadata?.providerName === 'gemini') {
    return {
      config: {
        protocol: CoworkModelProtocol.GeminiNative,
        apiKey: rawResolution.config.apiKey,
        baseURL: rawResolution.config.baseURL,
        model: rawResolution.config.model,
      },
    };
  }

  const resolution = resolveCurrentApiConfig();
  if (!resolution.config) {
    return {
      config: null,
      error: resolution.error ?? rawResolution.error,
    };
  }

  return {
    config: {
      protocol: CoworkModelProtocol.Anthropic,
      apiKey: resolution.config.apiKey,
      baseURL: resolution.config.baseURL,
      model: resolution.config.model,
    },
  };
}

export async function probeCoworkModelReadiness(
  timeoutMs = COWORK_MODEL_PROBE_TIMEOUT_MS
): Promise<{ ok: true } | { ok: false; error: string }> {
  const { config, error } = resolveSessionTitleApiConfig();
  if (!config) {
    return {
      ok: false,
      error: error || 'API configuration not found.',
    };
  }

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(
      config.protocol === CoworkModelProtocol.GeminiNative
        ? buildGeminiGenerateContentUrl(config.baseURL, config.model)
        : buildAnthropicMessagesUrl(config.baseURL),
      {
        method: 'POST',
        headers: config.protocol === CoworkModelProtocol.GeminiNative
          ? {
              'Content-Type': 'application/json',
              'x-goog-api-key': config.apiKey,
            }
          : {
              'Content-Type': 'application/json',
              'x-api-key': config.apiKey,
              'anthropic-version': '2023-06-01',
            },
        body: JSON.stringify(
          config.protocol === CoworkModelProtocol.GeminiNative
            ? {
                contents: [{ role: 'user', parts: [{ text: 'Reply with "ok".' }] }],
                generationConfig: {
                  maxOutputTokens: 1,
                  temperature: 0,
                },
              }
            : {
                model: config.model,
                max_tokens: 1,
                temperature: 0,
                messages: [{ role: 'user', content: 'Reply with "ok".' }],
              }
        ),
        signal: controller.signal,
      }
    );

    if (!response.ok) {
      const errorText = await response.text().catch(() => '');
      const errorSnippet = extractApiErrorSnippet(errorText);
      return {
        ok: false,
        error: errorSnippet
          ? `Model validation failed (${response.status}): ${errorSnippet}`
          : `Model validation failed with status ${response.status}.`,
      };
    }

    return { ok: true };
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      const timeoutSeconds = Math.ceil(timeoutMs / 1000);
      return {
        ok: false,
        error: `Model validation timed out after ${timeoutSeconds}s.`,
      };
    }
    return {
      ok: false,
      error: `Model validation failed: ${error instanceof Error ? error.message : String(error)}`,
    };
  } finally {
    clearTimeout(timeoutId);
  }
}

export async function generateSessionTitle(
  userIntent: string | null,
  defaultTitle = SESSION_TITLE_FALLBACK
): Promise<string> {
  return buildSessionTitleFromInput(userIntent, defaultTitle);
}
