import Anthropic from '@anthropic-ai/sdk';
import { resolveAnthropicApiKey } from './anthropic-key.js';

// The SDK already retries transient failures (default maxRetries=2) honoring
// Retry-After. We raise that ceiling and add an explicit outer retry so a burst
// of rapid calls (e.g. the Mines diligence pipeline firing many ticks back to
// back) survives a short 429/529 instead of failing a whole report. Override
// with MADARX_ANTHROPIC_MAX_RETRIES.
const SDK_MAX_RETRIES = Number(process.env.MADARX_ANTHROPIC_MAX_RETRIES ?? 4);
const OUTER_MAX_ATTEMPTS = Number(process.env.MADARX_ANTHROPIC_OUTER_RETRIES ?? 3);

function getClient() {
  const apiKey = resolveAnthropicApiKey();
  if (!apiKey) throw new Error('ANTHROPIC_API_KEY not configured');
  return new Anthropic({ apiKey, maxRetries: SDK_MAX_RETRIES });
}

export const MODEL = 'claude-sonnet-4-6';

// Transient = worth retrying. 429 (rate limit), 408/409 (timeout/conflict),
// 5xx (incl. 529 overloaded), and connection/timeout errors with no status.
function isRetryable(err) {
  const status = err?.status;
  if (status === 429 || status === 408 || status === 409) return true;
  if (typeof status === 'number' && status >= 500) return true;
  // APIConnectionError / timeout surface without a status code.
  if (status == null && (err?.name === 'APIConnectionError' || err?.name === 'APIConnectionTimeoutError' || /timeout|ECONNRESET|ENOTFOUND|EAI_AGAIN|socket hang up/i.test(String(err?.message || '')))) return true;
  return false;
}

// Backoff delay in ms. Honors a Retry-After header (seconds) when present,
// else exponential (0.5s, 1s, 2s…) capped at 8s with jitter.
function backoffMs(err, attempt) {
  const ra = err?.headers?.get?.('retry-after');
  const raSec = ra != null ? Number(ra) : NaN;
  if (Number.isFinite(raSec) && raSec >= 0) return Math.min(raSec * 1000, 30000);
  const base = Math.min(500 * (2 ** attempt), 8000);
  return base + Math.floor(Math.random() * 250);
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// Run an Anthropic call with outer retry on transient failures. The SDK retries
// internally too; this guards against the SDK's own ceiling being exhausted in
// a burst. Non-transient errors (4xx other than 429/408/409) throw immediately.
async function withRetry(fn, label) {
  let lastErr;
  for (let attempt = 0; attempt < OUTER_MAX_ATTEMPTS; attempt += 1) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      if (!isRetryable(err) || attempt === OUTER_MAX_ATTEMPTS - 1) throw err;
      const delay = backoffMs(err, attempt);
      // eslint-disable-next-line no-console
      console.warn(`[claude] ${label} transient ${err?.status || err?.name || 'error'} — retry ${attempt + 1}/${OUTER_MAX_ATTEMPTS - 1} in ${delay}ms`);
      await sleep(delay);
    }
  }
  throw lastErr;
}

export async function chatWithClaude(messages, systemPrompt, tools = [], options = {}) {
  const client = getClient();
  const params = {
    model: options.model || MODEL,
    max_tokens: options.max_tokens || 8192,
    system: systemPrompt,
    messages,
  };
  if (tools.length) params.tools = tools;
  // Forward optional controls (e.g. tool_choice to force a structured tool call,
  // temperature for determinism). Only set when provided so defaults are intact.
  if (options.tool_choice) params.tool_choice = options.tool_choice;
  if (typeof options.temperature === 'number') params.temperature = options.temperature;
  return withRetry(() => client.messages.create(params), `chat(${params.model})`);
}

export async function streamChatWithClaude(messages, systemPrompt, tools = [], model = MODEL) {
  const client = getClient();
  const params = {
    model,
    max_tokens: 8192,
    system: systemPrompt,
    messages,
  };
  if (tools.length) params.tools = tools;
  // Stream cannot be resumed mid-flight; outer retry only covers establishing
  // the stream (connection/429 before the first token), not a mid-stream drop.
  return withRetry(() => client.messages.stream(params), `stream(${model})`);
}
