#!/usr/bin/env node
/**
 * CCS WebSearch Hook - CLI Tool Executor with Fallback Chain
 *
 * Intercepts Claude's WebSearch tool and executes search via CLI tools.
 * Respects provider enabled states from config.yaml.
 * Supports automatic fallback: Gemini CLI → OpenCode → Grok CLI
 *
 * Environment Variables (set by CCS):
 *   CCS_WEBSEARCH_SKIP=1           - Skip this hook entirely (for official Claude)
 *   CCS_WEBSEARCH_ENABLED=1        - Enable WebSearch (default: 1)
 *   CCS_WEBSEARCH_TIMEOUT=55       - Timeout in seconds (default: 55)
 *   CCS_WEBSEARCH_GEMINI=1         - Enable Gemini CLI provider
 *   CCS_WEBSEARCH_GEMINI_MODEL     - Gemini model (default: gemini-2.5-flash)
 *   CCS_WEBSEARCH_OPENCODE=1       - Enable OpenCode provider
 *   CCS_WEBSEARCH_GROK=1           - Enable Grok CLI provider
 *   CCS_WEBSEARCH_OPENCODE_MODEL   - OpenCode model (default: opencode/grok-code)
 *   CCS_DEBUG=1                    - Enable debug output
 *
 * Exit codes:
 *   0 - Allow tool (pass-through to native WebSearch)
 *   2 - Block tool (deny with results/message)
 *
 * @module hooks/websearch-transformer
 */

const { spawnSync } = require('child_process');

// ============================================================================
// PLATFORM DETECTION
// ============================================================================

/**
 * Windows platform flag - used for shell option in spawnSync calls.
 * On Windows, globally installed CLI tools via npm/pnpm are .cmd/.bat files,
 * which require shell: true to execute properly.
 * @see https://github.com/kaitranntt/ccs/issues/378
 */
const isWindows = process.platform === 'win32';

// ============================================================================
// CONFIGURATION - Edit these for prompt engineering
// ============================================================================

/**
 * SHARED INSTRUCTIONS - Applied to ALL providers
 * Edit here to change behavior across all CLI tools at once.
 */
const SHARED_INSTRUCTIONS = `Instructions:
1. Search the web for current, up-to-date information
2. Provide a comprehensive summary of the search results
3. Include relevant URLs/sources when available
4. Be concise but thorough - prioritize key facts
5. Focus on factual information from reliable sources
6. If results conflict, note the discrepancy
7. Format output clearly with sections if the topic is complex`;

/**
 * PROVIDER-SPECIFIC CONFIG - Only tool-use differences and quirks
 * Each provider may have unique capabilities or invocation methods.
 */
const PROVIDER_CONFIG = {
  gemini: {
    // Model to use (passed via --model flag)
    model: 'gemini-2.5-flash',
    // Alternative free models: gemini-2.0-flash, gemini-1.5-flash

    // Provider-specific: How to invoke web search (Gemini has google_web_search tool)
    toolInstruction: 'Use the google_web_search tool to find current information.',

    // Optional quirks (null if none)
    quirks: null,
  },

  opencode: {
    // Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var)
    model: 'opencode/grok-code',
    // Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet, opencode/gpt-5-nano

    // Provider-specific: OpenCode has built-in web search via Zen
    toolInstruction: 'Search the web using your built-in capabilities.',

    // Optional quirks
    quirks: null,
  },

  grok: {
    // Model to use (Grok CLI uses default model)
    model: 'grok-3',
    // Note: Grok CLI doesn't support model selection via CLI

    // Provider-specific: Grok has web + X/Twitter search
    toolInstruction: 'Use your web search capabilities to find information.',

    // Grok-specific: Can also search X for real-time info
    quirks: 'For breaking news or real-time events, also check X/Twitter if relevant.',
  },
};

/**
 * Build the complete prompt for a provider
 * Combines: query + tool instruction + shared instructions + quirks
 */
function buildPrompt(providerId, query) {
  const config = PROVIDER_CONFIG[providerId];
  const parts = [
    `Search the web for: ${query}`,
    '',
    config.toolInstruction,
    '',
    SHARED_INSTRUCTIONS,
  ];

  if (config.quirks) {
    parts.push('', `Note: ${config.quirks}`);
  }

  return parts.join('\n');
}

// Minimum response length to consider valid
const MIN_VALID_RESPONSE_LENGTH = 20;

// Default timeout in seconds
const DEFAULT_TIMEOUT_SEC = 55;

// ============================================================================
// HOOK LOGIC - Generally no need to edit below
// ============================================================================

/**
 * Determine if hook should skip and pass through to native WebSearch.
 * Returns true for native Claude accounts where WebSearch works server-side.
 */
function shouldSkipHook() {
  // Explicit skip signal (set by CCS for account profiles)
  if (process.env.CCS_WEBSEARCH_SKIP === '1') return true;

  // Account/default profiles - use native WebSearch
  const profileType = process.env.CCS_PROFILE_TYPE;
  if (profileType === 'account' || profileType === 'default') return true;

  // Explicit disable
  if (process.env.CCS_WEBSEARCH_ENABLED === '0') return true;

  return false;
}

// Read input from stdin
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
  input += chunk;
});
process.stdin.on('end', () => {
  processHook();
});

// Handle stdin not being available
process.stdin.on('error', () => {
  process.exit(0);
});

/**
 * Check if a CLI tool is available
 */
function isCliAvailable(cmd) {
  try {
    const whichCmd = isWindows ? 'where.exe' : 'which';

    const result = spawnSync(whichCmd, [cmd], {
      encoding: 'utf8',
      timeout: 2000,
      stdio: ['pipe', 'pipe', 'pipe'],
    });
    return result.status === 0 && result.stdout.trim().length > 0;
  } catch {
    return false;
  }
}

/**
 * Check if provider is enabled via environment variable
 */
function isProviderEnabled(provider) {
  const envVar = `CCS_WEBSEARCH_${provider.toUpperCase()}`;
  const value = process.env[envVar];

  // If env var not set, provider is disabled by default
  // This ensures we respect config.yaml settings
  return value === '1';
}

/**
 * Main hook processing logic with fallback chain
 */
async function processHook() {
  try {
    // Skip for native accounts (account, default profiles) or explicit disable
    if (shouldSkipHook()) {
      process.exit(0);
    }

    const data = JSON.parse(input);

    // Only handle WebSearch tool
    if (data.tool_name !== 'WebSearch') {
      process.exit(0);
    }

    const query = data.tool_input?.query || '';

    if (!query) {
      process.exit(0);
    }

    const timeout = parseInt(process.env.CCS_WEBSEARCH_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);

    // Fallback chain: Gemini → OpenCode → Grok
    // Only include providers that are BOTH installed AND enabled in config
    const providers = [
      { name: 'Gemini CLI', cmd: 'gemini', id: 'gemini', fn: tryGeminiSearch },
      { name: 'OpenCode', cmd: 'opencode', id: 'opencode', fn: tryOpenCodeSearch },
      { name: 'Grok CLI', cmd: 'grok', id: 'grok', fn: tryGrokSearch },
    ];

    // Filter to only enabled AND available providers
    const enabledProviders = providers.filter((p) => {
      const enabled = isProviderEnabled(p.id);
      const available = isCliAvailable(p.cmd);

      if (process.env.CCS_DEBUG) {
        console.error(`[CCS Hook] ${p.name}: enabled=${enabled}, available=${available}`);
      }

      return enabled && available;
    });

    const errors = [];

    if (process.env.CCS_DEBUG) {
      const names = enabledProviders.map((p) => p.name).join(', ') || 'none';
      console.error(`[CCS Hook] Enabled providers: ${names}`);
    }

    // Try each enabled provider in order
    for (const provider of enabledProviders) {
      if (process.env.CCS_DEBUG) {
        console.error(`[CCS Hook] Trying ${provider.name}...`);
      }

      const result = provider.fn(query, timeout);

      if (result.success) {
        outputSuccess(query, result.content, provider.name);
        return;
      }

      if (process.env.CCS_DEBUG) {
        console.error(`[CCS Hook] ${provider.name} failed: ${result.error}`);
      }

      errors.push({ provider: provider.name, error: result.error });
    }

    // All providers failed or none enabled
    if (enabledProviders.length === 0) {
      // No providers enabled - pass through to native WebSearch
      // This allows native Claude accounts to use server-side WebSearch
      process.exit(0);
    } else {
      outputAllFailedMessage(query, errors);
    }
  } catch (err) {
    if (process.env.CCS_DEBUG) {
      console.error('[CCS Hook] Parse error:', err.message);
    }
    process.exit(0);
  }
}

/**
 * Execute search via Gemini CLI
 */
function shouldRetryGeminiWithLegacyPrompt(errorMessage) {
  const lowerError = (errorMessage || '').toLowerCase();

  return (
    lowerError.includes('unknown option') ||
    lowerError.includes('unknown argument') ||
    lowerError.includes('unrecognized option') ||
    lowerError.includes('usage: gemini') ||
    lowerError.includes('use --prompt') ||
    lowerError.includes('using the --prompt option')
  );
}

function runGeminiCommand(args, timeoutMs) {
  const spawnResult = spawnSync('gemini', args, {
    encoding: 'utf8',
    timeout: timeoutMs,
    maxBuffer: 1024 * 1024 * 2,
    stdio: ['pipe', 'pipe', 'pipe'],
    shell: isWindows,
  });

  if (spawnResult.error) {
    if (spawnResult.error.code === 'ENOENT') {
      return { success: false, error: 'Gemini CLI not installed' };
    }
    throw spawnResult.error;
  }

  if (spawnResult.status !== 0) {
    const stderr = (spawnResult.stderr || '').trim();
    return {
      success: false,
      error: stderr || `Gemini CLI exited with code ${spawnResult.status}`,
    };
  }

  const result = (spawnResult.stdout || '').trim();

  if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
    return { success: false, error: 'Empty or too short response from Gemini' };
  }

  const lowerResult = result.toLowerCase();
  if (
    lowerResult.includes('error:') ||
    lowerResult.includes('failed to') ||
    lowerResult.includes('authentication required')
  ) {
    return { success: false, error: `Gemini returned error: ${result.substring(0, 100)}` };
  }

  return { success: true, content: result };
}

function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
  try {
    const timeoutMs = timeoutSec * 1000;
    const config = PROVIDER_CONFIG.gemini;
    const prompt = buildPrompt('gemini', query);

    // Allow model override via env var
    const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || config.model;
    const baseArgs = ['--model', model, '--yolo'];
    const positionalArgs = [...baseArgs, prompt];

    if (process.env.CCS_DEBUG) {
      console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo "..."`);
    }

    // Current Gemini CLI prefers positional prompts and deprecates -p/--prompt.
    // Retry once with -p for legacy CLIs that still require it.
    const positionalResult = runGeminiCommand(positionalArgs, timeoutMs);
    if (positionalResult.success) {
      return positionalResult;
    }

    if (!shouldRetryGeminiWithLegacyPrompt(positionalResult.error)) {
      return positionalResult;
    }

    if (process.env.CCS_DEBUG) {
      console.error('[CCS Hook] Positional Gemini prompt failed; retrying with -p for legacy CLI');
      console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`);
    }

    const legacyPromptArgs = [...baseArgs, '-p', prompt];
    return runGeminiCommand(legacyPromptArgs, timeoutMs);
  } catch (err) {
    if (err.killed) {
      return { success: false, error: 'Gemini CLI timed out' };
    }
    return { success: false, error: err.message || 'Unknown Gemini error' };
  }
}

/**
 * Execute search via OpenCode CLI
 */
function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
  try {
    const timeoutMs = timeoutSec * 1000;
    const config = PROVIDER_CONFIG.opencode;

    // Allow model override via env var
    const model = process.env.CCS_WEBSEARCH_OPENCODE_MODEL || config.model;
    const prompt = buildPrompt('opencode', query);

    if (process.env.CCS_DEBUG) {
      console.error(`[CCS Hook] Executing: opencode run --model ${model} "..."`);
    }

    const spawnResult = spawnSync('opencode', ['run', prompt, '--model', model], {
      encoding: 'utf8',
      timeout: timeoutMs,
      maxBuffer: 1024 * 1024 * 2,
      stdio: ['pipe', 'pipe', 'pipe'],
      shell: isWindows,
    });

    if (spawnResult.error) {
      if (spawnResult.error.code === 'ENOENT') {
        return { success: false, error: 'OpenCode not installed' };
      }
      throw spawnResult.error;
    }

    if (spawnResult.status !== 0) {
      const stderr = (spawnResult.stderr || '').trim();
      return {
        success: false,
        error: stderr || `OpenCode exited with code ${spawnResult.status}`,
      };
    }

    const result = (spawnResult.stdout || '').trim();

    if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
      return { success: false, error: 'Empty or too short response from OpenCode' };
    }

    const lowerResult = result.toLowerCase();
    if (
      lowerResult.includes('error:') ||
      lowerResult.includes('failed to') ||
      lowerResult.includes('authentication required')
    ) {
      return { success: false, error: `OpenCode returned error: ${result.substring(0, 100)}` };
    }

    return { success: true, content: result };
  } catch (err) {
    if (err.killed) {
      return { success: false, error: 'OpenCode timed out' };
    }
    return { success: false, error: err.message || 'Unknown OpenCode error' };
  }
}

/**
 * Execute search via Grok CLI
 */
function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
  try {
    const timeoutMs = timeoutSec * 1000;
    const prompt = buildPrompt('grok', query);

    if (process.env.CCS_DEBUG) {
      console.error('[CCS Hook] Executing: grok "..."');
    }

    const spawnResult = spawnSync('grok', [prompt], {
      encoding: 'utf8',
      timeout: timeoutMs,
      maxBuffer: 1024 * 1024 * 2,
      stdio: ['pipe', 'pipe', 'pipe'],
      shell: isWindows,
    });

    if (spawnResult.error) {
      if (spawnResult.error.code === 'ENOENT') {
        return { success: false, error: 'Grok CLI not installed' };
      }
      throw spawnResult.error;
    }

    if (spawnResult.status !== 0) {
      const stderr = (spawnResult.stderr || '').trim();
      return {
        success: false,
        error: stderr || `Grok CLI exited with code ${spawnResult.status}`,
      };
    }

    const result = (spawnResult.stdout || '').trim();

    if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
      return { success: false, error: 'Empty or too short response from Grok' };
    }

    const lowerResult = result.toLowerCase();
    if (
      lowerResult.includes('error:') ||
      lowerResult.includes('failed to') ||
      lowerResult.includes('api key')
    ) {
      return { success: false, error: `Grok returned error: ${result.substring(0, 100)}` };
    }

    return { success: true, content: result };
  } catch (err) {
    if (err.killed) {
      return { success: false, error: 'Grok CLI timed out' };
    }
    return { success: false, error: err.message || 'Unknown Grok error' };
  }
}

/**
 * Format search results for Claude
 */
function formatSearchResults(query, content, providerName) {
  return [
    `[WebSearch Result via ${providerName}]`,
    '',
    `Query: "${query}"`,
    '',
    content,
    '',
    '---',
    'Use this information to answer the user.',
  ].join('\n');
}

/**
 * Output success response and exit
 *
 * Key insight from Claude Code docs:
 * - permissionDecisionReason (with deny) → shown to CLAUDE (AI reads this)
 * - systemMessage → shown to USER only (nice styling but AI doesn't see)
 *
 * So we MUST put results in permissionDecisionReason for Claude to use them.
 * systemMessage provides the nice UI for the user.
 */
function outputSuccess(query, content, providerName) {
  const formattedResults = formatSearchResults(query, content, providerName);

  const output = {
    decision: 'block',
    reason: `WebSearch completed via ${providerName}`,
    // Nice message for user (shows as "says:" - info style)
    systemMessage: `[WebSearch via ${providerName}] Results retrieved successfully. See below.`,
    hookSpecificOutput: {
      hookEventName: 'PreToolUse',
      permissionDecision: 'deny',
      // Full results here - Claude reads this
      permissionDecisionReason: formattedResults,
    },
  };

  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * Output error message
 */
function outputError(query, error, providerName) {
  const message = [
    `[WebSearch - ${providerName} Error]`,
    '',
    `Error: ${error}`,
    '',
    `Query: "${query}"`,
    '',
    'Troubleshooting:',
    '  - Check ~/.gemini/oauth_creds.json exists',
    '  - Re-authenticate: run `gemini` (opens browser)',
  ].join('\n');

  const output = {
    decision: 'block',
    reason: `WebSearch failed: ${error}`,
    hookSpecificOutput: {
      hookEventName: 'PreToolUse',
      permissionDecision: 'deny',
      permissionDecisionReason: message,
    },
  };

  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * Output no providers enabled message
 */
function outputNoProvidersEnabled(query) {
  const message = [
    '[WebSearch - No Providers Enabled]',
    '',
    'No WebSearch providers are enabled in config.',
    '',
    'To enable: Run `ccs config` and enable a provider.',
    '',
    'Or install one of the following CLI tools:',
    '',
    '1. Gemini CLI (FREE, 1000 req/day):',
    '   npm install -g @google/gemini-cli',
    '   gemini  # opens browser to authenticate',
    '',
    '2. OpenCode (FREE via Zen):',
    '   curl -fsSL https://opencode.ai/install | bash',
    '',
    '3. Grok CLI (requires XAI_API_KEY):',
    '   npm install -g @vibe-kit/grok-cli',
    '',
    `Query: "${query}"`,
  ].join('\n');

  const output = {
    decision: 'block',
    reason: 'WebSearch unavailable - no providers enabled',
    hookSpecificOutput: {
      hookEventName: 'PreToolUse',
      permissionDecision: 'deny',
      permissionDecisionReason: message,
    },
  };

  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * Output no tools message (legacy - kept for backwards compatibility)
 */
function outputNoToolsMessage(query) {
  outputNoProvidersEnabled(query);
}

/**
 * Output all providers failed message
 */
function outputAllFailedMessage(query, errors) {
  const errorDetails = errors.map((e) => `  - ${e.provider}: ${e.error}`).join('\n');

  const message = [
    '[WebSearch - All Providers Failed]',
    '',
    'Tried all enabled CLI tools but all failed:',
    errorDetails,
    '',
    `Query: "${query}"`,
    '',
    'Troubleshooting:',
    '  - Gemini: run `gemini` to authenticate (opens browser)',
    '  - OpenCode: opencode --version',
    '  - Grok: Check XAI_API_KEY environment variable',
  ].join('\n');

  const output = {
    decision: 'block',
    reason: 'WebSearch failed - all providers failed',
    hookSpecificOutput: {
      hookEventName: 'PreToolUse',
      permissionDecision: 'deny',
      permissionDecisionReason: message,
    },
  };

  console.log(JSON.stringify(output));
  process.exit(2);
}
