'use strict';

const { spawn } = require('child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const MODELS = [
  {
    id: 'claude-opus-4-7',
    name: 'Claude Opus 4.7 (via subscription)',
    capabilities: { vision: true, tools: false, streaming: false, reasoning: true },
  },
  {
    id: 'claude-sonnet-4-6',
    name: 'Claude Sonnet 4.6 (via subscription)',
    capabilities: { vision: true, tools: false, streaming: false },
  },
  {
    id: 'claude-haiku-4-5-20251001',
    name: 'Claude Haiku 4.5 (via subscription)',
    capabilities: { vision: false, tools: false, streaming: false },
  },
];

function _modelAlias(model) {
  if (!model) return 'sonnet';
  if (/opus/i.test(model)) return 'opus';
  if (/haiku/i.test(model)) return 'haiku';
  if (/sonnet/i.test(model)) return 'sonnet';
  return model;
}

function _serializeMessages(messages, system) {
  const parts = [];
  const tempFiles = [];
  const tempDirs = [];
  if (system) parts.push(`[system]\n${system}`);
  for (const msg of messages || []) {
    if (!msg || !msg.role) continue;
    let text = '';
    const imagePaths = [];
    if (typeof msg.content === 'string') {
      text = msg.content;
    } else if (Array.isArray(msg.content)) {
      const lines = [];
      for (const b of msg.content) {
        if (!b) continue;
        if (b.type === 'text' && typeof b.text === 'string') {
          lines.push(b.text);
        } else if (b.type === 'image' && b.source) {
          const src = b.source;
          if (src.type === 'path' && typeof src.path === 'string' && fs.existsSync(src.path)) {
            imagePaths.push(src.path);
          } else if (src.type === 'base64' && src.data) {
            let dir = tempDirs[0];
            if (!dir) {
              dir = fs.mkdtempSync(path.join(os.tmpdir(), 'walle-clicli-'));
              tempDirs.push(dir);
            }
            const ext = src.media_type === 'image/jpeg' ? 'jpg' : 'png';
            const filePath = path.join(dir, `img-${tempFiles.length + 1}.${ext}`);
            fs.writeFileSync(filePath, Buffer.from(src.data, 'base64'));
            tempFiles.push(filePath);
            imagePaths.push(filePath);
          }
        }
      }
      text = lines.join('\n');
    }
    if (!text && imagePaths.length === 0) continue;
    let body = text;
    if (imagePaths.length > 0) {
      const refList = imagePaths.map((p, i) => `  [${i + 1}] ${p}`).join('\n');
      body = `${body}\n\nIMAGES ATTACHED — view each by calling the Read tool on its absolute path:\n${refList}`;
    }
    parts.push(`[${msg.role}]\n${body}`);
  }
  return { prompt: parts.join('\n\n'), tempDirs };
}

function _spawnClaude(prompt, { model, signal, timeoutMs = 300000, cwd, addDirs = [], allowedTools } = {}) {
  return new Promise((resolve, reject) => {
    if (signal && signal.aborted) {
      return reject(new Error('Aborted'));
    }
    const args = [
      '-p',
      '--output-format', 'json',
      '--no-session-persistence',
      '--disable-slash-commands',
    ];
    if (model) args.push('--model', _modelAlias(model));
    for (const dir of addDirs) {
      if (dir && fs.existsSync(dir)) args.push('--add-dir', dir);
    }
    if (allowedTools && allowedTools.length > 0) {
      args.push('--allowed-tools', allowedTools.join(','));
    }

    // Subscription auth must come from claude.ai login keychain — strip
    // ANTHROPIC_API_KEY/AUTH_TOKEN so the CLI doesn't fall back to those.
    const childEnv = { ...process.env };
    delete childEnv.ANTHROPIC_API_KEY;
    delete childEnv.ANTHROPIC_AUTH_TOKEN;

    let proc;
    try {
      proc = spawn('claude', args, {
        stdio: ['pipe', 'pipe', 'pipe'],
        env: childEnv,
        cwd: cwd || process.cwd(),
        timeout: timeoutMs,
      });
    } catch (err) {
      return reject(new Error(`claude CLI spawn failed: ${err.message}`));
    }

    let stdout = '';
    let stderr = '';
    let aborted = false;

    proc.stdout.on('data', (d) => { stdout += d.toString(); });
    proc.stderr.on('data', (d) => { stderr += d.toString(); });
    // Swallow EPIPE on stdin: harmless when the child died before we wrote.
    proc.stdin.on('error', () => {});
    proc.on('error', (err) => {
      if (err.code === 'ENOENT') {
        reject(new Error('claude CLI not on PATH. Install Claude Code or pick a different auth method.'));
      } else {
        reject(new Error(`claude CLI error: ${err.message}`));
      }
    });
    proc.on('close', (code) => {
      if (aborted) return;
      if (code !== 0) {
        return reject(new Error(`claude exited ${code}: ${(stderr || stdout).slice(0, 500)}`));
      }
      let parsed;
      try {
        parsed = JSON.parse(stdout);
      } catch (parseErr) {
        return reject(new Error(`claude stdout not JSON: ${stdout.slice(0, 200)}`));
      }
      if (parsed.is_error) {
        return reject(new Error(parsed.error || parsed.result || 'claude returned is_error=true'));
      }
      resolve(parsed);
    });

    if (signal) {
      const onAbort = () => {
        aborted = true;
        try { proc.kill('SIGTERM'); } catch {}
        reject(new Error('Aborted'));
      };
      signal.addEventListener('abort', onAbort, { once: true });
    }

    try {
      proc.stdin.end(prompt);
    } catch {
      // Already handled by stdin.on('error') — no-op here.
    }
  });
}

/**
 * Create a Claude Code CLI–backed LLM provider. Wraps `claude -p` so the
 * user's claude.ai subscription pays for tokens instead of an API key.
 *
 * Limitations (v1):
 *   - No tool use. The CLI runs its own internal tools but doesn't expose
 *     Anthropic-format tool_use blocks back to the caller. Callers that
 *     pass `tools` get `toolCalls: []` — they should fall through to the
 *     coding-agent path (which already has WALLE_CODING_USE_CLI for that).
 *   - No streaming. `--output-format json` returns a single response.
 *   - No fine-grained temperature/max_tokens — the CLI ignores them.
 */
function createClaudeCliProvider(config = {}) {
  return {
    type: 'claude-cli',
    capabilities: { vision: true, tools: false, streaming: false, reasoning: true },

    async chat({ model, messages, tools, system, signal, cwd } = {}) {
      const { prompt, tempDirs } = _serializeMessages(messages, system);
      if (!prompt) {
        return {
          content: '',
          toolCalls: [],
          stopReason: 'end_turn',
          usage: { input: 0, output: 0 },
          latencyMs: 0,
          model: model || '',
          provider: 'claude-cli',
          raw: null,
        };
      }
      const start = Date.now();
      let raw;
      try {
        raw = await _spawnClaude(prompt, {
          model,
          signal,
          timeoutMs: config.timeoutMs,
          cwd: cwd || config.cwd,
          addDirs: tempDirs,
          allowedTools: tempDirs.length > 0 ? ['Read'] : undefined,
        });
      } finally {
        for (const dir of tempDirs) {
          try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
        }
      }
      const latencyMs = Date.now() - start;
      const reply = (raw.result || '').trim();

      // The CLI returns its own modelUsage block. Pull the first key as the
      // actual model used (lets us surface "claude-opus-4-7[1m]" etc.).
      const usedModel = raw.modelUsage ? Object.keys(raw.modelUsage)[0] : (model || '');
      const usage = raw.usage
        ? { input: raw.usage.input_tokens || 0, output: raw.usage.output_tokens || 0 }
        : undefined;

      // Note: `tools` is accepted for interface parity but unsupported by
      // `claude -p` in v1 — we always return toolCalls=[]. Tool-using callers
      // should fall through to a non-CLI path (e.g. WALLE_CODING_USE_CLI).
      void tools;

      return {
        content: reply || null,
        toolCalls: [],
        stopReason: raw.stop_reason || 'end_turn',
        usage,
        latencyMs,
        model: usedModel,
        provider: 'claude-cli',
        raw,
      };
    },

    async listModels() {
      return [...MODELS];
    },
  };
}

function createClaudeCliFromEnv() {
  return createClaudeCliProvider({});
}

module.exports = {
  createClaudeCliProvider,
  createClaudeCliFromEnv,
  _serializeMessages,
};
