'use strict';

const MAX_RAW_MESSAGE_CHARS = 2000;

const PROVIDER_LABELS = {
  anthropic: 'Anthropic',
  openai: 'OpenAI',
  google: 'Google',
  deepseek: 'DeepSeek',
  moonshot: 'Moonshot / Kimi',
  ollama: 'Ollama',
  mlx: 'MLX',
};

function providerLabel(provider) {
  const key = String(provider || '').toLowerCase();
  return PROVIDER_LABELS[key] || provider || 'AI provider';
}

function redactSecrets(value) {
  return String(value || '')
    .replace(/\bsk-[A-Za-z0-9_-]{12,}\b/g, 'sk-...redacted')
    .replace(/\bya29\.[A-Za-z0-9._-]{12,}\b/g, 'ya29...redacted')
    .replace(/\bAIza[A-Za-z0-9_-]{20,}\b/g, 'AIza...redacted')
    .replace(/(api[_-]?key["'\s:=]+)[^"',\s]+/gi, '$1...redacted')
    .slice(0, MAX_RAW_MESSAGE_CHARS);
}

function tryJson(value) {
  if (!value || typeof value !== 'string') return null;
  try { return JSON.parse(value); } catch { return null; }
}

function collectStrings(value, out = []) {
  if (value == null) return out;
  if (typeof value === 'string') {
    out.push(value);
    const parsed = tryJson(value);
    if (parsed) collectStrings(parsed, out);
    return out;
  }
  if (typeof value === 'number' || typeof value === 'boolean') {
    out.push(String(value));
    return out;
  }
  if (Array.isArray(value)) {
    value.forEach((item) => collectStrings(item, out));
    return out;
  }
  if (typeof value === 'object') {
    for (const key of ['message', 'error', 'type', 'code', 'status', 'statusCode', 'details', 'body']) {
      if (Object.prototype.hasOwnProperty.call(value, key)) collectStrings(value[key], out);
    }
  }
  return out;
}

function extractStatus(err) {
  const candidates = [
    err?.status,
    err?.statusCode,
    err?.response?.status,
    err?.response?.statusCode,
    err?.error?.status,
    err?.error?.statusCode,
  ];
  for (const candidate of candidates) {
    const n = Number(candidate);
    if (Number.isFinite(n) && n >= 100 && n < 600) return n;
  }
  const message = String(err?.message || err || '');
  const m = message.match(/\b(?:api error|http|status)\s*[:#]?\s*(\d{3})\b/i);
  return m ? Number(m[1]) : null;
}

function extractRetryAfter(err) {
  const headers = err?.headers || err?.response?.headers;
  if (!headers) return null;
  const get = typeof headers.get === 'function'
    ? (name) => headers.get(name)
    : (name) => headers[name] || headers[name.toLowerCase()];
  const retryAfterMs = get('retry-after-ms');
  if (retryAfterMs && !Number.isNaN(Number(retryAfterMs))) return `${Math.round(Number(retryAfterMs) / 1000)}s`;
  const retryAfter = get('retry-after');
  if (!retryAfter) return null;
  const seconds = Number(retryAfter);
  if (!Number.isNaN(seconds)) return `${Math.round(seconds)}s`;
  const date = Date.parse(retryAfter);
  if (!Number.isNaN(date)) {
    const delta = Math.max(0, date - Date.now());
    return `${Math.ceil(delta / 1000)}s`;
  }
  return String(retryAfter).slice(0, 80);
}

function buildRawMessage(err) {
  const pieces = [];
  collectStrings(err?.responseBody, pieces);
  collectStrings(err?.body, pieces);
  collectStrings(err?.error, pieces);
  collectStrings(err?.cause, pieces);
  collectStrings(err?.message || err, pieces);
  const unique = [...new Set(pieces.map((part) => String(part || '').trim()).filter(Boolean))];
  return redactSecrets(unique.join('\n'));
}

function classifyType(status, lower) {
  if (/insufficient[_ -]?(quota|balance|funds)|quota exceeded|quota_exceeded|resource[_ -]?exhausted|billing|credit|prepayment credits are depleted/i.test(lower)) return 'quota_exceeded';
  if (status === 429 || /rate[_ -]?limit|too many requests|too_many_requests|rate_limit/i.test(lower)) return 'rate_limited';
  if (
    status === 401
    || status === 403
    || /invalid[_ -]?(api[_ -]?)?key|unauthori[sz]ed|forbidden|authentication|auth token|authtoken|api[_ -]?key|could not resolve authentication method|expected either apikey or authtoken|oauth token not found|claude auth login|anthropic_api_key|token refresh failed|permission denied/i.test(lower)
  ) return 'auth_error';
  if (status === 404 || /model .*not found|not_found_error|model_not_found|does not exist|unknown model/i.test(lower)) return 'model_unavailable';
  if (/context length exceeded|context_length_exceeded|maximum context|token limit/i.test(lower)) return 'context_window';
  if (status === 408 || /timeout|timed out|etimedout|aborterror|aborted/i.test(lower)) return 'timeout';
  if (status === 529 || status === 503 || /overloaded|service unavailable|temporarily unavailable|unavailable/i.test(lower)) return 'provider_unavailable';
  if (/fetch failed|network|eai_again|enotfound|econnreset|socket hang up|connection refused|econnrefused/i.test(lower)) return 'network';
  if (status && status >= 500) return 'provider_unavailable';
  if (status && status >= 400) return 'provider_rejected';
  return 'provider_error';
}

function messageForType(type) {
  switch (type) {
    case 'rate_limited':
      return {
        title: 'AI provider rate limited',
        body: 'The provider rejected the request because it is rate limited. Wait and retry, or switch provider/model in Setup.',
      };
    case 'quota_exceeded':
      return {
        title: 'AI provider quota exhausted',
        body: 'The provider rejected the request because the account quota or credits are exhausted. Update billing or switch provider/model in Setup.',
      };
    case 'auth_error':
      return {
        title: 'AI provider authentication failed',
        body: 'The provider rejected the configured credential. Update the API key/token in Setup, then retry.',
      };
    case 'model_unavailable':
      return {
        title: 'AI model unavailable',
        body: 'The selected model is not available from the configured provider. Pick another model in Setup, then retry.',
      };
    case 'context_window':
      return {
        title: 'AI request exceeded context',
        body: 'The provider rejected the request because the conversation or attachments exceed the model context window. Shorten the prompt or switch to a larger-context model.',
      };
    case 'timeout':
      return {
        title: 'AI provider timed out',
        body: 'The provider did not answer before the timeout. Retry, or switch provider/model if this keeps happening.',
      };
    case 'network':
      return {
        title: 'AI provider network error',
        body: 'Wall-E could not reach the provider endpoint. Check connectivity, base URL, or local provider status, then retry.',
      };
    case 'provider_unavailable':
      return {
        title: 'AI provider unavailable',
        body: 'The provider is currently unavailable or overloaded. Retry later, or switch provider/model in Setup.',
      };
    case 'provider_unavailable_local':
      return {
        title: 'No AI provider available',
        body: 'Providers are configured, but every provider is currently marked unhealthy. Check Setup or wait for health checks to recover.',
      };
    default:
      return {
        title: 'AI provider failed',
        body: 'Wall-E could not get a response from the configured provider. Review the raw provider error, then retry or switch provider/model in Setup.',
      };
  }
}

function classifyProviderError(err, context = {}) {
  if (err?.providerError && typeof err.providerError === 'object') return err.providerError;
  const status = extractStatus(err);
  const rawMessage = buildRawMessage(err);
  const lower = rawMessage.toLowerCase();
  const type = context.type || classifyType(status, lower);
  const copy = messageForType(type);
  const provider = context.provider || err?.provider || err?.providerType || null;
  const model = context.model || err?.model || null;
  const providerId = context.providerId || err?.providerId || err?.provider_id || null;
  const registryId = context.registryId || err?.registryId || err?.registry_id || null;
  const routeLabel = context.routeLabel || err?.routeLabel || err?.route_label || null;
  const connectionKind = context.connectionKind || err?.connectionKind || err?.connection_kind || null;
  const providerName = providerLabel(provider);
  const modelSuffix = model ? ` (${model})` : '';
  const retryAfter = extractRetryAfter(err);
  const retrySuffix = retryAfter ? ` Retry after ${retryAfter}.` : '';
  return {
    code: 'AI_PROVIDER_ERROR',
    type,
    severity: type === 'context_window' ? 'warning' : 'error',
    title: copy.title,
    message: copy.body,
    userMessage: `${providerName}${modelSuffix}: ${copy.body}${retrySuffix}`,
    rawMessage,
    status,
    provider,
    providerId,
    registryId,
    routeLabel,
    connectionKind,
    model,
    retryAfter,
    actionLabel: 'Open Setup',
    actionUrl: '/setup.html',
    createdAt: new Date().toISOString(),
  };
}

function decorateProviderError(err, context = {}) {
  const original = err instanceof Error ? err : new Error(String(err || 'AI provider failed'));
  const providerError = classifyProviderError(original, context);
  const base = new Error(original.message || providerError.userMessage || 'AI provider failed');
  base.name = original.name || base.name;
  if (original.stack) base.stack = original.stack;
  base.providerError = providerError;
  base.code = 'AI_PROVIDER_ERROR';
  if (providerError.status || original.status) base.status = providerError.status || original.status;
  try { base.cause = original; } catch {}
  return base;
}

function unavailableProviderError(providers = [], context = {}) {
  const details = providers
    .map((provider) => {
      const label = provider.providerType || provider.providerId || 'provider';
      return `${label}: ${provider.lastError || provider.status || 'unhealthy'}`;
    })
    .join('\n');
  const err = new Error(details || 'No configured AI provider is currently available');
  err.status = 503;
  return decorateProviderError(err, {
    ...context,
    type: 'provider_unavailable_local',
    provider: context.provider || providers[0]?.providerType || null,
  });
}

function toApiPayload(err, context = {}) {
  const providerError = classifyProviderError(err, context);
  return {
    error: providerError.title,
    message: providerError.userMessage,
    code: 'AI_PROVIDER_ERROR',
    providerError,
  };
}

function recordProviderFailureAlert(providerError, brain) {
  if (!providerError) return { alerted: false };
  try {
    const planner = require('../skills/skill-planner');
    const actionUrl = providerError.type === 'request_invariant'
      ? null
      : (providerError.actionUrl || '/setup.html');
    planner.addServiceAlert({
      service: 'ai_provider',
      type: `ai_provider:${providerError.provider || 'default'}:${providerError.type}`,
      title: providerError.title,
      severity: providerError.severity || 'error',
      provider: providerError.provider || null,
      model: providerError.model || null,
      message: `${providerError.title}: ${providerError.userMessage}`,
      action_url: actionUrl,
      provider_error: providerError,
    });
    return { alerted: true };
  } catch (err) {
    return { alerted: false, error: err.message };
  }
}

// One-line, secret-redacted human detail of a failed-provider error — for the
// routing-notice banner, so a degraded default surfaces WHY ("HTTP 404: anthropic
// error: model deepseek-v4-flash") instead of a bare "unreachable". Never throws.
function fallbackErrorDetail(err) {
  if (!err) return '';
  let raw = '';
  try { raw = buildRawMessage(err); }
  catch { try { raw = redactSecrets(String((err && err.message) || err || '')); } catch { raw = ''; } }
  raw = String(raw || '').replace(/\s+/g, ' ').trim();
  if (!raw) return '';
  let status = 0;
  try { status = extractStatus(err); } catch { status = 0; }
  if (status && !new RegExp('\\b' + status + '\\b').test(raw)) raw = `HTTP ${status}: ${raw}`;
  return raw.slice(0, 200);
}

module.exports = {
  classifyProviderError,
  decorateProviderError,
  fallbackErrorDetail,
  providerLabel,
  recordProviderFailureAlert,
  redactSecrets,
  toApiPayload,
  unavailableProviderError,
};
