#!/usr/bin/env node
/**
 * CCS Image Analyzer Hook - Read Tool Interceptor
 *
 * Intercepts Claude's Read tool for image/PDF files and analyzes them via CLIProxy.
 * Returns detailed text descriptions instead of allowing direct visual access.
 *
 * Environment Variables (set by CCS):
 *   CCS_IMAGE_ANALYSIS_SKIP=1                 - Skip this hook entirely
 *   CCS_IMAGE_ANALYSIS_ENABLED=1              - Enable image analysis (default: 1)
 *   CCS_IMAGE_ANALYSIS_PROVIDER_MODELS        - Provider:model mapping (e.g., agy:gemini-2.5-flash,gemini:gemini-2.5-flash)
 *   CCS_CURRENT_PROVIDER                      - Current CLIProxy provider (e.g., agy, gemini, codex)
 *   CCS_IMAGE_ANALYSIS_TIMEOUT=60             - Timeout in seconds (default: 60)
 *   CCS_PROFILE_TYPE                          - Profile type (account/default skip)
 *   ANTHROPIC_MODEL                           - Chat model env (not used for image analysis fallback)
 *   CCS_DEBUG=1                               - Enable debug output
 *
 * Exit codes:
 *   0 - Allow tool (pass-through to native Read)
 *   2 - Block tool (deny with analysis/message)
 *
 * @module hooks/image-analyzer-transformer
 */

const fs = require('fs');
const path = require('path');
const http = require('http');

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

const isWindows = process.platform === 'win32';

// ============================================================================
// CONFIGURATION
// ============================================================================

const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.bmp', '.tiff'];
const PDF_EXTENSIONS = ['.pdf'];

const DEFAULT_MODEL = 'gemini-2.5-flash';
const DEFAULT_TIMEOUT_SEC = 60;
const MAX_FILE_SIZE_MB = 10;
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;

const CLIPROXY_HOST = '127.0.0.1';
const CLIPROXY_PORT = parseInt(process.env.CCS_CLIPROXY_PORT || '8317', 10);
const CLIPROXY_PATH = '/v1/messages';
// API key passed via env from cliproxy-executor, defaults to CCS internal key
const CLIPROXY_API_KEY = process.env.CCS_CLIPROXY_API_KEY || 'ccs-internal-managed';

// ============================================================================
// ERROR CODES (for categorization)
// ============================================================================

const ERROR_CODES = {
  FILE_TOO_LARGE: 'FILE_TOO_LARGE',
  CLIPROXY_UNAVAILABLE: 'CLIPROXY_UNAVAILABLE',
  AUTH_FAILED: 'AUTH_FAILED',
  TIMEOUT: 'TIMEOUT',
  RATE_LIMIT: 'RATE_LIMIT',
  API_ERROR: 'API_ERROR',
  PARSE_ERROR: 'PARSE_ERROR',
  UNKNOWN: 'UNKNOWN',
};

// Default analysis prompt
const DEFAULT_PROMPT = `Analyze this image/document thoroughly and provide a detailed description.

Include:
1. Overall content and purpose
2. Text content (if any) - transcribe important text
3. Visual elements (diagrams, charts, UI components)
4. Layout and structure
5. Colors, styling, notable design elements
6. Any actionable information (buttons, links, code)

Be comprehensive - this description replaces direct visual access.`;

// ============================================================================
// HELPER FUNCTIONS
// ============================================================================

/**
 * Output debug information to stderr
 * Only outputs when CCS_DEBUG=1
 */
function debugLog(message, data = {}) {
  if (!process.env.CCS_DEBUG) return;

  const lines = [`[CCS Hook] ${message}`];

  for (const [key, value] of Object.entries(data)) {
    if (value !== undefined && value !== null) {
      lines.push(`  ${key}: ${value}`);
    }
  }

  console.error(lines.join('\n'));
}

/**
 * Get detailed debug context
 */
function getDebugContext(filePath, stats) {
  const currentProvider = process.env.CCS_CURRENT_PROVIDER || 'unknown';
  const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
  const model = providerModels[currentProvider] || DEFAULT_MODEL;
  const timeout = parseInt(process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
  const modelsToTry = getModelsToTry();

  return {
    file: path.basename(filePath),
    size: stats ? `${(stats.size / 1024).toFixed(1)} KB` : 'unknown',
    provider: currentProvider,
    model: model,
    modelsToTry: modelsToTry.length > 1 ? modelsToTry.join(' -> ') : model,
    timeout: `${timeout}s`,
    endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}${CLIPROXY_PATH}`,
  };
}

/**
 * Get current provider/model context for error messages
 */
function getProviderContext() {
  const provider = process.env.CCS_CURRENT_PROVIDER || 'unknown';
  const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
  const model = providerModels[provider] || DEFAULT_MODEL;
  return { provider, model };
}

/**
 * Parse provider_models env var to object
 * Format: provider:model,provider:model
 */
function parseProviderModels(envValue) {
  if (!envValue) return {};
  const result = {};
  envValue.split(',').forEach((pair) => {
    const [provider, model] = pair.split(':');
    if (provider && model && model.trim()) {
      result[provider.trim()] = model.trim();
    }
  });
  return result;
}

/**
 * Extract concatenated text content from CLIProxy response blocks.
 * Skips thinking and other non-text blocks.
 */
function extractTextContent(response) {
  if (!response || !Array.isArray(response.content)) {
    return null;
  }

  const textBlocks = response.content
    .filter((block) => block && block.type === 'text' && typeof block.text === 'string')
    .map((block) => block.text)
    .filter((text) => text.trim());

  if (textBlocks.length === 0) {
    return null;
  }

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

/**
 * Parse raw CLIProxy response body and extract text content.
 */
function parseCliProxyResponse(data) {
  let response;
  try {
    response = JSON.parse(data);
  } catch (err) {
    throw new Error(`Failed to parse response: ${err.message}`);
  }

  const text = extractTextContent(response);
  if (!text) {
    throw new Error('No text content in response');
  }

  return text;
}

/**
 * Get model for current provider from provider_models mapping
 * Returns primary model only (for display/logging)
 */
function getModelForProvider() {
  const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
  const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
  return providerModels[currentProvider] || DEFAULT_MODEL;
}

/**
 * Get list of models to try in order:
 * 1. provider_models[current_provider] (if exists)
 * 2. DEFAULT_MODEL when no provider-specific vision model is configured
 *
 * ANTHROPIC_MODEL is intentionally ignored here because the chat model may
 * not be vision-capable on the current provider route.
 */
function getModelsToTry() {
  const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
  const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);

  const models = [];
  const seen = new Set();

  // 1. Provider-specific model
  if (providerModels[currentProvider]) {
    models.push(providerModels[currentProvider]);
    seen.add(providerModels[currentProvider]);
  }

  // 2. Default model — only when no provider-specific model is configured,
  // since DEFAULT_MODEL (gemini-2.5-flash) may not be routable on the
  // current provider's CLIProxy endpoint (e.g. codex, claude)
  if (models.length === 0 && !seen.has(DEFAULT_MODEL)) {
    models.push(DEFAULT_MODEL);
    seen.add(DEFAULT_MODEL);
  }

  return models;
}

/**
 * Analyze with retry logic - tries models in order until one succeeds
 */
async function analyzeWithRetry(base64Data, mediaType, timeoutMs) {
  const models = getModelsToTry();
  // Defensive check - should never happen but provides clear error
  if (models.length === 0) {
    throw new Error('No models configured for image analysis');
  }
  let lastError = null;

  for (let i = 0; i < models.length; i++) {
    const model = models[i];
    try {
      debugLog(`Trying model ${i + 1}/${models.length}`, { model });
      const result = await analyzeViaCliProxy(base64Data, mediaType, model, timeoutMs);
      if (i > 0) {
        debugLog('Retry succeeded', { model, attempt: i + 1 });
      }
      return { description: result, model };
    } catch (err) {
      lastError = err;
      const isLastModel = i === models.length - 1;

      // Don't retry on certain errors (auth, rate limit, timeout, file access, network)
      const errMsg = err.message || '';
      const noRetryPatterns = [
        'AUTH_ERROR', 'RATE_LIMIT', 'TIMEOUT',
        'EACCES', 'EPERM', 'ECONNREFUSED',
        'ENOTFOUND', 'ENETUNREACH', 'EAI_AGAIN'  // Network errors - no point retrying
      ];
      const shouldNotRetry = noRetryPatterns.some(p => errMsg.includes(p));

      if (shouldNotRetry || isLastModel) {
        debugLog('Analysis failed, no more retries', {
          model,
          error: errMsg,
          reason: shouldNotRetry ? 'non-retryable error' : 'last model'
        });
        throw err;
      }

      debugLog('Model failed, trying next', {
        model,
        error: errMsg.substring(0, 100),
        nextModel: models[i + 1]
      });
    }
  }

  throw lastError || new Error('No models available');
}

/**
 * Check if file is an analyzable image or PDF
 */
function isAnalyzableFile(filePath) {
  const ext = path.extname(filePath).toLowerCase();
  return IMAGE_EXTENSIONS.includes(ext) || PDF_EXTENSIONS.includes(ext);
}

/**
 * Get MIME type from file extension
 */
function getMediaType(filePath) {
  const ext = path.extname(filePath).toLowerCase();
  const mimeTypes = {
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.png': 'image/png',
    '.gif': 'image/gif',
    '.webp': 'image/webp',
    '.heic': 'image/heic',
    '.bmp': 'image/bmp',
    '.tiff': 'image/tiff',
    '.pdf': 'application/pdf',
  };
  return mimeTypes[ext] || 'application/octet-stream';
}

/**
 * Encode file to base64
 */
function encodeFileToBase64(filePath) {
  const content = fs.readFileSync(filePath);
  return content.toString('base64');
}

/**
 * Check if CLIProxy is available
 */
function isCliProxyAvailable() {
  return new Promise((resolve) => {
    const req = http.request(
      {
        hostname: CLIPROXY_HOST,
        port: CLIPROXY_PORT,
        path: '/',
        method: 'GET',
        timeout: 2000,
      },
      (res) => {
        resolve(res.statusCode >= 200 && res.statusCode < 500);
      }
    );

    req.on('error', () => resolve(false));
    req.on('timeout', () => {
      req.destroy();
      resolve(false);
    });

    req.end();
  });
}

/**
 * Analyze file via CLIProxy vision API
 */
function analyzeViaCliProxy(base64Data, mediaType, model, timeoutMs) {
  return new Promise((resolve, reject) => {
    const requestBody = JSON.stringify({
      model: model,
      max_tokens: 4096,
      messages: [
        {
          role: 'user',
          content: [
            { type: 'text', text: DEFAULT_PROMPT },
            {
              type: 'image',
              source: {
                type: 'base64',
                media_type: mediaType,
                data: base64Data,
              },
            },
          ],
        },
      ],
    });

    const req = http.request(
      {
        hostname: CLIPROXY_HOST,
        port: CLIPROXY_PORT,
        path: CLIPROXY_PATH,
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(requestBody),
          'x-api-key': CLIPROXY_API_KEY,
        },
        timeout: timeoutMs,
      },
      (res) => {
        let data = '';

        res.on('data', (chunk) => {
          data += chunk;
        });

        res.on('error', (err) => {
          reject(err);
        });

        res.on('end', () => {
          // Categorize by status code
          if (res.statusCode === 401 || res.statusCode === 403) {
            reject(new Error(`AUTH_ERROR:${res.statusCode}`));
            return;
          }

          if (res.statusCode === 429) {
            const retryAfter = res.headers['retry-after'];
            reject(new Error(`RATE_LIMIT:${retryAfter || ''}`));
            return;
          }

          if (res.statusCode !== 200) {
            reject(new Error(`API_ERROR:${res.statusCode}:${data}`));
            return;
          }

          if (!data || !data.trim()) {
            reject(new Error('Empty response from CLIProxy'));
            return;
          }

          try {
            const text = parseCliProxyResponse(data);
            resolve(text);
          } catch (err) {
            reject(err);
          }
        });
      }
    );

    req.on('error', (err) => reject(err));
    req.on('timeout', () => {
      req.destroy();
      reject(new Error('TIMEOUT'));
    });

    req.write(requestBody);
    req.end();
  });
}

/**
 * Format analysis description for Claude (matches websearch format)
 */
function formatDescription(filePath, description, model, fileSize) {
  const sizeKB = fileSize ? (fileSize / 1024).toFixed(1) : '?';
  return [
    `[Image Analysis via CLIProxy]`,
    '',
    `File: ${path.basename(filePath)} (${sizeKB} KB)`,
    `Model: ${model}`,
    '',
    '---',
    '',
    description,
    '',
    '---',
    '*Use this description to understand the image content.*',
  ].join('\n');
}

// ============================================================================
// SPECIALIZED ERROR HANDLERS
// ============================================================================

/**
 * Format error output for Claude hook
 */
function formatErrorOutput(filePath, errorCode, message, troubleshooting) {
  const { provider, model } = getProviderContext();

  const lines = [
    `[Image Analysis - Error]`,
    '',
    `File: ${path.basename(filePath)}`,
    `Provider: ${provider} | Model: ${model}`,
    '',
    `Error: ${message}`,
  ];

  if (troubleshooting && troubleshooting.length > 0) {
    lines.push('');
    lines.push('Troubleshooting:');
    troubleshooting.forEach((step, i) => {
      lines.push(`  ${i + 1}. ${step}`);
    });
  }

  lines.push('');
  lines.push('For help: ccs config image-analysis --help');

  return {
    decision: 'block',
    reason: `Image analysis failed: ${errorCode}`,
    systemMessage: `[Image Analysis] Failed: ${message}`,
    hookSpecificOutput: {
      hookEventName: 'PreToolUse',
      permissionDecision: 'deny',
      permissionDecisionReason: lines.join('\n'),
    },
  };
}

/**
 * File too large error
 */
function outputFileTooLargeError(filePath, actualSizeMB, maxSizeMB) {
  const output = formatErrorOutput(
    filePath,
    ERROR_CODES.FILE_TOO_LARGE,
    `File too large (${actualSizeMB.toFixed(2)}MB > ${maxSizeMB}MB limit)`,
    [
      'Reduce image resolution or use compression',
      'For screenshots: use PNG optimizer (pngquant, optipng)',
      'For photos: resize to max 2048px width',
      `Current limit: ${maxSizeMB}MB per file`,
    ]
  );
  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * CLIProxy unavailable error
 */
function outputCliProxyUnavailableError(filePath, endpoint) {
  const output = formatErrorOutput(
    filePath,
    ERROR_CODES.CLIPROXY_UNAVAILABLE,
    `CLIProxy not available at ${endpoint}`,
    [
      'CLIProxy service may not be running',
      'Start with: ccs config (opens dashboard, starts CLIProxy)',
      'Or manually: ccs cliproxy start',
      `Verify: curl ${endpoint}`,
      'Check status: ccs doctor',
    ]
  );
  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * Authentication error
 */
function outputAuthError(filePath, statusCode) {
  const { provider } = getProviderContext();
  const output = formatErrorOutput(
    filePath,
    ERROR_CODES.AUTH_FAILED,
    `Authentication failed (HTTP ${statusCode})`,
    [
      `Re-authenticate: ccs ${provider} --auth`,
      `Check accounts: ccs ${provider} --accounts`,
      'Verify OAuth token is valid',
      'Check: ccs doctor',
    ]
  );
  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * Timeout error
 */
function outputTimeoutError(filePath, timeoutSec) {
  const { model } = getProviderContext();
  const output = formatErrorOutput(
    filePath,
    ERROR_CODES.TIMEOUT,
    `Request timed out after ${timeoutSec}s`,
    [
      'Large files or complex images take longer',
      `Increase timeout: ccs config image-analysis --timeout ${timeoutSec * 2}`,
      'Or via env: CCS_IMAGE_ANALYSIS_TIMEOUT=120',
      `Current model (${model}) may be slow - try a faster variant`,
      'Check CLIProxy health: curl http://127.0.0.1:8317',
    ]
  );
  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * Rate limit error
 */
function outputRateLimitError(filePath, retryAfterSec) {
  const { provider } = getProviderContext();
  const retryHint = retryAfterSec ? `Retry after ${retryAfterSec}s` : 'Wait a moment and retry';
  const output = formatErrorOutput(
    filePath,
    ERROR_CODES.RATE_LIMIT,
    'Rate limit exceeded',
    [
      retryHint,
      `Provider ${provider} has usage limits`,
      'Consider switching accounts: ccs ' + provider + ' --accounts',
      'Check quota: ccs cliproxy doctor',
    ]
  );
  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * Generic API error
 */
function outputApiError(filePath, statusCode, responseBody) {
  // Try to extract error message from response
  let errorDetail = `HTTP ${statusCode}`;
  try {
    const parsed = JSON.parse(responseBody);
    if (parsed.error?.message) {
      errorDetail = parsed.error.message;
    } else if (parsed.message) {
      errorDetail = parsed.message;
    }
  } catch {
    // Use raw body if not JSON (truncated)
    if (responseBody && responseBody.length < 100) {
      errorDetail = responseBody;
    }
  }

  const output = formatErrorOutput(
    filePath,
    ERROR_CODES.API_ERROR,
    `API error: ${errorDetail}`,
    [
      'Check CLIProxy logs: ccs cleanup --show-logs',
      'Verify provider is authenticated: ccs doctor',
      'Try a different provider or model',
      'Report persistent issues: https://github.com/kaitranntt/ccs/issues',
    ]
  );
  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * File permission error
 */
function outputFileAccessError(filePath, error) {
  const output = formatErrorOutput(
    filePath,
    ERROR_CODES.UNKNOWN,
    `File access denied: ${error}`,
    [
      'Check file permissions: ls -l ' + filePath,
      isWindows ? 'Run terminal as Administrator if needed' : 'Use sudo or adjust file ownership',
      'Verify file is readable by current user',
      'Move file to accessible location',
    ]
  );
  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * Unknown/fallback error (replaces old outputError)
 */
function outputUnknownError(filePath, error) {
  const output = formatErrorOutput(
    filePath,
    ERROR_CODES.UNKNOWN,
    error || 'Unknown error occurred',
    [
      'Check CLIProxy is running: curl http://127.0.0.1:8317',
      'Verify authentication: ccs doctor',
      'Check file is valid image/PDF',
      'Enable debug: CCS_DEBUG=1 ccs <provider>',
    ]
  );
  console.log(JSON.stringify(output));
  process.exit(2);
}

/**
 * CLIProxy unavailable fallback - blocks Read to prevent context overflow
 * When CLIProxy is not running, we cannot analyze the image.
 * Blocking prevents the image from loading into Claude's context (100K+ tokens).
 */
function outputCliProxyUnavailableFallback(filePath) {
  const fileName = filePath.split(/[/\\]/).pop() || filePath;

  // Keep message minimal to avoid context pollution and hallucination
  const message = [
    '[Image Read Blocked]',
    '',
    `File: ${fileName}`,
    '',
    'CLIProxy unavailable. Image blocked to prevent context overflow.',
  ].join('\n');

  const output = {
    decision: 'block',
    reason: 'CLIProxy unavailable - image blocked to prevent context overflow',
    systemMessage: `[Image Blocked] ${fileName} - CLIProxy unavailable. Start: ccs config`,
    hookSpecificOutput: {
      hookEventName: 'PreToolUse',
      permissionDecision: 'deny',
      permissionDecisionReason: message,
    },
  };

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

/**
 * Output success response and exit
 */
function outputSuccess(filePath, description, model, fileSize) {
  debugLog('Returning analysis result', {
    file: path.basename(filePath),
    model: model,
    descriptionLength: `${description.length} chars`,
  });

  const formattedDescription = formatDescription(filePath, description, model, fileSize);

  const output = {
    decision: 'block',
    reason: `Image analyzed: ${path.basename(filePath)}`,
    systemMessage: `[Image Analysis] ${path.basename(filePath)} analyzed via CLIProxy (${model})`,
    hookSpecificOutput: {
      hookEventName: 'PreToolUse',
      permissionDecision: 'deny',
      permissionDecisionReason: formattedDescription,
    },
  };

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

/**
 * Determine if hook should skip, with debug logging
 */
function shouldSkipHook() {
  // Explicit skip signal
  if (process.env.CCS_IMAGE_ANALYSIS_SKIP === '1') {
    debugLog('Skipping: CCS_IMAGE_ANALYSIS_SKIP=1');
    return true;
  }

  // Explicit disable
  if (process.env.CCS_IMAGE_ANALYSIS_ENABLED === '0') {
    debugLog('Skipping: image analysis disabled (CCS_IMAGE_ANALYSIS_ENABLED=0)');
    return true;
  }

  // Account/default profiles - use native Read
  const profileType = process.env.CCS_PROFILE_TYPE;
  if (profileType === 'account' || profileType === 'default') {
    debugLog(`Skipping: profile type "${profileType}" uses native Read`);
    return true;
  }

  // Check if current provider has a vision model configured
  const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
  const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);

  if (!providerModels[currentProvider]) {
    debugLog(`Skipping: provider "${currentProvider}" not in provider_models`, {
      configured_providers: Object.keys(providerModels).join(', ') || 'none',
    });
    return true;
  }

  return false;
}

// ============================================================================
// MAIN HOOK LOGIC
// ============================================================================

// 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);
});

/**
 * Main hook processing logic
 *
 * Two-phase design: Phase 1 filters non-image Read calls silently (exit 0).
 * Phase 2 only runs for confirmed image/PDF files, so error messages are
 * always relevant and never confuse users reading code or text files.
 */
async function processHook() {
  // Phase 1: Fast bail-out for non-image files
  // Any failure here → pass through silently to native Read
  let filePath;
  try {
    const data = JSON.parse(input);

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

    filePath = data.tool_input?.file_path || '';

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

    // Check file extension BEFORE any other processing — this is the key gate
    // that ensures non-image Read calls never see hook errors
    if (!isAnalyzableFile(filePath)) {
      process.exit(0);
    }
  } catch {
    // stdin parse failure or unexpected error → pass through silently
    process.exit(0);
  }

  // Phase 2: Image/PDF file processing — errors here are relevant to the user
  try {
    // Skip for native accounts or explicit disable
    if (shouldSkipHook()) {
      process.exit(0);
    }

    // Check if file exists
    if (!fs.existsSync(filePath)) {
      // Let native Read handle the error
      process.exit(0);
    }

    // Check file size
    const stats = fs.statSync(filePath);
    if (stats.size >= MAX_FILE_SIZE_BYTES) {
      outputFileTooLargeError(filePath, stats.size / 1024 / 1024, MAX_FILE_SIZE_MB);
      return;
    }

    // Check CLIProxy availability
    const cliProxyAvailable = await isCliProxyAvailable();
    if (!cliProxyAvailable) {
      debugLog('Blocking: CLIProxy not available', {
        endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}`,
        action: 'blocking to prevent context overflow',
      });
      outputCliProxyUnavailableFallback(filePath);
      return;
    }

    const model = getModelForProvider();
    const timeout = parseInt(process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
    const timeoutMs = Math.max(1, Math.min(600, timeout)) * 1000;

    // Get debug context before analysis
    const debugContext = getDebugContext(filePath, stats);
    debugLog('Starting image analysis', debugContext);

    // Encode file to base64
    const base64Data = encodeFileToBase64(filePath);
    const mediaType = getMediaType(filePath);

    debugLog('File encoded', {
      mediaType: mediaType,
      base64Length: `${(base64Data.length / 1024).toFixed(1)}KB`,
    });

    // Analyze via CLIProxy with retry logic
    const { description, model: usedModel } = await analyzeWithRetry(base64Data, mediaType, timeoutMs);

    debugLog('Analysis complete', {
      responseLength: `${description.length} chars`,
      model: usedModel,
    });

    // Output success
    outputSuccess(filePath, description, usedModel, stats.size);
  } catch (err) {
    if (process.env.CCS_DEBUG) {
      console.error('[CCS Hook] Error:', err.message);
    }

    // filePath is guaranteed set by Phase 1 — only image files reach here

    // Categorize error by message pattern
    const errMsg = err.message || '';

    if (errMsg.startsWith('AUTH_ERROR:')) {
      const statusCode = parseInt(errMsg.split(':')[1], 10);
      outputAuthError(filePath, statusCode);
    } else if (errMsg.startsWith('RATE_LIMIT:')) {
      const retryAfter = errMsg.split(':')[1];
      outputRateLimitError(filePath, retryAfter ? parseInt(retryAfter, 10) : null);
    } else if (errMsg.startsWith('API_ERROR:')) {
      const parts = errMsg.split(':');
      const statusCode = parseInt(parts[1], 10);
      const body = parts.slice(2).join(':');
      outputApiError(filePath, statusCode, body);
    } else if (errMsg === 'TIMEOUT' || errMsg.includes('timed out') || errMsg.includes('timeout')) {
      const timeout = parseInt(process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
      outputTimeoutError(filePath, timeout);
    } else if (errMsg.includes('ECONNREFUSED') || errMsg.includes('ENOTFOUND')) {
      outputCliProxyUnavailableError(filePath, `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}`);
    } else if (errMsg.includes('EACCES') || errMsg.includes('EPERM')) {
      outputFileAccessError(filePath, errMsg);
    } else {
      outputUnknownError(filePath, errMsg);
    }
  }
}
