import { spawn, execSync } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const AGENT_ROOT = join(__dirname, '..');
const DEFAULT_WORKSPACE = join(AGENT_ROOT, 'workspaces', 'default');

export const name = 'claude';

export const modelMap = {
  fast: 'haiku',
  balanced: 'sonnet',
  powerful: 'opus'
};

function resolveModel(model) {
  const key = String(model || 'balanced').trim().toLowerCase();
  if (key === 'fast' || key === 'balanced' || key === 'powerful') {
    return modelMap[key];
  }
  return String(model || modelMap.balanced);
}

// Resolved full path to the CLI binary (set by isAvailable)
let resolvedBin = 'claude';

/**
 * Platform-aware spawn helper.
 * Uses the resolved binary path from isAvailable() to avoid ENOENT when
 * the CLI is installed in a directory not in Node's default PATH (e.g.
 * nvm-managed npm global bin, homebrew, etc).
 */
function spawnClaude(args, options = {}) {
  if (process.platform === 'win32') {
    return spawn('cmd.exe', ['/d', '/s', '/c', resolvedBin, ...args], {
      ...options,
      shell: false
    });
  }
  return spawn(resolvedBin, args, {
    ...options,
    shell: false
  });
}

export function isAvailable() {
  try {
    if (process.platform === 'win32') {
      resolvedBin = execSync('where claude.cmd 2>nul || where claude', { encoding: 'utf-8' }).trim().split(/\r?\n/)[0];
    } else {
      resolvedBin = execSync('which claude', { encoding: 'utf-8' }).trim();
    }
    return true;
  } catch {
    return false;
  }
}

/**
 * Spawn Claude CLI and return the result.
 *
 * @param {string} prompt
 * @param {object} opts
 * @param {string} opts.model
 * @param {number} opts.timeout - ms before killing
 * @param {boolean} opts.sandboxed - if true (default), run in workspace/ with hooks
 * @param {function} opts.onStream - callback(partialText) for streaming updates
 * @param {string[]} opts.extraArgs - additional CLI args
 */
export async function ask(prompt, { model = 'balanced', timeout = 600_000, sandboxed = true, onStream, extraArgs = [] } = {}) {
  return new Promise((resolve, reject) => {
    const selectedModel = resolveModel(model);
    const args = [
      '-p',
      '--model', selectedModel,
      '--output-format', 'stream-json',
      '--verbose',
      '--tools', 'Bash,Edit,Write,Read,Glob,Grep,WebFetch,WebSearch',
      '--strict-mcp-config',
      ...extraArgs
    ];

    // Strip CLAUDECODE env var so the CLI doesn't refuse to run inside this session
    const childEnv = { ...process.env };
    delete childEnv.CLAUDECODE;

    const proc = spawnClaude(args, {
      cwd: sandboxed ? DEFAULT_WORKSPACE : AGENT_ROOT,
      env: childEnv,
      stdio: ['pipe', 'pipe', 'pipe']
    });

    // Pipe prompt via stdin to avoid Windows cmd.exe length limits and escaping issues
    proc.stdin.write(prompt);
    proc.stdin.end();

    let fullResult = '';
    let accumulatedText = '';
    let buffer = '';
    let killed = false;

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

    proc.stdout.on('data', (chunk) => {
      buffer += chunk.toString();
      const lines = buffer.split('\n');
      buffer = lines.pop();

      for (const line of lines) {
        if (!line.trim()) continue;
        try {
          const event = JSON.parse(line);

          if (event.type === 'assistant' && event.message?.content) {
            // Each assistant event contains text for that turn.
            // Multiple assistant events can occur (e.g. after tool use).
            // Accumulate all text and stream the full accumulated content.
            for (const block of event.message.content) {
              if (block.type === 'text' && block.text) {
                accumulatedText += block.text;
              }
            }
            if (onStream) onStream(accumulatedText);
          } else if (event.type === 'result') {
            fullResult = event.result || '';
          }
        } catch {
          // Ignore malformed lines
        }
      }
    });

    let stderr = '';
    proc.stderr.on('data', (chunk) => {
      const text = chunk.toString();
      stderr += text;
      // Log non-empty stderr for debugging (trim progress bars / blank lines)
      const trimmed = text.trim();
      if (trimmed && !trimmed.startsWith('\r')) {
        console.error(`[claude-cli] ${trimmed.slice(0, 200)}`);
      }
    });

    proc.on('close', (code) => {
      clearTimeout(timer);

      // Process any remaining buffer
      if (buffer.trim()) {
        try {
          const event = JSON.parse(buffer);
          if (event.type === 'assistant' && event.message?.content) {
            for (const block of event.message.content) {
              if (block.type === 'text' && block.text) {
                accumulatedText += block.text;
              }
            }
          } else if (event.type === 'result') {
            fullResult = event.result || '';
          }
        } catch {
          // Non-JSON tail — ignore
        }
      }

      // Prefer the 'result' event text, but fall back to accumulated assistant text
      const output = (fullResult || accumulatedText).trim();

      if (killed) {
        reject(new Error(`Claude CLI timed out after ${timeout}ms`));
      } else if (code !== 0 && !output) {
        reject(new Error(`Claude CLI exited with code ${code}\n${stderr}`));
      } else if (code === 0 && !output) {
        reject(new Error(`Claude CLI returned empty output\n${stderr}`.trim()));
      } else {
        resolve(output);
      }
    });

    proc.on('error', (err) => {
      clearTimeout(timer);
      reject(new Error(`Claude CLI spawn error: ${err.message}`));
    });
  });
}

/**
 * Smart chat completion with controlled tool access.
 *
 * Two modes depending on allowedTools:
 *
 *  Chat mode (allowedTools = ['Read', 'Glob', ...]):
 *    Claude can read files, search code, and browse the web — but cannot
 *    write files, run commands, or trigger deployments. All thinking and
 *    tool-use text streams to the user. Uses --verbose for streaming.
 *
 *  Raw completion mode (allowedTools = null, default):
 *    No tools at all. Used by skills that need clean output (HTML, markdown).
 *    Returns only the last turn's text with no tool traces.
 *
 * Both modes:
 *  - shell: true — Node.js handles arg quoting on Windows + Mac (fixes
 *    the cmd.exe empty-string mangling that broke --tools "")
 *  - --strict-mcp-config — blocks all MCP tools (Cloudflare, etc.)
 *  - --no-session-persistence — faster, no disk session writes
 */
export async function streamChat(prompt, { model = 'balanced', timeout = 300_000, onStream, allowedTools = null } = {}) {
  return new Promise((resolve, reject) => {
    const selectedModel = resolveModel(model);
    const chatMode = Array.isArray(allowedTools) && allowedTools.length > 0;

    const args = [
      '-p',
      '--model', selectedModel,
      '--output-format', 'stream-json',
      '--strict-mcp-config',
      '--no-session-persistence'
    ];

    // --verbose is required when using --output-format stream-json
    args.push('--verbose');

    if (chatMode) {
      // Chat: allow specific read-only tools, stream full conversation
      args.push('--allowedTools', allowedTools.join(','));
    } else {
      // Raw completion: disable all tools for clean skill output (HTML, markdown, JSON).
      // Uses --disallowedTools instead of --tools '' because empty strings get
      // swallowed by cmd.exe on Windows, causing "argument missing" errors.
      args.push('--disallowedTools', 'Bash,Edit,Write,Read,Glob,Grep,WebFetch,WebSearch');
    }

    const childEnv = { ...process.env };
    delete childEnv.CLAUDECODE;

    // shell: true — Node.js handles argument quoting correctly on all
    // platforms (empty strings, .cmd files on Windows, etc.)
    const proc = spawn(resolvedBin, args, {
      cwd: DEFAULT_WORKSPACE,
      env: childEnv,
      stdio: ['pipe', 'pipe', 'pipe'],
      shell: true
    });

    proc.stdin.write(prompt);
    proc.stdin.end();

    let accumulatedText = '';   // all text across turns (for chat mode)
    let currentTurnText = '';   // last turn only (for raw completion mode)
    let fullResult = '';
    let buffer = '';
    let killed = false;
    let lastEventWasAssistant = false;

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

    proc.stdout.on('data', (chunk) => {
      buffer += chunk.toString();
      const lines = buffer.split('\n');
      buffer = lines.pop();

      for (const line of lines) {
        if (!line.trim()) continue;
        try {
          const event = JSON.parse(line);

          if (event.type === 'assistant' && event.message?.content) {
            // New turn detection: reset per-turn text after tool use
            if (!lastEventWasAssistant) {
              currentTurnText = '';
            }
            lastEventWasAssistant = true;

            for (const block of event.message.content) {
              if (block.type === 'text' && block.text) {
                accumulatedText += block.text;
                currentTurnText += block.text;
              }
            }
            // Chat mode: stream all text (user sees thinking + tool context)
            // Raw mode: stream current turn only
            if (onStream) onStream(chatMode ? accumulatedText : currentTurnText);
          } else {
            lastEventWasAssistant = false;
            if (event.type === 'result') {
              fullResult = event.result || '';
            }
          }
        } catch {
          // Ignore malformed lines
        }
      }
    });

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

    proc.on('close', (code) => {
      clearTimeout(timer);

      if (buffer.trim()) {
        try {
          const event = JSON.parse(buffer);
          if (event.type === 'assistant' && event.message?.content) {
            if (!lastEventWasAssistant) currentTurnText = '';
            for (const block of event.message.content) {
              if (block.type === 'text' && block.text) {
                accumulatedText += block.text;
                currentTurnText += block.text;
              }
            }
          } else if (event.type === 'result') {
            fullResult = event.result || '';
          }
        } catch {}
      }

      // Chat mode: full conversation text (all turns)
      // Raw mode: last turn only (clean content, no tool traces)
      const output = chatMode
        ? (accumulatedText || fullResult).trim()
        : (currentTurnText || fullResult).trim();

      if (killed) reject(new Error('Claude CLI timed out'));
      else if (code !== 0 && !output) reject(new Error(`Claude CLI exited with code ${code}\n${stderr}`));
      else if (!output) reject(new Error(`Claude CLI returned empty output\n${stderr}`.trim()));
      else resolve(output);
    });

    proc.on('error', (err) => {
      clearTimeout(timer);
      reject(new Error(`Claude CLI spawn error: ${err.message}`));
    });
  });
}

/**
 * Spawn Claude in interactive mode (for meta-develop).
 * Returns the spawned child process.
 */
export function spawnInteractive({ cwd, model = 'powerful' } = {}) {
  const selectedModel = resolveModel(model);
  const childEnv = { ...process.env };
  delete childEnv.CLAUDECODE;
  return spawnClaude([
    '--model', selectedModel,
    '--dangerously-skip-permissions',
    '--output-format', 'stream-json',
    '--verbose'
  ], {
    cwd: cwd || DEFAULT_WORKSPACE,
    env: childEnv,
    stdio: ['pipe', 'pipe', 'pipe']
  });
}
