import { Command } from 'commander';
import http from 'node:http';
import https from 'node:https';
import { existsSync } from 'node:fs';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { homedir } from 'node:os';
import { loadConfig, saveConfig } from '../config/index.js';
import type { ConfigLoaderDeps, McpctlConfig, LlmConfig, LlmProviderName, LlmProviderEntry, LlmTier } from '../config/index.js';
import type { SecretStore } from '@mcpctl/shared';
import { createSecretStore } from '@mcpctl/shared';

const execFileAsync = promisify(execFile);

export interface ConfigSetupPrompt {
  select<T>(message: string, choices: Array<{ name: string; value: T; description?: string }>): Promise<T>;
  input(message: string, defaultValue?: string): Promise<string>;
  password(message: string): Promise<string>;
  confirm(message: string, defaultValue?: boolean): Promise<boolean>;
}

export interface ConfigSetupDeps {
  configDeps: Partial<ConfigLoaderDeps>;
  secretStore: SecretStore;
  log: (...args: string[]) => void;
  prompt: ConfigSetupPrompt;
  fetchModels: (url: string, path: string) => Promise<string[]>;
  whichBinary: (name: string) => Promise<string | null>;
}

interface ProviderChoice {
  name: string;
  value: LlmProviderName;
  description: string;
}

/** Provider config fields returned by per-provider setup functions. */
interface ProviderFields {
  model?: string;
  url?: string;
  binaryPath?: string;
  venvPath?: string;
  port?: number;
  gpuMemoryUtilization?: number;
  maxModelLen?: number;
  idleTimeoutMinutes?: number;
  extraArgs?: string[];
}

const FAST_PROVIDER_CHOICES: ProviderChoice[] = [
  { name: 'Run vLLM Instance', value: 'vllm-managed', description: 'Auto-managed local vLLM (starts/stops with mcplocal)' },
  { name: 'vLLM (external)', value: 'vllm', description: 'Self-hosted vLLM (OpenAI-compatible)' },
  { name: 'Ollama', value: 'ollama', description: 'Local models via Ollama' },
  { name: 'Anthropic (Claude)', value: 'anthropic', description: 'Claude Haiku — fast & cheap' },
];

const HEAVY_PROVIDER_CHOICES: ProviderChoice[] = [
  { name: 'Gemini CLI', value: 'gemini-cli', description: 'Google Gemini via local CLI (free, no API key)' },
  { name: 'Anthropic (Claude)', value: 'anthropic', description: 'Claude API (requires API key)' },
  { name: 'OpenAI', value: 'openai', description: 'OpenAI API (requires API key)' },
  { name: 'DeepSeek', value: 'deepseek', description: 'DeepSeek API (requires API key)' },
];

const ALL_PROVIDER_CHOICES: ProviderChoice[] = [
  ...FAST_PROVIDER_CHOICES,
  ...HEAVY_PROVIDER_CHOICES,
  { name: 'None (disable)', value: 'none', description: 'Disable LLM features' },
] as ProviderChoice[];

const GEMINI_MODELS = ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'];
const ANTHROPIC_MODELS = ['claude-haiku-3-5-20241022', 'claude-sonnet-4-20250514', 'claude-sonnet-4-5-20250514', 'claude-opus-4-20250514'];
const DEEPSEEK_MODELS = ['deepseek-chat', 'deepseek-reasoner'];

function defaultFetchModels(baseUrl: string, path: string): Promise<string[]> {
  return new Promise((resolve) => {
    const url = new URL(path, baseUrl);
    const isHttps = url.protocol === 'https:';
    const transport = isHttps ? https : http;

    const req = transport.get({
      hostname: url.hostname,
      port: url.port || (isHttps ? 443 : 80),
      path: url.pathname,
      timeout: 5000,
    }, (res) => {
      const chunks: Buffer[] = [];
      res.on('data', (chunk: Buffer) => chunks.push(chunk));
      res.on('end', () => {
        try {
          const raw = Buffer.concat(chunks).toString('utf-8');
          const data = JSON.parse(raw) as { models?: Array<{ name: string }>; data?: Array<{ id: string }> };
          // Ollama format: { models: [{ name }] }
          if (data.models) {
            resolve(data.models.map((m) => m.name));
            return;
          }
          // OpenAI/vLLM format: { data: [{ id }] }
          if (data.data) {
            resolve(data.data.map((m) => m.id));
            return;
          }
          resolve([]);
        } catch {
          resolve([]);
        }
      });
    });
    req.on('error', () => resolve([]));
    req.on('timeout', () => { req.destroy(); resolve([]); });
  });
}

async function defaultSelect<T>(message: string, choices: Array<{ name: string; value: T; description?: string }>): Promise<T> {
  const { default: inquirer } = await import('inquirer');
  const { answer } = await inquirer.prompt([{
    type: 'list',
    name: 'answer',
    message,
    choices: choices.map((c) => ({
      name: c.description ? `${c.name} — ${c.description}` : c.name,
      value: c.value,
      short: c.name,
    })),
  }]);
  return answer as T;
}

async function defaultInput(message: string, defaultValue?: string): Promise<string> {
  const { default: inquirer } = await import('inquirer');
  const { answer } = await inquirer.prompt([{
    type: 'input',
    name: 'answer',
    message,
    default: defaultValue,
  }]);
  return answer as string;
}

async function defaultPassword(message: string): Promise<string> {
  const { default: inquirer } = await import('inquirer');
  const { answer } = await inquirer.prompt([{ type: 'password', name: 'answer', message }]);
  return answer as string;
}

async function defaultConfirm(message: string, defaultValue?: boolean): Promise<boolean> {
  const { default: inquirer } = await import('inquirer');
  const { answer } = await inquirer.prompt([{
    type: 'confirm',
    name: 'answer',
    message,
    default: defaultValue ?? true,
  }]);
  return answer as boolean;
}

export const defaultPrompt: ConfigSetupPrompt = {
  select: defaultSelect,
  input: defaultInput,
  password: defaultPassword,
  confirm: defaultConfirm,
};

async function defaultWhichBinary(name: string): Promise<string | null> {
  try {
    const { stdout } = await execFileAsync('which', [name], { timeout: 3000 });
    const path = stdout.trim();
    return path || null;
  } catch {
    return null;
  }
}

// --- Per-provider setup functions (return ProviderFields for reuse in both modes) ---

async function setupGeminiCliFields(
  prompt: ConfigSetupPrompt,
  log: (...args: string[]) => void,
  whichBinary: (name: string) => Promise<string | null>,
  currentModel?: string,
): Promise<ProviderFields> {
  const model = await prompt.select<string>('Select model:', [
    ...GEMINI_MODELS.map((m) => ({
      name: m === currentModel ? `${m} (current)` : m,
      value: m,
    })),
    { name: 'Custom...', value: '__custom__' },
  ]);

  const finalModel = model === '__custom__'
    ? await prompt.input('Model name:', currentModel)
    : model;

  let binaryPath: string | undefined;
  const detected = await whichBinary('gemini');
  if (detected) {
    log(`Found gemini at: ${detected}`);
    binaryPath = detected;
  } else {
    log('Warning: gemini binary not found in PATH');
    const manualPath = await prompt.input('Binary path (or install with: npm i -g @google/gemini-cli):');
    if (manualPath) binaryPath = manualPath;
  }

  const result: ProviderFields = { model: finalModel };
  if (binaryPath) result.binaryPath = binaryPath;
  return result;
}

async function setupOllamaFields(
  prompt: ConfigSetupPrompt,
  fetchModels: ConfigSetupDeps['fetchModels'],
  currentUrl?: string,
  currentModel?: string,
): Promise<ProviderFields> {
  const url = await prompt.input('Ollama URL:', currentUrl ?? 'http://localhost:11434');
  const models = await fetchModels(url, '/api/tags');
  let model: string;

  if (models.length > 0) {
    const choices = models.map((m) => ({
      name: m === currentModel ? `${m} (current)` : m,
      value: m,
    }));
    choices.push({ name: 'Custom...', value: '__custom__' });
    model = await prompt.select<string>('Select model:', choices);
    if (model === '__custom__') {
      model = await prompt.input('Model name:', currentModel);
    }
  } else {
    model = await prompt.input('Model name (could not fetch models):', currentModel ?? 'llama3.2');
  }

  const result: ProviderFields = { model };
  if (url) result.url = url;
  return result;
}

async function setupVllmFields(
  prompt: ConfigSetupPrompt,
  fetchModels: ConfigSetupDeps['fetchModels'],
  currentUrl?: string,
  currentModel?: string,
): Promise<ProviderFields> {
  const url = await prompt.input('vLLM URL:', currentUrl ?? 'http://localhost:8000');
  const models = await fetchModels(url, '/v1/models');
  let model: string;

  if (models.length > 0) {
    const choices = models.map((m) => ({
      name: m === currentModel ? `${m} (current)` : m,
      value: m,
    }));
    choices.push({ name: 'Custom...', value: '__custom__' });
    model = await prompt.select<string>('Select model:', choices);
    if (model === '__custom__') {
      model = await prompt.input('Model name:', currentModel);
    }
  } else {
    model = await prompt.input('Model name (could not fetch models):', currentModel ?? 'default');
  }

  const result: ProviderFields = { model };
  if (url) result.url = url;
  return result;
}

async function setupVllmManagedFields(
  prompt: ConfigSetupPrompt,
  log: (...args: string[]) => void,
): Promise<ProviderFields> {
  const defaultVenv = '~/vllm_env';
  const venvPath = await prompt.input('vLLM venv path:', defaultVenv);

  // Validate venv exists
  const expandedPath = venvPath.startsWith('~') ? venvPath.replace('~', homedir()) : venvPath;
  const vllmBin = `${expandedPath}/bin/vllm`;
  if (!existsSync(vllmBin)) {
    log(`Warning: ${vllmBin} not found.`);
    log(`  Create it with: uv venv ${venvPath} --python 3.12 && ${expandedPath}/bin/pip install vllm`);
  } else {
    log(`Found vLLM at: ${vllmBin}`);
  }

  const model = await prompt.input('Model to serve:', 'Qwen/Qwen2.5-7B-Instruct-AWQ');
  const gpuStr = await prompt.input('GPU memory utilization (0.1–1.0):', '0.75');
  const gpuMemoryUtilization = parseFloat(gpuStr) || 0.75;
  const idleStr = await prompt.input('Stop after N minutes idle:', '15');
  const idleTimeoutMinutes = parseInt(idleStr, 10) || 15;
  const portStr = await prompt.input('Port:', '8000');
  const port = parseInt(portStr, 10) || 8000;

  return {
    model,
    venvPath,
    port,
    gpuMemoryUtilization,
    idleTimeoutMinutes,
  };
}

async function setupApiKeyFields(
  prompt: ConfigSetupPrompt,
  secretStore: SecretStore,
  provider: LlmProviderName,
  secretKey: string,
  hardcodedModels: string[],
  currentModel?: string,
  currentUrl?: string,
): Promise<ProviderFields> {
  const existingKey = await secretStore.get(secretKey);
  let apiKey: string;

  if (existingKey) {
    const masked = `****${existingKey.slice(-4)}`;
    const changeKey = await prompt.confirm(`API key stored (${masked}). Change it?`, false);
    apiKey = changeKey ? await prompt.password('API key:') : existingKey;
  } else {
    apiKey = await prompt.password('API key:');
  }

  if (apiKey !== existingKey) {
    await secretStore.set(secretKey, apiKey);
  }

  let model: string;
  if (hardcodedModels.length > 0) {
    const choices = hardcodedModels.map((m) => ({
      name: m === currentModel ? `${m} (current)` : m,
      value: m,
    }));
    choices.push({ name: 'Custom...', value: '__custom__' });
    model = await prompt.select<string>('Select model:', choices);
    if (model === '__custom__') {
      model = await prompt.input('Model name:', currentModel);
    }
  } else {
    model = await prompt.input('Model name:', currentModel ?? 'gpt-4o');
  }

  let url: string | undefined;
  if (provider === 'openai') {
    const customUrl = await prompt.confirm('Use custom API endpoint?', false);
    if (customUrl) {
      url = await prompt.input('API URL:', currentUrl ?? 'https://api.openai.com');
    }
  }

  const result: ProviderFields = { model };
  if (url) result.url = url;
  return result;
}

async function promptForAnthropicKey(
  prompt: ConfigSetupPrompt,
  log: (...args: string[]) => void,
  whichBinary: (name: string) => Promise<string | null>,
): Promise<string> {
  const claudePath = await whichBinary('claude');

  if (claudePath) {
    log(`Found Claude CLI at: ${claudePath}`);
    const useOAuth = await prompt.confirm(
      'Generate free token via Claude CLI? (requires Pro/Max subscription)', true);
    if (useOAuth) {
      log('');
      log('  Run:  claude setup-token');
      log('  Then paste the token below (starts with sk-ant-oat01-)');
      log('');
      return prompt.password('OAuth token:');
    }
  } else {
    log('Tip: Install Claude CLI (npm i -g @anthropic-ai/claude-code) to generate');
    log('     a free OAuth token with "claude setup-token" (Pro/Max subscription).');
    log('');
  }

  return prompt.password('API key (from console.anthropic.com):');
}

async function setupAnthropicFields(
  prompt: ConfigSetupPrompt,
  secretStore: SecretStore,
  log: (...args: string[]) => void,
  whichBinary: (name: string) => Promise<string | null>,
  currentModel?: string,
): Promise<ProviderFields> {
  const existingKey = await secretStore.get('anthropic-api-key');
  let apiKey: string;

  if (existingKey) {
    const isOAuth = existingKey.startsWith('sk-ant-oat');
    const masked = `****${existingKey.slice(-4)}`;
    const label = isOAuth ? `OAuth token stored (${masked})` : `API key stored (${masked})`;
    const changeKey = await prompt.confirm(`${label}. Change it?`, false);
    apiKey = changeKey ? await promptForAnthropicKey(prompt, log, whichBinary) : existingKey;
  } else {
    apiKey = await promptForAnthropicKey(prompt, log, whichBinary);
  }

  if (apiKey !== existingKey) {
    await secretStore.set('anthropic-api-key', apiKey);
  }

  const choices = ANTHROPIC_MODELS.map((m) => ({
    name: m === currentModel ? `${m} (current)` : m,
    value: m,
  }));
  choices.push({ name: 'Custom...', value: '__custom__' });
  let model = await prompt.select<string>('Select model:', choices);
  if (model === '__custom__') {
    model = await prompt.input('Model name:', currentModel);
  }

  return { model };
}

/** Configure a single provider type and return its fields. */
async function setupProviderFields(
  providerType: LlmProviderName,
  prompt: ConfigSetupPrompt,
  log: (...args: string[]) => void,
  fetchModels: ConfigSetupDeps['fetchModels'],
  whichBinary: (name: string) => Promise<string | null>,
  secretStore: SecretStore,
): Promise<ProviderFields> {
  switch (providerType) {
    case 'gemini-cli':
      return setupGeminiCliFields(prompt, log, whichBinary);
    case 'ollama':
      return setupOllamaFields(prompt, fetchModels);
    case 'vllm':
      return setupVllmFields(prompt, fetchModels);
    case 'vllm-managed':
      return setupVllmManagedFields(prompt, log);
    case 'anthropic':
      return setupAnthropicFields(prompt, secretStore, log, whichBinary);
    case 'openai':
      return setupApiKeyFields(prompt, secretStore, 'openai', 'openai-api-key', []);
    case 'deepseek':
      return setupApiKeyFields(prompt, secretStore, 'deepseek', 'deepseek-api-key', DEEPSEEK_MODELS);
    default:
      return {};
  }
}

/** Build a LlmProviderEntry from type, name, and fields. */
function buildEntry(providerType: LlmProviderName, name: string, fields: ProviderFields, tier?: LlmTier): LlmProviderEntry {
  const entry: LlmProviderEntry = { name, type: providerType };
  if (fields.model) entry.model = fields.model;
  if (fields.url) entry.url = fields.url;
  if (fields.binaryPath) entry.binaryPath = fields.binaryPath;
  if (fields.venvPath) entry.venvPath = fields.venvPath;
  if (fields.port !== undefined) entry.port = fields.port;
  if (fields.gpuMemoryUtilization !== undefined) entry.gpuMemoryUtilization = fields.gpuMemoryUtilization;
  if (fields.maxModelLen !== undefined) entry.maxModelLen = fields.maxModelLen;
  if (fields.idleTimeoutMinutes !== undefined) entry.idleTimeoutMinutes = fields.idleTimeoutMinutes;
  if (fields.extraArgs !== undefined) entry.extraArgs = fields.extraArgs;
  if (tier) entry.tier = tier;
  return entry;
}

/** Simple mode: single provider (legacy format). */
async function simpleSetup(
  config: McpctlConfig,
  configDeps: Partial<ConfigLoaderDeps>,
  prompt: ConfigSetupPrompt,
  log: (...args: string[]) => void,
  fetchModels: ConfigSetupDeps['fetchModels'],
  whichBinary: (name: string) => Promise<string | null>,
  secretStore: SecretStore,
): Promise<void> {
  const currentLlm = config.llm && 'provider' in config.llm ? config.llm : undefined;

  const choices = ALL_PROVIDER_CHOICES.map((c) => {
    if (currentLlm?.provider === c.value) {
      return { ...c, name: `${c.name} (current)` };
    }
    return c;
  });

  const provider = await prompt.select<LlmProviderName>('Select LLM provider:', choices);

  if (provider === 'none') {
    const updated: McpctlConfig = { ...config, llm: { provider: 'none' } };
    saveConfig(updated, configDeps);
    log('LLM disabled. Restart mcplocal: systemctl --user restart mcplocal');
    return;
  }

  const fields = await setupProviderFields(provider, prompt, log, fetchModels, whichBinary, secretStore);
  const llmConfig: LlmConfig = { provider, ...fields };
  const updated: McpctlConfig = { ...config, llm: llmConfig };
  saveConfig(updated, configDeps);
  log(`\nLLM configured: ${llmConfig.provider}${llmConfig.model ? ` / ${llmConfig.model}` : ''}`);
  log('Restart mcplocal: systemctl --user restart mcplocal');
}

/** Generate a unique default name given names already in use. */
function uniqueDefaultName(baseName: string, usedNames: Set<string>): string {
  if (!usedNames.has(baseName)) return baseName;
  let i = 2;
  while (usedNames.has(`${baseName}-${i}`)) i++;
  return `${baseName}-${i}`;
}

/** Advanced mode: multiple providers with tier assignments. */
async function advancedSetup(
  config: McpctlConfig,
  configDeps: Partial<ConfigLoaderDeps>,
  prompt: ConfigSetupPrompt,
  log: (...args: string[]) => void,
  fetchModels: ConfigSetupDeps['fetchModels'],
  whichBinary: (name: string) => Promise<string | null>,
  secretStore: SecretStore,
): Promise<void> {
  const entries: LlmProviderEntry[] = [];
  const usedNames = new Set<string>();

  // Fast providers
  const addFast = await prompt.confirm('Add a FAST provider? (vLLM, Ollama — local, cheap, fast)', true);
  if (addFast) {
    let addMore = true;
    while (addMore) {
      const providerType = await prompt.select<LlmProviderName>('Fast provider type:', FAST_PROVIDER_CHOICES);
      const rawDefault = providerType === 'vllm' || providerType === 'vllm-managed' ? 'vllm-local' : providerType;
      const defaultName = uniqueDefaultName(rawDefault, usedNames);
      const name = await prompt.input('Provider name:', defaultName);
      usedNames.add(name);
      const fields = await setupProviderFields(providerType, prompt, log, fetchModels, whichBinary, secretStore);
      entries.push(buildEntry(providerType, name, fields, 'fast'));
      log(`  Added: ${name} (${providerType}) → fast tier`);
      addMore = await prompt.confirm('Add another fast provider?', false);
    }
  }

  // Heavy providers
  const addHeavy = await prompt.confirm('Add a HEAVY provider? (Gemini, Anthropic, OpenAI — cloud, smart)', true);
  if (addHeavy) {
    let addMore = true;
    while (addMore) {
      const providerType = await prompt.select<LlmProviderName>('Heavy provider type:', HEAVY_PROVIDER_CHOICES);
      const defaultName = uniqueDefaultName(providerType, usedNames);
      const name = await prompt.input('Provider name:', defaultName);
      usedNames.add(name);
      const fields = await setupProviderFields(providerType, prompt, log, fetchModels, whichBinary, secretStore);
      entries.push(buildEntry(providerType, name, fields, 'heavy'));
      log(`  Added: ${name} (${providerType}) → heavy tier`);
      addMore = await prompt.confirm('Add another heavy provider?', false);
    }
  }

  if (entries.length === 0) {
    log('No providers configured.');
    return;
  }

  // Summary
  log('\nProvider configuration:');
  for (const e of entries) {
    log(`  ${e.tier ?? 'unassigned'}: ${e.name} (${e.type})${e.model ? ` / ${e.model}` : ''}`);
  }

  const updated: McpctlConfig = { ...config, llm: { providers: entries } };
  saveConfig(updated, configDeps);
  log('\nRestart mcplocal: systemctl --user restart mcplocal');
}

export function createConfigSetupCommand(deps?: Partial<ConfigSetupDeps>): Command {
  return new Command('setup')
    .description('Interactive LLM provider setup wizard')
    .action(async () => {
      const configDeps = deps?.configDeps ?? {};
      const log = deps?.log ?? ((...args: string[]) => console.log(...args));
      const prompt = deps?.prompt ?? defaultPrompt;
      const fetchModels = deps?.fetchModels ?? defaultFetchModels;
      const whichBinary = deps?.whichBinary ?? defaultWhichBinary;
      const secretStore = deps?.secretStore ?? await createSecretStore();

      const config = loadConfig(configDeps);

      const mode = await prompt.select<'simple' | 'advanced'>('Setup mode:', [
        { name: 'Simple', value: 'simple', description: 'One provider for everything' },
        { name: 'Advanced', value: 'advanced', description: 'Multiple providers with fast/heavy tiers' },
      ]);

      if (mode === 'simple') {
        await simpleSetup(config, configDeps, prompt, log, fetchModels, whichBinary, secretStore);
      } else {
        await advancedSetup(config, configDeps, prompt, log, fetchModels, whichBinary, secretStore);
      }
    });
}
