'use strict';

const path = require('path');
const {
  SETUP_PROVIDER_ENV_KEYS,
  SETUP_PROVIDER_NAMES,
  sanitizeSetupProviderType,
  setupProviderHasRuntimeAccess,
} = require('./setup-provider-config');

const MODEL_TIER_ENV = {
  fast: 'WALLE_MODEL_FAST',
  balanced: 'WALLE_MODEL_BALANCED',
  quality: 'WALLE_MODEL_QUALITY',
};

// The starred default provider/model live in Wall-E's brain (set from the AI
// Providers page) and are served by GET /api/wall-e/status. CTM's process env
// (WALLE_PROVIDER/WALLE_MODEL from the launchd plist) goes stale the moment the
// user re-stars a provider, so background calls must ask Wall-E first and only
// fall back to env when Wall-E is unreachable. Cached briefly to keep the
// per-call overhead at one HTTP roundtrip per TTL, with a shorter negative TTL
// so a Wall-E restart doesn't pin us to env for long.
const WALLE_DEFAULTS_TTL_MS = 30000;
const WALLE_DEFAULTS_NEGATIVE_TTL_MS = 5000;
let _walleDefaultsCache = null; // { at, value: {provider, model} | null }

function _resetWalleDefaultsCache() {
  _walleDefaultsCache = null;
}

async function fetchWalleStatusDefaults(env) {
  const port = Number(env?.WALLE_PORT) || 3457;
  const response = await fetch(`http://127.0.0.1:${port}/api/wall-e/status`, {
    signal: AbortSignal.timeout(1500),
  });
  if (!response.ok) throw new Error(`Wall-E status returned ${response.status}`);
  const json = await response.json();
  return json?.data || json || {};
}

async function getWalleDefaultSelection(options = {}) {
  const env = options.env || process.env;
  if (String(env.CTM_BACKGROUND_LLM_WALLE_DEFAULTS || '').trim() === '0') return null;
  const now = Date.now();
  if (_walleDefaultsCache) {
    const ttl = _walleDefaultsCache.value ? WALLE_DEFAULTS_TTL_MS : WALLE_DEFAULTS_NEGATIVE_TTL_MS;
    if (now - _walleDefaultsCache.at < ttl) return _walleDefaultsCache.value;
  }
  let value = null;
  try {
    const fetchStatus = options.fetchWalleStatus || fetchWalleStatusDefaults;
    const status = await fetchStatus(env);
    const provider = sanitizeSetupProviderType(status?.walle_provider || '');
    if (provider) {
      value = { provider, model: typeof status?.walle_model === 'string' ? status.walle_model : '' };
    }
  } catch {
    value = null;
  }
  _walleDefaultsCache = { at: now, value };
  return value;
}

function defaultClientModule() {
  return require(path.resolve(__dirname, '..', '..', 'wall-e', 'llm', 'client'));
}

function defaultBrain() {
  return null;
}

function getBrainKv(brain, key) {
  try {
    return typeof brain?.getKv === 'function' ? (brain.getKv(key) || '') : '';
  } catch {
    return '';
  }
}

function getStoredProviderRow(brain, type) {
  try {
    return brain?.getDb?.().prepare(
      'SELECT api_key_encrypted, auth_method FROM model_providers WHERE type = ? AND enabled = 1 ORDER BY updated_at DESC LIMIT 1'
    ).get(type) || null;
  } catch {
    return null;
  }
}

function storedProviderKey(brain, type) {
  const row = getStoredProviderRow(brain, type);
  return row?.api_key_encrypted || '';
}

function providerRuntimeState({ brain, env, provider }) {
  const type = sanitizeSetupProviderType(provider);
  const row = getStoredProviderRow(brain, type);
  const authMethod = row?.auth_method
    || ((type === 'anthropic' || type === 'openai') ? (env.WALLE_AUTH_METHOD || '') : '');
  const hasStoredKey = !!row?.api_key_encrypted;
  return {
    type,
    authMethod,
    hasStoredKey,
    hasRuntimeAccess: setupProviderHasRuntimeAccess({
      type,
      env,
      authMethod,
      hasStoredKey,
    }),
  };
}

function modelMatchesProvider(model, provider, clientModule) {
  if (!model) return false;
  if (typeof clientModule?.modelMatchesProvider === 'function') {
    return clientModule.modelMatchesProvider(model, provider);
  }
  if (typeof clientModule?.resolveCompatibleModel === 'function') {
    return clientModule.resolveCompatibleModel(model, provider) === model;
  }
  return true;
}

function selectTierModel(modelTier, provider, { brain, env, clientModule } = {}) {
  const tier = ['fast', 'balanced', 'quality'].includes(modelTier) ? modelTier : '';
  if (!tier) return '';

  const envModel = env?.[MODEL_TIER_ENV[tier]] || '';
  if (envModel && modelMatchesProvider(envModel, provider, clientModule)) {
    return envModel;
  }

  try {
    const { DEFAULT_TIER_MODELS } = require(path.resolve(__dirname, '..', '..', 'wall-e', 'evaluation', 'tier-selector'));
    const defaultModel = DEFAULT_TIER_MODELS?.[provider]?.[tier] || '';
    if (defaultModel && modelMatchesProvider(defaultModel, provider, clientModule)) {
      return defaultModel;
    }
  } catch {}

  return '';
}

function resolveBackgroundLlmSelection(options = {}) {
  const env = options.env || process.env;
  const brain = options.brain === undefined ? defaultBrain() : options.brain;
  const clientModule = options.clientModule || defaultClientModule();
  const provider = sanitizeSetupProviderType(
    options.provider
      || getBrainKv(brain, 'walle_provider')
      || env.WALLE_PROVIDER
      || 'anthropic'
  ) || 'anthropic';
  const runtime = providerRuntimeState({ brain, env, provider });
  // options.defaultModel is the starred default model from Wall-E status — the
  // live value of the brain's walle_model KV, so it slots in at that priority,
  // ahead of the potentially stale env.WALLE_MODEL.
  const requestedModel = options.model
    || selectTierModel(options.modelTier, provider, { brain, env, clientModule })
    || getBrainKv(brain, 'walle_model_' + provider)
    || getBrainKv(brain, 'walle_model')
    || options.defaultModel
    || env.WALLE_MODEL
    || '';
  const model = clientModule.resolveCompatibleModel
    ? clientModule.resolveCompatibleModel(requestedModel, provider)
    : (requestedModel || '');
  const config = {};
  let clientType = provider;

  if (provider === 'anthropic' && runtime.authMethod === 'claude_cli') {
    clientType = 'claude-cli';
  } else if (provider === 'anthropic' && runtime.authMethod === 'oauth_proxy') {
    config.apiKey = 'oauth-proxy-placeholder';
    config.baseUrl = `http://127.0.0.1:${env.OAUTH_PROXY_PORT || '3458'}`;
  } else if (provider === 'openai' && runtime.authMethod === 'codex_cli') {
    clientType = 'codex-cli';
  } else if (provider === 'ollama' || provider === 'mlx') {
    if (provider === 'ollama' && env.OLLAMA_BASE_URL) config.baseUrl = env.OLLAMA_BASE_URL;
    if (provider === 'mlx' && env.MLX_MODEL) config.model = env.MLX_MODEL;
  } else {
    const envKey = SETUP_PROVIDER_ENV_KEYS[provider];
    let apiKey = storedProviderKey(brain, provider) || (envKey ? env[envKey] : '');
    if (provider === 'anthropic') apiKey = apiKey || env.ANTHROPIC_AUTH_TOKEN || '';
    if (provider === 'google') apiKey = apiKey || env.GEMINI_API_KEY || '';
    if (!apiKey && !runtime.hasRuntimeAccess) {
      const err = new Error(`No ${SETUP_PROVIDER_NAMES[provider] || provider} provider credentials configured`);
      err.provider = provider;
      err.model = model || requestedModel || '';
      err.reason = 'no_provider_credentials';
      throw err;
    }
    if (apiKey) config.apiKey = apiKey;
    if (provider === 'anthropic' && env.ANTHROPIC_BASE_URL) config.baseUrl = env.ANTHROPIC_BASE_URL;
    if (provider === 'openai' && env.OPENAI_BASE_URL) config.baseUrl = env.OPENAI_BASE_URL;
    if (provider === 'deepseek' && env.DEEPSEEK_BASE_URL) config.baseUrl = env.DEEPSEEK_BASE_URL;
    if (provider === 'moonshot' && env.MOONSHOT_BASE_URL) config.baseUrl = env.MOONSHOT_BASE_URL;
    if (provider === 'google' && env.GOOGLE_AUTH_MODE === 'oauth') {
      config.authMode = 'oauth';
      config.refreshToken = env.GOOGLE_REFRESH_TOKEN;
    }
  }

  return {
    provider,
    clientType,
    config,
    requestedModel,
    model,
    authMethod: runtime.authMethod,
    hasRuntimeAccess: runtime.hasRuntimeAccess,
    modelAdjusted: !!(requestedModel && model && requestedModel !== model),
  };
}

function extractBackgroundLlmText(response) {
  const content = response?.content ?? response?.text ?? '';
  if (typeof content === 'string') return content;
  if (Array.isArray(content)) {
    return content.map((part) => {
      if (!part) return '';
      if (typeof part === 'string') return part;
      return part.text || part.content || '';
    }).filter(Boolean).join('\n');
  }
  return '';
}

// Failure classes (wall-e/llm/provider-error.js) that mean "this provider can't
// serve background work right now" — worth one hop to the local fallback. This
// deliberately includes quota_exceeded and provider_rejected (a depleted
// account, e.g. DeepSeek "402 Insufficient Balance", classifies as the former;
// other 4xx rejections as the latter) — background work is best-effort, so any
// provider-side refusal is better served locally than dropped.
const FALLBACK_ELIGIBLE_CLASSES = new Set([
  'quota_exceeded', 'auth_error', 'rate_limited', 'provider_unavailable',
  'network', 'timeout', 'provider_rejected',
]);

function classifyBackgroundLlmError(err, options = {}) {
  if (err?.reason === 'no_provider_credentials') return 'auth_error';
  try {
    const classify = options.classifyError
      || require(path.resolve(__dirname, '..', '..', 'wall-e', 'llm', 'provider-error')).classifyProviderError;
    return classify(err)?.type || 'unknown';
  } catch {
    return 'unknown';
  }
}

function backgroundLlmFallbackProvider(env) {
  const raw = String(env?.CTM_BACKGROUND_LLM_FALLBACK_PROVIDER ?? 'ollama').trim().toLowerCase();
  if (!raw || raw === '0' || raw === 'none' || raw === 'off') return '';
  return sanitizeSetupProviderType(raw);
}

let _lastFallbackLogAt = 0;

async function callBackgroundLlm(prompt, options = {}) {
  const clientModule = options.clientModule || defaultClientModule();
  const env = options.env || process.env;

  // Respect the starred default from the AI Providers page (Wall-E brain) over
  // the process env, which only reflects the launchd plist at boot.
  let walleDefaults = null;
  if (!options.provider) walleDefaults = await getWalleDefaultSelection(options);

  const attempt = async (selection) => {
    const createClient = options.createClient || clientModule.createClient;
    const client = createClient(selection.clientType, selection.config);
    const timeoutMs = options.timeoutMs || 60000;
    const signal = options.signal || AbortSignal.timeout(timeoutMs);
    const response = await client.chat({
      model: selection.model || undefined,
      system: options.system,
      messages: [{ role: 'user', content: prompt }],
      maxTokens: options.maxTokens || 2048,
      temperature: options.temperature ?? 0.2,
      thinking: options.thinking || 'disabled',
      reasoningEffort: options.reasoningEffort || 'low',
      signal,
    });
    const text = extractBackgroundLlmText(response).replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
    return {
      text,
      provider: selection.provider,
      model: `${selection.provider}:${selection.model || response?.model || selection.clientType}`,
      requestedModel: selection.requestedModel,
      modelAdjusted: selection.modelAdjusted,
    };
  };

  const resolveOptions = {
    ...options,
    clientModule,
    provider: options.provider || walleDefaults?.provider || undefined,
    defaultModel: options.defaultModel
      || (walleDefaults?.provider && !options.provider ? walleDefaults.model : ''),
  };

  let selection = null;
  let primaryError = null;
  try {
    selection = resolveBackgroundLlmSelection(resolveOptions);
    return await attempt(selection);
  } catch (err) {
    primaryError = err;
  }

  const failedProvider = selection?.provider || primaryError.provider || resolveOptions.provider || '';
  primaryError.provider = primaryError.provider || failedProvider;
  primaryError.model = primaryError.model || selection?.model || selection?.requestedModel || '';
  primaryError.clientType = primaryError.clientType || selection?.clientType || '';

  // One hop to the local fallback provider (free, no surprise spend) when the
  // configured provider can't serve — never on caller-initiated aborts.
  const fallbackProvider = backgroundLlmFallbackProvider(env);
  const callerAborted = !!(options.signal && options.signal.aborted);
  if (!fallbackProvider || fallbackProvider === failedProvider || callerAborted) throw primaryError;
  const errorClass = classifyBackgroundLlmError(primaryError, options);
  if (!FALLBACK_ELIGIBLE_CLASSES.has(errorClass)) throw primaryError;

  try {
    const fallbackSelection = resolveBackgroundLlmSelection({
      ...resolveOptions,
      provider: fallbackProvider,
      model: undefined, // primary's model hints don't apply to the fallback
      defaultModel: walleDefaults?.provider === fallbackProvider ? walleDefaults.model : '',
    });
    const result = await attempt(fallbackSelection);
    const now = Date.now();
    if (now - _lastFallbackLogAt > 60000) {
      _lastFallbackLogAt = now;
      console.warn(`[background-llm] ${failedProvider} failed (${errorClass}: ${String(primaryError.message || '').slice(0, 120)}); served by fallback provider ${fallbackProvider}`);
    }
    return { ...result, fallbackFrom: failedProvider, fallbackReason: errorClass };
  } catch (fallbackErr) {
    primaryError.fallbackError = String(fallbackErr?.message || fallbackErr || '');
    throw primaryError;
  }
}

module.exports = {
  callBackgroundLlm,
  extractBackgroundLlmText,
  getWalleDefaultSelection,
  providerRuntimeState,
  resolveBackgroundLlmSelection,
  _resetWalleDefaultsCache,
};
