/**
 * Anthropic adapter — uses Vercel AI SDK (@ai-sdk/anthropic + ai).
 * Public contract (streamLlm interface) is unchanged from the hand-rolled
 * version this replaced; agent-loop.ts callers see no difference.
 *
 * Forge-specific bits we keep:
 *  - OAuth token (sk-ant-oat-*) → Authorization: Bearer + oauth beta header
 *  - Connector tool names (dotted, e.g. "gitlab.list_my_mrs") encoded as
 *    "__" to satisfy Anthropic's name regex; decoded on tool_use events
 */

import Anthropic from '@anthropic-ai/sdk';
import { createAnthropic } from '@ai-sdk/anthropic';
import { jsonSchema, streamText, type ModelMessage } from 'ai';
import type { ContentBlock, Message } from '../types';
import type { LlmAdapter, LlmCallbacks, LlmRequest, LlmTurnResult, StopReason } from './types';

function isOauthToken(key: string): boolean {
  return key.startsWith('sk-ant-oat');
}

/**
 * Raw Anthropic SDK client (one-shot calls, non-streaming). Used by
 * /api/schedules/extract for structured JSON extraction. Kept here so OAuth
 * + baseURL handling matches the streaming adapter; callers wanting tool
 * calls / streaming should use streamLlm via the Vercel AI SDK adapter below.
 */
export function makeAnthropicClient(apiKey: string, baseUrl: string): Anthropic {
  const oauth = isOauthToken(apiKey);
  const opts: ConstructorParameters<typeof Anthropic>[0] = {
    baseURL: baseUrl || undefined,
  };
  if (oauth) {
    opts.authToken = apiKey;
    opts.defaultHeaders = { 'anthropic-beta': 'oauth-2025-04-20' };
  } else {
    opts.apiKey = apiKey;
  }
  return new Anthropic(opts);
}

/**
 * Anthropic restricts tool names to `^[a-zA-Z0-9_-]{1,128}$`. Forge's
 * connector tools use a `connector.tool` namespace, so we encode the
 * dot as `__` on the way out and decode it back on tool_use. Internal
 * dispatcher still sees the canonical dotted form.
 */
function encodeToolName(name: string): string { return name.replace(/\./g, '__'); }
function decodeToolName(name: string): string { return name.replace(/__/g, '.'); }

function makeClient(apiKey: string, baseUrl: string) {
  const oauth = isOauthToken(apiKey);
  // @ai-sdk/anthropic appends `/messages` directly onto the baseURL (unlike
  // @anthropic-ai/sdk which inserts `/v1/`). If the URL doesn't end in /v1,
  // the request goes to `${host}/messages` and 404s. Forge's defaultBaseUrl
  // returns the host without /v1 to match the other SDK — normalize here.
  const normalized = baseUrl
    ? baseUrl.replace(/\/+$/, '') + (/\/v\d+$/.test(baseUrl.replace(/\/+$/, '')) ? '' : '/v1')
    : undefined;
  return createAnthropic({
    apiKey: oauth ? '' : apiKey,
    baseURL: normalized,
    headers: oauth
      ? {
          authorization: `Bearer ${apiKey}`,
          'anthropic-beta': 'oauth-2025-04-20',
        }
      : undefined,
  });
}

/** Convert Forge Message[] → AI SDK ModelMessage[]. */
function historyToModelMessages(history: Message[]): ModelMessage[] {
  const out: ModelMessage[] = [];
  for (const m of history) {
    if (m.role === 'user') {
      // User message may carry plain text OR tool_result blocks.
      const toolResults = m.blocks.filter((b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result');
      if (toolResults.length > 0) {
        out.push({
          role: 'tool',
          content: toolResults.map((r) => ({
            type: 'tool-result' as const,
            toolCallId: r.tool_use_id,
            toolName: '', // tool name not tracked in our model; SDK ignores when matching by toolCallId
            output: { type: 'text', value: r.content },
          })),
        });
        continue;
      }
      const text = m.blocks
        .filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
        .map((b) => b.text).join('\n');
      if (text.length > 0) out.push({ role: 'user', content: text });
    } else {
      // Assistant: text + tool_use blocks
      const parts: any[] = [];
      for (const b of m.blocks) {
        if (b.type === 'text' && b.text.length > 0) {
          parts.push({ type: 'text', text: b.text });
        } else if (b.type === 'tool_use') {
          parts.push({
            type: 'tool-call',
            toolCallId: b.id,
            toolName: encodeToolName(b.name),
            input: b.input ?? {},
          });
        }
      }
      if (parts.length > 0) out.push({ role: 'assistant', content: parts });
    }
  }
  return out;
}

function mapStop(r: string | undefined): StopReason {
  switch (r) {
    case 'stop': return 'end_turn';
    case 'tool-calls': return 'tool_use';
    case 'length': return 'max_tokens';
    case 'content-filter': return 'refusal';
    case 'error': return 'error';
    default: return 'other';
  }
}

export const anthropicAdapter: LlmAdapter = {
  async stream(req: LlmRequest, cb: LlmCallbacks): Promise<LlmTurnResult> {
    const client = makeClient(req.apiKey, req.baseUrl);

    // Build tool set as Record<encoded_name, ToolDef>. We DO NOT supply
    // execute — chat owns dispatch (destructive confirm, browser bridge,
    // memory tools etc all live in agent-loop). Setting stopWhen with
    // stepCountIs(1) prevents the SDK from auto-rolling a second step.
    // Build tool record. Mark the LAST tool with cache_control so
    // Anthropic-family backends (or LiteLLM proxies that forward it)
    // cache the system+tools prefix. Subsequent turns within the 5-min
    // TTL pay 0.1× input price for the cached portion instead of 1×.
    // Backends that don't honor cache_control silently ignore it,
    // costing nothing.
    const toolNames = req.tools.map((t) => t.name);
    const lastName = toolNames[toolNames.length - 1];
    const tools: Record<string, any> = {};
    for (const t of req.tools) {
      tools[encodeToolName(t.name)] = {
        description: t.description,
        inputSchema: jsonSchema(t.input_schema),
        ...(t.name === lastName ? {
          providerOptions: {
            anthropic: { cacheControl: { type: 'ephemeral' } },
          },
        } : {}),
      };
    }

    // Single cache breakpoint at end-of-tools — Anthropic caches the
    // prefix (system + tools) since system comes first in the wire
    // format. No need to add a separate marker on system.
    const result = streamText({
      model: client(req.model),
      system: req.system,
      messages: historyToModelMessages(req.history),
      tools,
      maxOutputTokens: req.maxTokens,
    });

    const content: ContentBlock[] = [];
    let textBuf = '';
    for await (const part of result.fullStream) {
      if (part.type === 'text-delta' || part.type === 'reasoning-delta') {
        // Treat reasoning-delta the same as text-delta — Fortinet
        // relay endpoints (forti-k2, forti-coder, etc.) return their
        // entire response as reasoning blocks instead of regular text.
        // Without this branch the model output silently disappears
        // (assistant message saved with blocks=[]).
        textBuf += part.text;
        cb.onTextDelta(part.text);
      } else if (part.type === 'tool-call') {
        if (textBuf.length > 0) {
          content.push({ type: 'text', text: textBuf });
          textBuf = '';
        }
        const decoded = decodeToolName(part.toolName);
        cb.onToolUse({ id: part.toolCallId, name: decoded, input: part.input });
        content.push({ type: 'tool_use', id: part.toolCallId, name: decoded, input: part.input });
      } else if (part.type === 'error') {
        throw new Error(`Anthropic stream error: ${String((part as any).error)}`);
      }
    }
    if (textBuf.length > 0) content.push({ type: 'text', text: textBuf });

    const finishReason = await result.finishReason;
    let usage;
    try {
      const u: any = await result.usage;
      if (u) {
        usage = {
          inputTokens: u.inputTokens ?? u.promptTokens,
          outputTokens: u.outputTokens ?? u.completionTokens,
          cacheReadTokens: u.cachedInputTokens ?? u.cacheReadInputTokens,
          cacheCreationTokens: u.cacheCreationInputTokens,
          totalTokens: u.totalTokens,
        };
      }
    } catch {}
    return { stopReason: mapStop(finishReason), content, usage };
  },
};
