/**
 * LLM Provider — Claude CLI only (Team subscription, zero per-token cost)
 *
 * Uses `claude --print` for all LLM calls.
 *
 * Interface:
 *   provider.messages.create(params)  → Promise<Response>
 *   provider.messages.stream(params)  → StreamObject (async iterable + finalMessage + abort)
 *
 * MCP Integration:
 *   When params.tools is provided, CLIProvider automatically:
 *   1. Generates a temp MCP config pointing to VLCode's MCP server
 *   2. Passes --mcp-config to claude CLI
 *   3. Claude CLI discovers VLCode tools via MCP and handles the full agent loop
 *   4. Returns the final result (all tool calls happen inside Claude CLI)
 */
import { spawn, spawnSync } from 'child_process';
import { writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { createHash } from 'crypto';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const MCP_SERVER_PATH = join(__dirname, '..', 'mcp', 'mcp-server.js');
const _providerInstanceCache = new Map();
const _mcpConfigPathCache = new Map();

function getCachedMCPConfigPath(serverPort) {
  if (!serverPort) return null;
  const cacheKey = String(serverPort);
  const cached = _mcpConfigPathCache.get(cacheKey);
  if (cached) return cached;

  const mcpConfig = {
    mcpServers: {
      vlcode: {
        command: 'node',
        args: [MCP_SERVER_PATH, '--port', cacheKey],
      },
    },
  };
  const mcpConfigPath = join(tmpdir(), `vlcode-mcp-port-${cacheKey}.json`);
  writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig));
  _mcpConfigPathCache.set(cacheKey, mcpConfigPath);
  return mcpConfigPath;
}

// ─── CLI Provider ────────────────────────────────────────────────────────────
/**
 * Wraps `claude --print` CLI — uses Team OAuth subscription (zero per-token cost).
 *
 * Two modes of operation:
 *
 * 1. Simple mode (no tools): direct `claude --print -s "system" "message"`
 *    - Used by: workflow executor, generation pipeline, context compression
 *    - Fast, single-turn text generation
 *
 * 2. MCP mode (with tools): `claude --print --mcp-config ... --allowedTools ...`
 *    - Used by: orchestrator (when tools are needed)
 *    - Claude CLI discovers VLCode tools via MCP server
 *    - Claude CLI handles the full agent loop internally (multi-turn tool calls)
 *    - Returns final result after all tool calls complete
 *    - VLCode tools are executed via MCP → HTTP → VLCode tool registry
 */
export class CLIProvider {
  constructor(config) {
    this.config = config;
    this.model = config.model || 'claude-sonnet-4-6';
    this.name = 'cli';
    this.messages = {
      create: (params) => this._create(params),
      stream: (params) => this._stream(params),
    };
  }

  getServerPort() {
    return this.config?.port || 3200;
  }

  /**
   * Non-streaming call via `claude --print`.
   * Returns Anthropic-compatible response object.
   */
  async _create(params) {
    const text = await this._callCLI(params);
    return {
      content: [{ type: 'text', text }],
      stop_reason: 'end_turn',
      usage: { input_tokens: 0, output_tokens: 0 },
    };
  }

  /**
   * Streaming call via `claude --print --output-format stream-json`.
   * Returns an object that mimics Anthropic's MessageStream:
   *   - async iterable (yields content_block_start/delta/stop events)
   *   - .finalMessage() → Promise<Response>
   *   - .abort()
   *
   * Uses real streaming from Claude CLI — events are emitted in real-time
   * as the CLI outputs stream-json lines containing Anthropic-compatible events.
   */
  _stream(params) {
    const { system, messages, max_tokens, model, tools } = params;
    const useModel = model || this.model;
    const userMessage = this._serializeMessages(messages);
    const systemText = this._extractSystemText(system);

    const args = ['--print', '--no-session-persistence', '--output-format', 'stream-json', '--verbose', '--include-partial-messages'];

    // ── No-tool mode: disable all built-in tools for pure generation calls ──
    if (!tools || tools.length === 0) {
      args.push('--tools', '');
    }

    // ── MCP mode: when tools are provided, enable MCP server ──
    const serverPort = this.getServerPort();
    if (tools?.length > 0 && serverPort) {
      const mcpConfigPath = getCachedMCPConfigPath(serverPort);

      args.push('--mcp-config', mcpConfigPath);
      args.push('--strict-mcp-config');

      // Block Claude Code's built-in tools so the AI only sees VLCode MCP tools.
      // Without this, the AI gets confused by duplicate functionality (e.g., uses built-in
      // ToolSearch to search for VLCompile, which fails because it's an MCP tool).
      args.push('--disallowedTools', 'Read,Edit,Write,Bash,Grep,Glob,Agent,TodoWrite,ToolSearch,NotebookEdit,WebFetch,WebSearch,AskFollowupQuestion');

      const mcpToolNames = tools.map(t => `mcp__vlcode__${t.name}`);
      args.push('--allowedTools', mcpToolNames.join(','));
      args.push('--permission-mode', 'bypassPermissions');
    }

    // ── System prompt ──
    // CLI arg limit: macOS ARG_MAX ~1MB total, but with MCP tool names etc. we keep it safe.
    // If system prompt fits in args (<100KB), pass directly. Otherwise, prepend to stdin message.
    let stdinPrefix = '';
    if (systemText) {
      if (systemText.length < 100000) {
        args.push('--system-prompt', systemText);
      } else {
        // Too long for CLI arg — pass a short summary as system prompt,
        // and prepend the FULL system prompt to the user message via stdin.
        args.push('--append-system-prompt', systemText.slice(0, 4000) + '\n\n[Full VL specification and instructions are provided at the start of the user message below. Follow them precisely.]');
        stdinPrefix = `<system-context>\n${systemText}\n</system-context>\n\n`;
      }
    }

    if (useModel) args.push('--model', useModel);

    // Timeout: overridable for long workflow prompts
    const timeout = Number(process.env.VL_CLAUDE_TIMEOUT_MS || 600000);

    // Combine system prefix with user message for stdin
    const stdinData = stdinPrefix + (userMessage || '');

    // ── Spawn CLI process with real-time streaming ──
    const env = { ...process.env, NO_PROXY: 'localhost,127.0.0.1,::1' };
    delete env.CLAUDECODE;

    const proc = spawn('claude', args, {
      stdio: ['pipe', 'pipe', 'pipe'],
      env,
    });

    let aborted = false;
    let timedOut = false;
    const timer = setTimeout(() => {
      timedOut = true;
      proc.kill('SIGTERM');
    }, timeout);

    // Event queue for async iteration
    const eventQueue = [];
    let resolveWaiting = null;
    let streamDone = false;
    let finalMsg = null;
    let resultUsage = { input_tokens: 0, output_tokens: 0 };
    let resultText = '';

    const pushEvent = (evt) => {
      if (aborted) return;
      if (resolveWaiting) {
        const r = resolveWaiting;
        resolveWaiting = null;
        r(evt);
      } else {
        eventQueue.push(evt);
      }
    };

    const finishStream = () => {
      streamDone = true;
      if (resolveWaiting) {
        const r = resolveWaiting;
        resolveWaiting = null;
        r(null); // signal done
      }
    };

    // Parse stdout line by line for stream-json events
    let lineBuf = '';
    proc.stdout.on('data', chunk => {
      lineBuf += chunk.toString();
      let nlIdx;
      while ((nlIdx = lineBuf.indexOf('\n')) !== -1) {
        const line = lineBuf.slice(0, nlIdx).trim();
        lineBuf = lineBuf.slice(nlIdx + 1);
        if (!line) continue;
        try {
          const obj = JSON.parse(line);
          if (obj.type === 'stream_event' && obj.event) {
            // Forward Anthropic-compatible streaming events directly
            pushEvent(obj.event);
          } else if (obj.type === 'assistant' && obj.message) {
            // Full assistant message — extract for finalMessage
            finalMsg = obj.message;
            if (obj.message.content) {
              for (const block of obj.message.content) {
                if (block.type === 'text') resultText += block.text;
              }
            }
          } else if (obj.type === 'result') {
            // Final result — extract usage
            if (obj.usage) {
              resultUsage = {
                input_tokens: obj.usage.input_tokens || 0,
                output_tokens: obj.usage.output_tokens || 0,
                cache_creation_input_tokens: obj.usage.cache_creation_input_tokens || 0,
                cache_read_input_tokens: obj.usage.cache_read_input_tokens || 0,
              };
            }
          }
        } catch {}
      }
    });

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

    if (stdinData) {
      proc.stdin.write(stdinData);
    }
    proc.stdin.end();

    proc.on('close', code => {
      clearTimeout(timer);
      if (timedOut) {
        pushEvent({ type: '_error', message: `claude CLI timed out after ${timeout / 1000}s` });
      } else if (code !== 0 && code !== null && !aborted) {
        // code=null means killed by signal (SIGTERM/SIGKILL) — not an error if we aborted
        pushEvent({ type: '_error', message: `claude CLI exited with code ${code}: ${stderr.slice(0, 500)}` });
      }
      finishStream();
    });

    proc.on('error', err => {
      clearTimeout(timer);
      pushEvent({ type: '_error', message: `Failed to spawn claude CLI: ${err.message}` });
      finishStream();
    });

    const streamObj = {
      [Symbol.asyncIterator]() {
        return {
          async next() {
            if (aborted) return { done: true };

            // Drain queued events first
            if (eventQueue.length > 0) {
              const evt = eventQueue.shift();
              if (evt.type === '_error') throw new Error(evt.message);
              return { value: evt, done: false };
            }

            if (streamDone) return { done: true };

            // Wait for next event
            const evt = await new Promise(r => { resolveWaiting = r; });
            if (evt === null) return { done: true };
            if (evt.type === '_error') throw new Error(evt.message);
            return { value: evt, done: false };
          },
        };
      },
      finalMessage: async () => {
        // Wait for stream to finish if not done yet
        if (!streamDone) {
          await new Promise(r => {
            const check = () => { if (streamDone) r(); else setTimeout(check, 50); };
            check();
          });
        }
        if (finalMsg) {
          return {
            content: finalMsg.content || [{ type: 'text', text: resultText }],
            stop_reason: finalMsg.stop_reason || 'end_turn',
            usage: resultUsage,
          };
        }
        return {
          content: [{ type: 'text', text: resultText }],
          stop_reason: 'end_turn',
          usage: resultUsage,
        };
      },
      abort: () => {
        aborted = true;
        try { proc.kill('SIGTERM'); } catch {}
        clearTimeout(timer);
      },
    };
    return streamObj;
  }

  /**
   * Core CLI execution — spawns `claude --print` with system prompt and message.
   *
   * When params.tools is provided (orchestrator mode):
   *   - Generates MCP config pointing to VLCode's MCP server
   *   - Passes --mcp-config to claude CLI
   *   - Claude CLI handles the full agent loop with MCP tools
   *
   * When no tools (workflow/generation mode):
   *   - Simple `claude --print -s "system" < stdin`
   */
  async _callCLI(params) {
    const { system, messages, max_tokens, model, tools } = params;
    const useModel = model || this.model;
    const userMessage = this._serializeMessages(messages);
    const systemText = this._extractSystemText(system);

    const args = ['--print', '--no-session-persistence'];

    // ── No-tool mode: disable all built-in tools for pure generation calls ──
    if (!tools || tools.length === 0) {
      args.push('--tools', '');
    }

    // ── MCP mode: when tools are provided, enable MCP server ──
    const serverPort = this.getServerPort();
    if (tools?.length > 0 && serverPort) {
      const mcpConfigPath = getCachedMCPConfigPath(serverPort);

      args.push('--mcp-config', mcpConfigPath);
      args.push('--strict-mcp-config');  // Only use our MCP, ignore user's

      // Block Claude Code's built-in tools so the AI only sees VLCode MCP tools
      args.push('--disallowedTools', 'Read,Edit,Write,Bash,Grep,Glob,Agent,TodoWrite,ToolSearch,NotebookEdit,WebFetch,WebSearch,AskFollowupQuestion');

      // Allow all VLCode MCP tools + built-in tools for file ops
      const mcpToolNames = tools.map(t => `mcp__vlcode__${t.name}`);
      args.push('--allowedTools', mcpToolNames.join(','));

      // Auto-accept permissions in --print mode for MCP tools
      args.push('--permission-mode', 'bypassPermissions');
    }

    // ── System prompt ──
    // Same logic as _stream: if too long for CLI arg, prepend to stdin
    let stdinPrefix = '';
    if (systemText) {
      if (systemText.length < 100000) {
        args.push('--system-prompt', systemText);
      } else {
        args.push('--append-system-prompt', systemText.slice(0, 4000) + '\n\n[Full VL specification and instructions are provided at the start of the user message below. Follow them precisely.]');
        stdinPrefix = `<system-context>\n${systemText}\n</system-context>\n\n`;
      }
    }

    if (useModel) args.push('--model', useModel);
    // Note: claude CLI doesn't have --max-tokens flag; it manages output length internally

    // Timeout: overridable for long workflow prompts
    const defaultTimeout = tools?.length > 0 ? 600000 : 300000;
    const timeout = Number(process.env.VL_CLAUDE_TIMEOUT_MS || defaultTimeout);
    const stdinData = stdinPrefix + (userMessage || '');
    const result = await this._exec(args, stdinData, timeout);
    return result;
  }

  /** Extract plain text from Anthropic system prompt format */
  _extractSystemText(system) {
    if (!system) return '';
    if (typeof system === 'string') return system;
    if (Array.isArray(system)) {
      return system.map(s => {
        if (typeof s === 'string') return s;
        if (s.type === 'text') return s.text;
        return '';
      }).filter(Boolean).join('\n\n');
    }
    return '';
  }

  /** Serialize Anthropic messages array into a single prompt string */
  _serializeMessages(messages) {
    if (!messages || messages.length === 0) return '';

    // Single user message — extract text directly
    if (messages.length === 1 && messages[0].role === 'user') {
      return this._extractMessageText(messages[0]);
    }

    // Multi-turn — serialize as labeled conversation
    return messages.map(msg => {
      const role = msg.role === 'assistant' ? 'Assistant' : 'Human';
      const text = this._extractMessageText(msg);
      return `${role}:\n${text}`;
    }).join('\n\n');
  }

  /** Extract text content from a single message */
  _extractMessageText(msg) {
    const content = msg.content;
    if (typeof content === 'string') return content;
    if (Array.isArray(content)) {
      return content
        .filter(b => b.type === 'text' || b.type === 'tool_result')
        .map(b => b.text || b.content || '')
        .filter(Boolean)
        .join('\n');
    }
    return '';
  }

  /**
   * Spawn `claude` CLI and return stdout.
   * User message is piped via stdin to avoid ARG_MAX issues.
   */
  _exec(args, stdinData, timeout = 300000) {
    return new Promise((resolve, reject) => {
      // Build clean env: remove CLAUDECODE to allow nested CLI spawning,
      // and ensure NO_PROXY bypasses proxy for localhost
      const env = { ...process.env, NO_PROXY: 'localhost,127.0.0.1,::1' };
      delete env.CLAUDECODE;

      const proc = spawn('claude', args, {
        stdio: ['pipe', 'pipe', 'pipe'],
        env,
      });

      let stdout = '';
      let stderr = '';
      let timedOut = false;

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

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

      if (stdinData) {
        proc.stdin.write(stdinData);
      }
      proc.stdin.end();

      proc.on('close', code => {
        clearTimeout(timer);
        if (timedOut) {
          reject(new Error(`claude CLI timed out after ${timeout / 1000}s`));
        } else if (code !== 0 && code !== null) {
          reject(new Error(`claude CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
        } else {
          resolve(stdout.trim());
        }
      });

      proc.on('error', err => {
        clearTimeout(timer);
        reject(new Error(`Failed to spawn claude CLI: ${err.message}. Is claude installed? Run: npm install -g @anthropic-ai/claude-code`));
      });
    });
  }
}

// ─── API Provider (Direct Anthropic SDK) ─────────────────────────────────────

/**
 * Direct Anthropic API provider — uses @anthropic-ai/sdk.
 * Used when ANTHROPIC_API_KEY is set or config.apiKey is provided.
 */
export class APIProvider {
  constructor(config) {
    this.model = config.model || 'claude-opus-4-6';
    this.name = 'api-key';
    this.maxOutputTokens = config.maxOutputTokens || 32000;
    this._initPromise = this._init(config);
    this.messages = {
      create: (params) => this._create(params),
      stream: (params) => this._stream(params),
    };
  }

  async _init(config) {
    const { default: Anthropic } = await import('@anthropic-ai/sdk');
    this._anthropic = new Anthropic({
      apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,
    });
  }

  async _ensureReady() {
    await this._initPromise;
  }

  async _create(params) {
    await this._ensureReady();
    const { system, messages, max_tokens, model, tools, thinking } = params;
    const apiParams = {
      model: model || this.model,
      max_tokens: max_tokens || this.maxOutputTokens,
      messages: messages || [],
    };
    if (system) apiParams.system = system;
    if (tools?.length > 0) apiParams.tools = tools;

    // Only enable extended thinking when explicitly requested by caller
    if (thinking) {
      apiParams.temperature = 1;
      apiParams.thinking = thinking;
    }

    return await this._anthropic.messages.create(apiParams);
  }

  _stream(params) {
    const { system, messages, max_tokens, model, tools, thinking } = params;
    const apiParams = {
      model: model || this.model,
      max_tokens: max_tokens || this.maxOutputTokens,
      messages: messages || [],
    };
    if (system) apiParams.system = system;
    if (tools?.length > 0) apiParams.tools = tools;

    // Only enable extended thinking when explicitly requested by caller
    if (thinking) {
      apiParams.temperature = 1;
      apiParams.thinking = thinking;
    }

    // Return a wrapper that lazily initializes and delegates to SDK stream
    let sdkStream = null;
    let sdkStreamPromise = null;

    const ensureStream = () => {
      if (!sdkStreamPromise) {
        sdkStreamPromise = (async () => {
          await this._ensureReady();
          sdkStream = this._anthropic.messages.stream(apiParams);
          return sdkStream;
        })();
      }
      return sdkStreamPromise;
    };

    return {
      [Symbol.asyncIterator]() {
        let inner = null;
        return {
          async next() {
            if (!inner) {
              const s = await ensureStream();
              inner = s[Symbol.asyncIterator]();
            }
            return inner.next();
          },
        };
      },
      finalMessage: async () => {
        const s = await ensureStream();
        return s.finalMessage();
      },
      abort: () => {
        if (sdkStream) {
          try { sdkStream.abort(); } catch {}
        }
      },
    };
  }
}

// ─── Factory ─────────────────────────────────────────────────────────────────

let _cliAvailabilityCache = null;

function isCLIAvailableSync() {
  if (_cliAvailabilityCache !== null) return _cliAvailabilityCache;
  try {
    const result = spawnSync('claude', ['--version'], { stdio: 'ignore', timeout: 5000 });
    _cliAvailabilityCache = result.status === 0;
  } catch {
    _cliAvailabilityCache = false;
  }
  return _cliAvailabilityCache;
}

export function resolveProviderName(config = {}) {
  const requested = config.llmProvider || 'cli';
  const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
  const hasApiKey = !!apiKey;
  const cliAvailable = typeof config.cliAvailable === 'boolean' ? config.cliAvailable : isCLIAvailableSync();

  if (requested === 'api-key') {
    if (hasApiKey) return 'api-key';
    if (cliAvailable) return 'cli';
    throw new Error('API Key mode selected but no API key is configured.');
  }

  if (requested === 'cli') {
    if (cliAvailable) return 'cli';
    if (hasApiKey) return 'api-key';
    throw new Error('Claude CLI is not available and no API key is configured.');
  }

  if (hasApiKey) return 'api-key';
  if (cliAvailable) return 'cli';
  throw new Error('No LLM provider is available.');
}

/**
 * Create LLM provider.
 * Respects the explicit provider selection and only falls back when the
 * selected mode is unavailable.
 */
export function createProvider(config) {
  const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
  const providerName = resolveProviderName(config);
  const keySeed = [
    providerName,
    config.model || '',
    config.port || '',
    providerName === 'api-key'
      ? createHash('sha1').update(String(apiKey || '')).digest('hex').slice(0, 12)
      : 'cli',
  ].join(':');

  if (_providerInstanceCache.has(keySeed)) {
    return _providerInstanceCache.get(keySeed);
  }

  const provider = providerName === 'api-key'
    ? new APIProvider({ ...config, apiKey })
    : new CLIProvider(config);
  _providerInstanceCache.set(keySeed, provider);
  return provider;
}

/**
 * Check if claude CLI is available on the system.
 * @returns {Promise<boolean>}
 */
export async function isCLIAvailable() {
  return new Promise(resolve => {
    const proc = spawn('claude', ['--version'], {
      stdio: ['pipe', 'pipe', 'pipe'],
      timeout: 5000,
    });
    proc.on('close', code => resolve(code === 0));
    proc.on('error', () => resolve(false));
  });
}
