'use strict';

const Anthropic = require('@anthropic-ai/sdk');
const { fetchWithRetry } = require('./retry');
const { filterToSupportedModels, supportedModelsForProvider } = require('./supported-models');
const { isPortkeyProviderConfig } = require('./portkey');
const {
  isAnthropicSamplingParamError,
  stripAnthropicUnsupportedParams,
} = require('./request-compat');

// ============================================================
// Environment configuration
// ============================================================

/**
 * Build Anthropic client options from environment variables.
 * Reads: ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY,
 *        ANTHROPIC_CUSTOM_HEADERS_B64, ANTHROPIC_CUSTOM_HEADERS
 *
 * @returns {{ apiKey: string|undefined, baseURL: string|undefined, defaultHeaders: object }}
 */
function buildClientOptsFromEnv() {
  const opts = { defaultHeaders: {} };

  // API key vs OAuth token routing.
  //
  // Anthropic's @anthropic-ai/sdk distinguishes two auth fields:
  //   - apiKey    → sent as `X-Api-Key: <value>`
  //   - authToken → sent as `Authorization: Bearer <value>`
  //
  // Standard API keys (sk-ant-api03-…) belong in apiKey.
  // OAuth tokens issued by Claude Code's `claude.ai login` flow have the
  // `sk-ant-oat-` prefix and MUST go through Bearer (authToken) — putting
  // them in X-Api-Key gets rejected with "invalid x-api-key".
  //
  // ANTHROPIC_AUTH_TOKEN env var is always Bearer (matches SDK convention).
  // ANTHROPIC_API_KEY is auto-routed: oat-prefixed → authToken, otherwise apiKey.
  // OAuth token prefix is `sk-ant-oat<version>-…` (e.g. `sk-ant-oat01-…`).
  // Match `sk-ant-oat` followed by ANY non-hyphen-non-end (versions/digits)
  // so future `oat02-`, `oatNN-` variants stay covered.
  const isOAuthToken = (s) => typeof s === 'string' && /^sk-ant-oat\w*-/i.test(s);
  if (process.env.ANTHROPIC_AUTH_TOKEN) {
    opts.authToken = process.env.ANTHROPIC_AUTH_TOKEN;
  } else if (process.env.ANTHROPIC_API_KEY) {
    if (isOAuthToken(process.env.ANTHROPIC_API_KEY)) {
      opts.authToken = process.env.ANTHROPIC_API_KEY;
    } else {
      opts.apiKey = process.env.ANTHROPIC_API_KEY;
    }
  }

  if (process.env.ANTHROPIC_BASE_URL) {
    opts.baseURL = process.env.ANTHROPIC_BASE_URL;
  }

  // Parse custom headers — prefer base64 version (has all headers including virtual-key)
  let headerText = '';
  if (process.env.ANTHROPIC_CUSTOM_HEADERS_B64) {
    headerText = Buffer.from(process.env.ANTHROPIC_CUSTOM_HEADERS_B64, 'base64').toString();
  } else if (process.env.ANTHROPIC_CUSTOM_HEADERS) {
    headerText = process.env.ANTHROPIC_CUSTOM_HEADERS;
  }
  if (headerText) {
    for (const line of headerText.split('\n')) {
      const idx = line.indexOf(':');
      if (idx > 0) {
        opts.defaultHeaders[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
      }
    }
  }

  return opts;
}

// ============================================================
// Hardcoded model catalog (fallback when /v1/models is unreachable)
// ============================================================

const MODELS = supportedModelsForProvider('anthropic');

function portkeyMessagesUrl(baseUrl) {
  const root = String(baseUrl || '').trim().replace(/\/+$/, '');
  if (!root) return '/v1/messages';
  return /\/v1$/i.test(root) ? `${root}/messages` : `${root}/v1/messages`;
}

// ============================================================
// Prompt caching
// ============================================================

const EPHEMERAL = Object.freeze({ type: 'ephemeral' });

function _stripCacheControl(block) {
  if (block && typeof block === 'object' && block.cache_control) {
    const { cache_control, ...rest } = block;
    return rest;
  }
  return block;
}

// Thinking blocks can't carry cache_control; empty text blocks are rejected.
function _blockSupportsCacheControl(block) {
  if (!block || typeof block !== 'object') return false;
  if (block.type === 'thinking' || block.type === 'redacted_thinking') return false;
  if (block.type === 'text' && !String(block.text || '').length) return false;
  return true;
}

/**
 * Place prompt-cache breakpoints on a /v1/messages request:
 * last tool definition, last system block, and a sliding breakpoint on the
 * last content block of the last message (re-placed every turn so turn N+1
 * reads turn N's prefix from cache). Uses 3 of the 4 allowed breakpoints.
 *
 * Pure: returns a new request object; never mutates the caller's messages
 * (the agent loop reuses message objects across turns — mutating them would
 * accumulate stale breakpoints and blow the 4-breakpoint API limit).
 */
function applyAnthropicPromptCaching(reqParams) {
  const out = { ...reqParams };

  if (typeof out.system === 'string' && out.system.length > 0) {
    out.system = [{ type: 'text', text: out.system, cache_control: EPHEMERAL }];
  } else if (Array.isArray(out.system) && out.system.length > 0) {
    const blocks = out.system.map(_stripCacheControl);
    const last = blocks.length - 1;
    if (_blockSupportsCacheControl(blocks[last])) {
      blocks[last] = { ...blocks[last], cache_control: EPHEMERAL };
    }
    out.system = blocks;
  }

  if (Array.isArray(out.tools) && out.tools.length > 0) {
    const tools = out.tools.map(_stripCacheControl);
    tools[tools.length - 1] = { ...tools[tools.length - 1], cache_control: EPHEMERAL };
    out.tools = tools;
  }

  if (Array.isArray(out.messages) && out.messages.length > 0) {
    const messages = out.messages.map((msg) => (
      msg && Array.isArray(msg.content)
        ? { ...msg, content: msg.content.map(_stripCacheControl) }
        : msg
    ));
    const lastIdx = messages.length - 1;
    const last = messages[lastIdx];
    if (last && typeof last === 'object') {
      if (typeof last.content === 'string' && last.content.length > 0) {
        messages[lastIdx] = { ...last, content: [{ type: 'text', text: last.content, cache_control: EPHEMERAL }] };
      } else if (Array.isArray(last.content) && last.content.length > 0) {
        const blocks = [...last.content];
        const lastBlock = blocks.length - 1;
        if (_blockSupportsCacheControl(blocks[lastBlock])) {
          blocks[lastBlock] = { ...blocks[lastBlock], cache_control: EPHEMERAL };
          messages[lastIdx] = { ...last, content: blocks };
        }
      }
    }
    out.messages = messages;
  }

  return out;
}

// Build a human-readable display name from a model id, e.g.
// "claude-opus-4-7" -> "Claude Opus 4.7"
function _prettyAnthropicName(id, providedDisplayName) {
  if (providedDisplayName && typeof providedDisplayName === 'string') return providedDisplayName;
  const m = id.match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)(?:-\d{8})?$/);
  if (m) {
    const tier = m[1].charAt(0).toUpperCase() + m[1].slice(1);
    return `Claude ${tier} ${m[2]}.${m[3]}`;
  }
  const m2 = id.match(/^claude-(\d)-(\d)-(opus|sonnet|haiku)-\d{8}$/);
  if (m2) {
    const tier = m2[3].charAt(0).toUpperCase() + m2[3].slice(1);
    return `Claude ${m2[1]}.${m2[2]} ${tier}`;
  }
  return id;
}

// ============================================================
// Provider factory
// ============================================================

/**
 * Create an Anthropic provider instance.
 *
 * @param {object} config
 * @param {string}  [config.apiKey]        - Anthropic API key (X-Api-Key header)
 * @param {string}  [config.authToken]     - OAuth bearer token (Authorization: Bearer)
 *                                            Use for `sk-ant-oat-…` tokens issued by
 *                                            Claude Code's `claude.ai login` flow.
 * @param {string}  [config.baseUrl]       - Base URL (e.g. Portkey gateway)
 * @param {object}  [config.customHeaders] - Extra headers (Portkey detection key: x-portkey-api-key)
 * @returns {object} Provider with { type, chat, listModels, _isPortkey }
 */
function createAnthropicProvider(config = {}) {
  const { apiKey, authToken, baseUrl, customHeaders = {} } = config;
  const isPortkey = Boolean(customHeaders['x-portkey-api-key']);

  // Build the underlying client
  let client;
  if (isPortkey) {
    // Portkey gateway: SDK sends x-api-key which breaks auth. Use fetch wrapper.
    if (!baseUrl) {
      throw new Error(
        'Anthropic provider has Portkey custom headers (x-portkey-api-key) but no baseUrl. ' +
        'Set ANTHROPIC_BASE_URL to your Portkey gateway URL (e.g. https://api.portkey.ai) ' +
        'or remove the x-portkey-api-key header from ANTHROPIC_CUSTOM_HEADERS.'
      );
    }
    const headers = {
      'content-type': 'application/json',
      'anthropic-version': '2023-06-01',
      ...customHeaders,
    };
    // If the gateway forwards OAuth-style Bearer auth (`Authorization: Bearer
    // sk-ant-oat…`), Anthropic rejects with 401 "OAuth authentication is
    // currently not supported" unless `anthropic-beta: oauth-2025-04-20` is
    // present. Auto-add (idempotent) so devbox/Portkey paths work too.
    {
      const authHeader = headers['authorization'] || headers['Authorization'] || '';
      const betaHeader = headers['anthropic-beta'] || headers['Anthropic-Beta'] || '';
      const isOAuthBearer = /^Bearer\s+sk-ant-oat/i.test(authHeader);
      if (isOAuthBearer && !/oauth-2025-04-20/.test(betaHeader)) {
        headers['anthropic-beta'] = betaHeader ? betaHeader + ',oauth-2025-04-20' : 'oauth-2025-04-20';
      }
    }
    const messagesUrl = portkeyMessagesUrl(baseUrl);

    client = {
      messages: {
        async create(params, fetchOpts) {
          const res = await fetchWithRetry(messagesUrl, {
            method: 'POST',
            headers,
            body: JSON.stringify(params),
            signal: fetchOpts?.signal,
          });
          return res.json();
        },
      },
    };
  } else {
    // SDK constructor uses destructuring defaults — `apiKey = readEnv(…) ?? null`
    // only triggers the env fallback when the prop is `undefined`. When we want
    // authToken auth, we must explicitly null `apiKey` to suppress the env-read,
    // otherwise SDK picks up ANTHROPIC_API_KEY from env and sends BOTH
    // X-Api-Key + Authorization headers (which Anthropic rejects with 401
    // "invalid x-api-key").
    const sdkOpts = {};
    // Claude.ai OAuth Bearer tokens (`sk-ant-oat01-…`) require the
    // `anthropic-beta: oauth-2025-04-20` header on every request — without it
    // /v1/messages rejects with 401 "OAuth authentication is currently not
    // supported." Auto-inject the beta header alongside customHeaders so the
    // user doesn't have to remember it.
    const headersWithOauthBeta = { ...customHeaders };
    if (authToken) {
      sdkOpts.authToken = authToken;
      sdkOpts.apiKey = null;          // suppress env-fallback
      const existingBeta = headersWithOauthBeta['anthropic-beta'] || headersWithOauthBeta['Anthropic-Beta'];
      if (!existingBeta) {
        headersWithOauthBeta['anthropic-beta'] = 'oauth-2025-04-20';
      } else if (!/oauth-2025-04-20/.test(existingBeta)) {
        headersWithOauthBeta['anthropic-beta'] = existingBeta + ',oauth-2025-04-20';
      }
    } else if (apiKey) {
      sdkOpts.apiKey = apiKey;
      sdkOpts.authToken = null;       // suppress env-fallback
    }
    if (baseUrl) sdkOpts.baseURL = baseUrl;
    if (Object.keys(headersWithOauthBeta).length > 0) sdkOpts.defaultHeaders = headersWithOauthBeta;
    client = new Anthropic(sdkOpts);
  }

  return {
    type: 'anthropic',
    _isPortkey: isPortkey,

    /**
     * Send a chat request to the Anthropic API.
     *
     * @param {object} params
     * @param {string}  params.model       - Model ID
     * @param {Array}   params.messages    - Anthropic-format messages
     * @param {Array}   [params.tools]     - Anthropic-format tool definitions
     * @param {string}  [params.system]    - System prompt
     * @param {number}  [params.maxTokens] - Max output tokens (default 4096)
     * @param {number}  [params.temperature]
     * @param {number}  [params.top_p]
     * @param {number}  [params.top_k]
     * @param {AbortSignal} [params.signal]
     * @returns {Promise<object>} Normalized response
     */
    async chat({ model, messages, tools, system, maxTokens = 4096, temperature, top_p, topP, top_k, topK, signal, promptCache } = {}) {
      const start = Date.now();

      let reqParams = {
        model,
        messages,
        max_tokens: maxTokens,
      };
      if (system) reqParams.system = system;
      if (tools && tools.length > 0) reqParams.tools = tools;
      if (temperature !== undefined) reqParams.temperature = temperature;
      if (top_p !== undefined) reqParams.top_p = top_p;
      if (topP !== undefined && reqParams.top_p === undefined) reqParams.top_p = topP;
      if (top_k !== undefined) reqParams.top_k = top_k;
      if (topK !== undefined && reqParams.top_k === undefined) reqParams.top_k = topK;
      if (promptCache) reqParams = applyAnthropicPromptCaching(reqParams);

      const compat = stripAnthropicUnsupportedParams(reqParams, { model });
      const providerReqParams = compat.request;

      const fetchOpts = signal ? { signal } : undefined;
      let raw;
      try {
        raw = await client.messages.create(providerReqParams, fetchOpts);
      } catch (err) {
        const fallback = stripAnthropicUnsupportedParams(reqParams, { model, force: true });
        const didStrip = fallback.warnings.some((warning) => warning.keys?.length > 0);
        if (!didStrip || !isAnthropicSamplingParamError(err)) throw err;
        raw = await client.messages.create(fallback.request, fetchOpts);
      }
      const latencyMs = Date.now() - start;

      // Normalize response
      const textParts = (raw.content || []).filter((b) => b.type === 'text');
      const toolUses = (raw.content || []).filter((b) => b.type === 'tool_use');

      return {
        content: textParts.map((b) => b.text).join('\n') || null,
        toolCalls: toolUses.map((tu) => ({
          id: tu.id,
          name: tu.name,
          input: tu.input,
        })),
        stopReason: raw.stop_reason === 'end_turn' ? 'end_turn' : raw.stop_reason === 'tool_use' ? 'tool_use' : raw.stop_reason,
        // `input` is the TOTAL effective input (uncached + cache hits + cache
        // writes) so context-size consumers (compaction triggers) see the real
        // prompt size; cacheRead/cacheWrite let cost accounting discount them.
        usage: raw.usage
          ? {
            input: (raw.usage.input_tokens || 0)
              + (raw.usage.cache_read_input_tokens || 0)
              + (raw.usage.cache_creation_input_tokens || 0),
            output: raw.usage.output_tokens,
            cacheRead: raw.usage.cache_read_input_tokens || 0,
            cacheWrite: raw.usage.cache_creation_input_tokens || 0,
          }
          : undefined,
        latencyMs,
        model: raw.model || model,
        provider: 'anthropic',
        raw,
      };
    },

    /**
     * List available Anthropic models. Fetches /v1/models with the configured
     * key/headers; falls back to hardcoded MODELS on any failure (no key,
     * non-2xx, network error). Always async to allow live fetch.
     * @returns {Promise<Array<{id: string, name: string, capabilities: object}>>}
     */
    async listModels(options = {}) {
      const exact = Boolean(options.exact || options.liveOnly);
      const includeUnknown = Boolean(
        options.includeUnknown
        || options.allowUnknown
        || isPortkeyProviderConfig({ baseUrl, customHeaders })
      );
      const configuredHeaders = customHeaders || {};
      const hasHeaderCredential = Boolean(
        configuredHeaders['x-api-key']
        || configuredHeaders['X-Api-Key']
        || configuredHeaders.authorization
        || configuredHeaders.Authorization
      );
      const credential = apiKey || authToken;
      if (!credential && !hasHeaderCredential) return exact ? [] : [...MODELS];
      const endpoint = (baseUrl || 'https://api.anthropic.com').replace(/\/+$/, '').replace(/\/v1$/, '') + '/v1/models?limit=1000';
      const headers = {
        'anthropic-version': '2023-06-01',
        ...configuredHeaders,
      };
      // OAuth tokens (sk-ant-oat-*) use Bearer; everything else (api keys, gateway tokens) uses x-api-key.
      if (!hasHeaderCredential && credential) {
        if (authToken || (typeof credential === 'string' && credential.startsWith('sk-ant-oat-'))) {
          headers['authorization'] = `Bearer ${credential}`;
        } else {
          headers['x-api-key'] = credential;
        }
      }
      try {
        const resp = await fetch(endpoint, { headers, signal: AbortSignal.timeout(8000) });
        if (!resp.ok) {
          if (exact) throw new Error(`Anthropic models API returned HTTP ${resp.status}`);
          return [...MODELS];
        }
        const data = await resp.json();
        const list = Array.isArray(data?.data) ? data.data : [];
        if (list.length === 0) return exact ? [] : [...MODELS];
        const out = [];
        for (const m of list) {
          if (!m?.id) continue;
          const id = m.id;
          // Best-effort capability inference — Anthropic /v1/models doesn't expose them
          const capabilities = { vision: true, tools: true, streaming: true };
          if (/opus/i.test(id) || /-4-7/.test(id)) capabilities.reasoning = true;
          out.push({ id, name: _prettyAnthropicName(id, m.display_name), capabilities });
        }
        if (includeUnknown) return out;
        const supported = filterToSupportedModels('anthropic', out);
        return supported.length > 0 ? supported : supportedModelsForProvider('anthropic');
      } catch (err) {
        if (exact) throw err;
        console.warn('[anthropic] Failed to list models from API, using fallback:', err.message);
        return supportedModelsForProvider('anthropic');
      }
    },
  };
}

/**
 * Create an Anthropic provider from environment variables.
 * Convenience wrapper around buildClientOptsFromEnv + createAnthropicProvider.
 *
 * @returns {object} Provider instance
 */
function createAnthropicFromEnv() {
  const opts = buildClientOptsFromEnv();
  return createAnthropicProvider({
    apiKey: opts.apiKey,
    authToken: opts.authToken,
    baseUrl: opts.baseURL,
    customHeaders: opts.defaultHeaders,
  });
}

module.exports = {
  buildClientOptsFromEnv,
  createAnthropicProvider,
  createAnthropicFromEnv,
  portkeyMessagesUrl,
  applyAnthropicPromptCaching,
};
