'use strict';

const { DEFAULT_TIER_MODELS } = require('../evaluation/tier-selector');
const providerRegistry = require('./registry');
const { isPortkeyProviderConfig } = require('./portkey');
const { withProviderMessageGuard } = require('./message-guard');

function normalizeProviderType(type) {
  const text = String(type || '').trim().toLowerCase().replace(/_/g, '-');
  if (text === 'kimi' || text === 'kimi-k2' || text === 'moonshot-ai') return 'moonshot';
  if (text === 'openai-compatible' || text.startsWith('openai-compatible-')) return 'openai';
  return text;
}

// ============================================================
// Factory
// ============================================================

/**
 * Create an LLM provider by type. Backwards-compatible signature with
 * the original FACTORIES map; under the hood, the provider is looked up
 * via the plugin registry which scans wall-e/llm/*.plugin.json on
 * first call.
 *
 * @param {'anthropic'|'openai'|'google'|'deepseek'|'moonshot'|'ollama'|'mlx'|string} type
 * @param {object} config - Provider-specific configuration
 * @returns {object} Provider instance with { type, chat, listModels }
 */
function createClient(type, config = {}) {
  providerRegistry.ensureBootstrapped();
  const providerType = normalizeProviderType(type);
  const providerConfig = { ...config };
  const rawProviderType = String(type || '').trim().toLowerCase().replace(/_/g, '-');
  if (rawProviderType === 'openai-compatible' || rawProviderType.startsWith('openai-compatible-')) {
    providerConfig.allowUnknownModels = true;
  }
  let provider = providerRegistry.createProvider(providerType, providerConfig);
  if (!provider && isPortkeyProviderConfig(config)) {
    provider = providerRegistry.createProvider('openai', providerConfig);
  }
  if (!provider) {
    throw new Error(`Unknown provider type: ${type}`);
  }
  return withStreamFallback(withProviderMessageGuard(provider));
}

function withStreamFallback(provider) {
  if (!provider || typeof provider.chat !== 'function' || typeof provider.chatStream === 'function') {
    return provider;
  }
  return {
    ...provider,
    async *chatStream(opts = {}) {
      const { streamFromChat } = require('../coding/stream-processor');
      yield* streamFromChat(provider, opts);
    },
  };
}

// ============================================================
// Default provider + model helpers
// ============================================================

let _defaultClient = null;

function getDefaultProviderType() {
  // Setup persists to .env and mirrors to process.env for the running daemon.
  if (process.env.WALLE_PROVIDER) return normalizeProviderType(process.env.WALLE_PROVIDER);
  // Fall back to Wall-E's kv_store for older/live setup writes.
  try {
    const brain = require('../brain');
    const val = typeof brain.getKv === 'function'
      ? brain.getKv('walle_provider')
      : brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'walle_provider'").get()?.value;
    if (val) return normalizeProviderType(val);
  } catch {}
  return 'anthropic';
}

function getDefaultModel() {
  providerRegistry.ensureBootstrapped();
  const providerType = getDefaultProviderType();
  if (process.env.WALLE_MODEL && _modelMatchesProvider(process.env.WALLE_MODEL, providerType)) return process.env.WALLE_MODEL;
  try {
    const brain = require('../brain');
    const val = typeof brain.getKv === 'function'
      ? brain.getKv('walle_model')
      : brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'walle_model'").get()?.value;
    if (val && _modelMatchesProvider(val, providerType)) return val;
  } catch {}
  return getDefaultModelForProvider(providerType);
}

function getDefaultModelForProvider(providerType) {
  providerRegistry.ensureBootstrapped();
  return providerRegistry.getDefaultModel(providerType) || providerRegistry.getDefaultModel('anthropic');
}

function getDefaultAuthMethod(providerType) {
  const type = normalizeProviderType(providerType || getDefaultProviderType());
  try {
    const brain = require('../brain');
    const dbMethod = typeof brain.getProviderAuthMethod === 'function'
      ? brain.getProviderAuthMethod(type)
      : null;
    if (dbMethod) return dbMethod;
  } catch {}
  if (type === 'anthropic' || type === 'openai') {
    return process.env.WALLE_AUTH_METHOD || '';
  }
  return '';
}

function resolveCompatibleModel(model, providerType) {
  providerRegistry.ensureBootstrapped();
  const targetProvider = normalizeProviderType(providerType || getDefaultProviderType());
  if (model && _modelMatchesProvider(model, targetProvider)) return model;
  return getDefaultModelForProvider(targetProvider);
}

/**
 * Guard against model-provider mismatch. Delegates to the registry's
 * manifest-driven prefix matching (replaces the old hardcoded
 * regex-per-provider switch).
 */
function _modelMatchesProvider(model, provider) {
  providerRegistry.ensureBootstrapped();
  return providerRegistry.modelMatchesProvider(model, normalizeProviderType(provider));
}

/**
 * Get or create the default LLM client based on WALLE_PROVIDER env var.
 * Falls back to Anthropic if not set. Cached after first call.
 */
// Resolve ONLY the user's chosen default provider into a client + identity,
// applying the full auth_method handling (claude_cli / oauth_proxy / codex_cli /
// DB config / env). This is the FIRST link of the resilient chain.
function _resolvePrimaryDefault() {
  providerRegistry.ensureBootstrapped();
  const providerType = getDefaultProviderType();
  const dbKey = _getProviderKeyFromDb(providerType);
  const authMethod = getDefaultAuthMethod(providerType);

  // Anthropic auth_method=claude_cli routes through the user's `claude`
  // CLI (subscription billing) instead of the SDK against api.anthropic.com.
  if (providerType === 'anthropic' && authMethod === 'claude_cli') {
    return { id: 'claude-cli', type: 'claude-cli', client: createClient('claude-cli', {}) };
  }

  // Anthropic auth_method=oauth_proxy points the SDK at the local proxy
  // (CTM-managed, see claude-task-manager/oauth-proxy.js). The proxy forwards
  // requests with the user's claude.ai OAuth Bearer token.
  if (providerType === 'anthropic' && authMethod === 'oauth_proxy') {
    const port = process.env.OAUTH_PROXY_PORT || '3458';
    return {
      id: `${providerType}-oauth`,
      type: providerType,
      client: createClient('anthropic', { apiKey: 'oauth-proxy-placeholder', baseUrl: `http://127.0.0.1:${port}` }),
    };
  }

  // OpenAI auth_method=codex_cli routes through the user's `codex` CLI
  // (ChatGPT subscription billing) instead of api.openai.com.
  if (providerType === 'openai' && authMethod === 'codex_cli') {
    return { id: 'codex-cli', type: 'codex-cli', client: createClient('codex-cli', {}) };
  }

  let client;
  if (providerType === 'anthropic') {
    const config = _getProviderConfigFromDb(providerType);
    const hasSpecificConfig = !!(config.apiKey || config.baseUrl || config.customHeaders);
    if (hasSpecificConfig) {
      if (!config.apiKey) config.apiKey = dbKey || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN;
      client = createClient('anthropic', config);
    } else {
      // If DB has the key, set it in env so createAnthropicFromEnv picks it up
      if (dbKey && !process.env.ANTHROPIC_API_KEY) process.env.ANTHROPIC_API_KEY = dbKey;
      const fromEnv = providerRegistry.createProviderFromEnv('anthropic');
      client = fromEnv || createClient('anthropic', { apiKey: dbKey });
    }
  } else {
    const config = _getProviderConfigFromDb(providerType);
    if (providerType === 'openai') {
      config.apiKey = config.apiKey || dbKey || process.env.OPENAI_API_KEY;
      if (!config.baseUrl && process.env.OPENAI_BASE_URL) config.baseUrl = process.env.OPENAI_BASE_URL;
    } else if (providerType === 'google') {
      config.apiKey = config.apiKey || dbKey || process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
      if (process.env.GOOGLE_AUTH_MODE === 'oauth') {
        config.authMode = 'oauth';
        config.refreshToken = process.env.GOOGLE_REFRESH_TOKEN;
      }
    } else if (providerType === 'ollama') {
      if (!config.baseUrl && process.env.OLLAMA_BASE_URL) config.baseUrl = process.env.OLLAMA_BASE_URL;
    } else if (providerType === 'mlx') {
      if (process.env.MLX_MODEL) config.model = process.env.MLX_MODEL;
    } else if (providerType === 'deepseek') {
      config.apiKey = config.apiKey || dbKey || process.env.DEEPSEEK_API_KEY;
      if (!config.baseUrl && process.env.DEEPSEEK_BASE_URL) config.baseUrl = process.env.DEEPSEEK_BASE_URL;
    } else if (providerType === 'moonshot') {
      config.apiKey = config.apiKey || dbKey || process.env.MOONSHOT_API_KEY;
      if (!config.baseUrl && process.env.MOONSHOT_BASE_URL) config.baseUrl = process.env.MOONSHOT_BASE_URL;
    }
    client = createClient(providerType, config);
  }

  let id = `${providerType}-default`;
  try {
    const brain = require('../brain');
    const preferred = brain.getPreferredModelProviderForType?.(providerType);
    if (preferred?.id) id = preferred.id;
  } catch {}
  return { id, type: providerType, client };
}

// Surface a fallback to the user/operator: the live default was unreachable and a
// different set-up provider (or a CLI) answered. Logged + emitted on the health bus
// (consumed by chat-api / model-admin → UI). Never throws.
function _notifyDefaultFallback(from, to, error) {
  const fromLabel = from?.label || from?.type || from?.id || 'default provider';
  const toLabel = to?.label || to?.type || to?.id || 'fallback provider';
  const message = `Default provider "${fromLabel}" is unreachable — answering via "${toLabel}" until it recovers.`;
  // Surface WHY (secret-redacted) so a misconfig — e.g. a model/virtual-key mismatch —
  // doesn't look like a generic outage in the routing-notice banner.
  let detail = '';
  try { detail = require('./provider-error').fallbackErrorDetail(error); } catch {}
  // Steady-state path: once the primary is health-marked 'down', activeChain() skips
  // it entirely so the fallback loop never re-throws — `error` is undefined and the
  // banner would lose its reason. Recover it from the primary's stored health error
  // (the failure that marked it down is exactly why we're still falling back).
  if (!detail && from?.id) {
    try {
      const le = require('./health').default.getProviderHealth?.(from.id)?.lastError;
      if (le && (le.message || le.code != null)) {
        const synth = { message: le.message, code: le.code };
        if (typeof le.code === 'number') synth.status = le.code;
        detail = require('./provider-error').fallbackErrorDetail(synth);
      }
    } catch {}
  }
  try { console.warn(`[llm] ${message}${detail ? ' — ' + detail : ''}`); } catch {}
  try {
    const health = require('./health').default;
    health.emit('default-provider-fallback', { from: { id: from?.id, type: from?.type }, to: { id: to?.id, type: to?.type }, detail });
    health.recordRoutingNotice({ kind: 'fallback', severity: 'warning', from: { id: from?.id, type: from?.type, label: fromLabel }, to: { id: to?.id, type: to?.type, label: toLabel }, message, detail });
  } catch {}
  try { require('../lib/recovery-logger').warn('default-fallback', 'provider_unreachable', { from: from?.type, to: to?.type, detail }); } catch {}
}

function _notifyDefaultExhausted(error) {
  // Secret-redacted detail (replaces the old raw error.message, which could echo creds).
  let detail = '';
  try { detail = require('./provider-error').fallbackErrorDetail(error); } catch {}
  const message = `Every configured provider is unreachable. Check connectivity or Setup.${detail ? ' (' + detail + ')' : ''}`;
  try { console.error(`[llm] ${message}`); } catch {}
  try {
    const health = require('./health').default;
    health.emit('default-provider-unreachable', { error: detail || String(error?.message || error || '') });
    health.recordRoutingNotice({ kind: 'unreachable', severity: 'error', message, detail });
  } catch {}
}

function _clearDefaultRoutingNotice() {
  try { require('./health').default.clearRoutingNotice(); } catch {}
}

function getDefaultClient() {
  if (_defaultClient) return _defaultClient;

  const primary = _resolvePrimaryDefault();

  // Opt-out: behave exactly as before (single provider, no fallback).
  if (String(process.env.WALLE_DEFAULT_FALLBACK || '').trim() === '0') {
    _defaultClient = primary.client;
    return _defaultClient;
  }

  // Wrap the default in a resilient chain: user default → other set-up providers
  // → installed coding-agent CLIs. Any failure here falls back to the bare primary
  // client so fallback wiring can never break basic operation.
  try {
    const { buildDefaultProviderChain, makeResilientClient } = require('./default-fallback');
    const brain = require('../brain');
    let health = null;
    try { health = require('./health').default; } catch {}
    const chain = buildDefaultProviderChain({
      brain,
      createClient,
      buildPrimary: () => primary,
      resolveModelForType: (type) => {
        try { return getDefaultModelForProvider(type); } catch { return undefined; }
      },
      env: process.env,
    });
    if (!chain.length || (chain.length === 1 && chain[0].id === primary.id)) {
      // Nothing to fall back to — still add transient retries around the primary.
      _defaultClient = makeResilientClient([primary], {
        health,
        onExhausted: ({ error }) => _notifyDefaultExhausted(error),
      });
      return _defaultClient;
    }
    _defaultClient = makeResilientClient(chain, {
      health,
      isDown: (id) => {
        try { return health?.getProviderHealth?.(id)?.status === 'down'; } catch { return false; }
      },
      onFallback: ({ from, to, error }) => _notifyDefaultFallback(from, to, error),
      onPrimaryOk: () => _clearDefaultRoutingNotice(),
      onExhausted: ({ error }) => _notifyDefaultExhausted(error),
    });
  } catch (e) {
    try { console.error('[llm] resilient default wiring failed, using bare primary:', e.message); } catch {}
    _defaultClient = primary.client;
  }
  return _defaultClient;
}

/**
 * Read API key from model_providers DB table.
 * Returns the key string or null if not found.
 */
function _getProviderKeyFromDb(type) {
  try {
    const brain = require('../brain');
    const row = typeof brain.getPreferredModelProviderForType === 'function'
      ? brain.getPreferredModelProviderForType(type)
      : brain.getDb().prepare(
        'SELECT api_key_encrypted FROM model_providers WHERE type = ? AND enabled = 1 AND api_key_encrypted IS NOT NULL ORDER BY updated_at DESC LIMIT 1'
      ).get(type);
    return row?.api_key_encrypted || null;
  } catch { return null; }
}

function _parseCustomHeaders(value) {
  if (!value) return undefined;
  if (typeof value === 'object' && !Array.isArray(value)) return value;
  if (typeof value !== 'string') return undefined;
  try { return JSON.parse(value); } catch { return undefined; }
}

function _hasProviderConnectionConfig(config = {}) {
  return !!(
    config.apiKey
    || config.baseUrl
    || config.customHeaders
    || config.authMode
    || config.refreshToken
  );
}

function _applyProviderEnvConfig(providerType, config) {
  if (providerType === 'openai') {
    config.apiKey = config.apiKey || process.env.OPENAI_API_KEY;
    if (!config.baseUrl && process.env.OPENAI_BASE_URL) config.baseUrl = process.env.OPENAI_BASE_URL;
  } else if (providerType === 'google') {
    config.apiKey = config.apiKey || process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
    if (!config.authMode && process.env.GOOGLE_AUTH_MODE === 'oauth') {
      config.authMode = 'oauth';
      config.refreshToken = config.refreshToken || process.env.GOOGLE_REFRESH_TOKEN;
    }
  } else if (providerType === 'ollama') {
    if (!config.baseUrl && process.env.OLLAMA_BASE_URL) config.baseUrl = process.env.OLLAMA_BASE_URL;
  } else if (providerType === 'mlx') {
    if (!config.model && process.env.MLX_MODEL) config.model = process.env.MLX_MODEL;
  } else if (providerType === 'deepseek') {
    config.apiKey = config.apiKey || process.env.DEEPSEEK_API_KEY;
    if (!config.baseUrl && process.env.DEEPSEEK_BASE_URL) config.baseUrl = process.env.DEEPSEEK_BASE_URL;
  } else if (providerType === 'moonshot') {
    config.apiKey = config.apiKey || process.env.MOONSHOT_API_KEY;
    if (!config.baseUrl && process.env.MOONSHOT_BASE_URL) config.baseUrl = process.env.MOONSHOT_BASE_URL;
  } else if (providerType === 'anthropic') {
    config.apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN;
    if (!config.baseUrl && process.env.ANTHROPIC_BASE_URL) config.baseUrl = process.env.ANTHROPIC_BASE_URL;
  }
  return config;
}

function getProviderRuntimeConfig(type, config = {}) {
  providerRegistry.ensureBootstrapped();
  const providerType = normalizeProviderType(type);
  const supplied = { ...(config || {}) };
  const base = _hasProviderConnectionConfig(supplied)
    ? {}
    : _getProviderConfigFromDb(providerType);
  return _applyProviderEnvConfig(providerType, { ...base, ...supplied });
}

function _getProviderConfigFromDb(type) {
  try {
    const brain = require('../brain');
    const row = typeof brain.getPreferredModelProviderForType === 'function'
      ? brain.getPreferredModelProviderForType(type)
      : null;
    if (!row) return {};
    const config = {};
    if (row.api_key_encrypted) config.apiKey = row.api_key_encrypted;
    if (row.base_url) config.baseUrl = row.base_url;
    const customHeaders = _parseCustomHeaders(row.custom_headers);
    if (customHeaders && Object.keys(customHeaders).length > 0) config.customHeaders = customHeaders;
    return config;
  } catch {
    return {};
  }
}

/**
 * Clear the cached default client (for testing or env changes).
 */
function resetDefaultClient() {
  _defaultClient = null;
}

module.exports = {
  createClient,
  withStreamFallback,
  withProviderMessageGuard,
  getDefaultClient,
  getDefaultProviderType,
  getDefaultAuthMethod,
  getDefaultModel,
  getDefaultModelForProvider,
  getProviderRuntimeConfig,
  resolveCompatibleModel,
  resetDefaultClient,
  DEFAULT_TIER_MODELS,
  // Exported for tests: the routing-notice fallback notifier (verifies it recovers
  // the reason from the primary's stored health error on the steady-state skip path).
  _notifyDefaultFallback,
  // Re-exported for callers that want to introspect the manifest
  // (chat router, setup page) without poking at the registry directly.
  detectProviderForModel: (model) => {
    providerRegistry.ensureBootstrapped();
    return providerRegistry.detectProviderForModel(model);
  },
  modelMatchesProvider: (model, provider) => {
    providerRegistry.ensureBootstrapped();
    return providerRegistry.modelMatchesProvider(model, provider);
  },
};
