/**
 * TITAN — Anthropic/Claude Provider
 */
import {
    LLMProvider,
    withTimeoutSignal,
    STREAM_FETCH_TIMEOUT_MS,
    type ChatOptions,
    type ChatResponse,
    type ChatStreamChunk,
    type ToolCall,
} from './base.js';
import { loadConfig } from '../config/config.js';
import logger from '../utils/logger.js';
import { fetchWithRetry } from '../utils/helpers.js';
import { resolveApiKey } from './authResolver.js';
import { v4 as uuid } from 'uuid';
import { clampMaxTokens } from './modelCapabilities.js';

const COMPONENT = 'Anthropic';

export class AnthropicProvider extends LLMProvider {
    readonly name = 'anthropic';
    readonly displayName = 'Anthropic (Claude)';

    private get apiKey(): string {
        const config = loadConfig();
        const p = config.providers.anthropic;
        if (p.enabled === false) return '';
        return resolveApiKey('anthropic', p.authProfiles || [], p.apiKey || '', 'ANTHROPIC_API_KEY', p.rotationStrategy, p.credentialCooldownMs);
    }

    private get baseUrl(): string {
        const config = loadConfig();
        return config.providers.anthropic.baseUrl || 'https://api.anthropic.com';
    }

    async chat(options: ChatOptions): Promise<ChatResponse> {
        const model = options.model || 'claude-sonnet-4-20250514';
        const apiKey = this.apiKey;
        if (!apiKey) throw new Error('Anthropic API key not configured');

        logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);

        const systemMessage = options.messages.find((m) => m.role === 'system');
        const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');

        const body: Record<string, unknown> = {
            model: model.replace('anthropic/', ''),
            max_tokens: clampMaxTokens(model, options.maxTokens),
            messages: nonSystemMessages.map((m) => ({
                role: m.role === 'tool' ? 'user' : m.role,
                content: m.role === 'tool'
                    ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]
                    : m.content,
            })),
        };

        if (systemMessage) {
            // TITAN pattern: prompt cache splitting
            // Place cache_control breakpoint on system prompt to cache it across turns.
            // This reduces input costs by ~75% for subsequent messages in the same session.
            body.system = [
                {
                    type: 'text',
                    text: systemMessage.content,
                    cache_control: { type: 'ephemeral' },
                },
            ];
        }

        if (options.tools && options.tools.length > 0) {
            body.tools = options.tools.map((t) => ({
                name: t.function.name,
                description: t.function.description,
                input_schema: t.function.parameters,
            }));
            // Force at least one tool call on first round when task requires it.
            // Cannot combine tool_choice:any with extended thinking — skip if thinking enabled.
            if (options.forceToolUse && !options.thinking) {
                body.tool_choice = { type: 'any' };
            }
        }

        if (options.temperature !== undefined) {
            body.temperature = options.temperature;
        }

        // Extended thinking support
        if (options.thinking) {
            const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };
            const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;
            body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
        }

        const response = await fetchWithRetry(`${this.baseUrl}/v1/messages`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'x-api-key': apiKey,
                'anthropic-version': '2023-06-01',
                'anthropic-beta': 'prompt-caching-2024-07-31',
            },
            body: JSON.stringify(body),
        });

        if (!response.ok) {
            const errorText = await response.text();
            // Hunt Finding #37: attach status + Retry-After so the router can respect backoff
            const { createProviderError } = await import('./errorTaxonomy.js');
            throw createProviderError('Anthropic API', response, errorText, { provider: 'anthropic', model });
        }

        const data = await response.json() as Record<string, unknown>;
        const content = data.content as Array<Record<string, unknown>> | undefined;

        let textContent = '';
        const toolCalls: ToolCall[] = [];

        if (!content || !Array.isArray(content)) {
            return {
                id: (data.id as string) || uuid(),
                content: '',
                usage: undefined,
                finishReason: 'stop',
                model,
            };
        }

        for (const block of content) {
            if (block.type === 'text') {
                textContent += block.text as string;
            } else if (block.type === 'tool_use') {
                toolCalls.push({
                    id: block.id as string,
                    type: 'function',
                    function: {
                        name: block.name as string,
                        arguments: JSON.stringify(block.input),
                    },
                });
            }
        }

        const usage = data.usage as { input_tokens: number; output_tokens: number } | undefined;

        return {
            id: (data.id as string) || uuid(),
            content: textContent,
            toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
            usage: usage
                ? {
                    promptTokens: usage.input_tokens,
                    completionTokens: usage.output_tokens,
                    totalTokens: usage.input_tokens + usage.output_tokens,
                }
                : undefined,
            finishReason: (() => {
                const sr = data.stop_reason as string | undefined;
                if (sr === 'max_tokens') return 'length';
                if (sr === 'tool_use') return 'tool_calls';
                return toolCalls.length > 0 ? 'tool_calls' : 'stop';
            })(),
            model,
        };
    }

    async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {
        const model = options.model || 'claude-sonnet-4-20250514';
        const apiKey = this.apiKey;
        if (!apiKey) { yield { type: 'error', error: 'Anthropic API key not configured' }; return; }

        const systemMessage = options.messages.find((m) => m.role === 'system');
        const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');

        const body: Record<string, unknown> = {
            model: model.replace('anthropic/', ''),
            max_tokens: clampMaxTokens(model, options.maxTokens),
            stream: true,
            messages: nonSystemMessages.map((m) => ({
                role: m.role === 'tool' ? 'user' : m.role,
                content: m.role === 'tool'
                    ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]
                    : m.content,
            })),
        };

        if (systemMessage) {
            body.system = [{ type: 'text', text: systemMessage.content, cache_control: { type: 'ephemeral' } }];
        }
        if (options.tools && options.tools.length > 0) {
            body.tools = options.tools.map((t) => ({
                name: t.function.name,
                description: t.function.description,
                input_schema: t.function.parameters,
            }));
        }
        if (options.temperature !== undefined) body.temperature = options.temperature;

        // Extended thinking support
        if (options.thinking) {
            const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };
            const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;
            body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
        }

        try {
            const response = await fetch(`${this.baseUrl}/v1/messages`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'x-api-key': apiKey,
                    'anthropic-version': '2023-06-01',
                    'anthropic-beta': 'prompt-caching-2024-07-31',
                },
                body: JSON.stringify(body),
                signal: withTimeoutSignal(options.signal, STREAM_FETCH_TIMEOUT_MS),
            });

            if (!response.ok || !response.body) {
                const errorText = await response.text();
                // v6.5 — THROW (not yield) so the router routes HTTP errors through
                // retry / fallback chain / circuit breaker, like non-streaming chat().
                const { createProviderError } = await import('./errorTaxonomy.js');
                throw createProviderError('Anthropic API', response, errorText, { provider: 'anthropic', model });
            }

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            let currentToolId = '';
            let currentToolName = '';
            let toolArgsBuffer = '';

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                buffer += decoder.decode(value, { stream: true });

                const lines = buffer.split('\n');
                buffer = lines.pop() || '';

                for (const line of lines) {
                    if (!line.startsWith('data: ')) continue;
                    const json = line.slice(6).trim();
                    if (json === '[DONE]' || !json) continue;

                    try {
                        const event = JSON.parse(json);
                        if (event.type === 'content_block_delta') {
                            const delta = event.delta;
                            if (delta.type === 'text_delta' && delta.text) {
                                yield { type: 'text', content: delta.text };
                            } else if (delta.type === 'input_json_delta' && delta.partial_json) {
                                toolArgsBuffer += delta.partial_json;
                            }
                        } else if (event.type === 'content_block_start') {
                            const block = event.content_block;
                            if (block?.type === 'tool_use') {
                                currentToolId = block.id;
                                currentToolName = block.name;
                                toolArgsBuffer = '';
                            }
                        } else if (event.type === 'content_block_stop') {
                            if (currentToolId) {
                                yield {
                                    type: 'tool_call',
                                    toolCall: {
                                        id: currentToolId,
                                        type: 'function',
                                        function: { name: currentToolName, arguments: toolArgsBuffer || '{}' },
                                    },
                                };
                                currentToolId = '';
                                toolArgsBuffer = '';
                            }
                        }
                    } catch { /* skip malformed SSE lines */ }
                }
            }
            yield { type: 'done' };
        } catch (error) {
            yield { type: 'error', error: (error as Error).message };
        }
    }

    async listModels(): Promise<string[]> {
        // Hardcoded comprehensive fallback used when no key is configured
        // or the live discovery call fails. Covers every Claude API model
        // ID currently in production as of 2026-05. Keeps the picker
        // useful even on a fresh-clone TITAN with no key — the user can
        // browse and choose, then configure the key. Live discovery (when
        // a key IS configured) will replace this with whatever the upstream
        // /v1/models returns, which may include models the key doesn't
        // have access to.
        const FALLBACK = [
            // Claude 4.x family (2025-2026)
            'claude-opus-4-7',
            'claude-opus-4-6',
            'claude-opus-4-5',
            'claude-opus-4-0',
            'claude-sonnet-4-6',
            'claude-sonnet-4-5',
            'claude-sonnet-4-20250514',
            'claude-haiku-4-6',
            'claude-haiku-4-5-20251001',
            'claude-haiku-4-20250414',
            // Claude 3.7 / 3.5 / 3.0 (still production-supported)
            'claude-3-7-sonnet-20250219',
            'claude-3-5-sonnet-20241022',
            'claude-3-5-sonnet-20240620',
            'claude-3-5-haiku-20241022',
            'claude-3-opus-20240229',
            'claude-3-sonnet-20240229',
            'claude-3-haiku-20240307',
        ];
        if (!this.apiKey) return FALLBACK;

        // Live discovery — Anthropic's /v1/models is paginated (page size 1000)
        // and returns objects shaped like `{ id, type: 'model', display_name, created_at }`.
        try {
            const response = await fetch(`${this.baseUrl}/v1/models?limit=1000`, {
                headers: {
                    'x-api-key': this.apiKey,
                    'anthropic-version': '2023-06-01',
                },
                signal: AbortSignal.timeout(5000),
            });
            if (!response.ok) {
                logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);
                return FALLBACK;
            }
            const data = await response.json() as { data?: Array<{ id: string }> };
            const ids = (data.data || []).map((m) => m.id).filter(Boolean);
            return ids.length > 0 ? ids : FALLBACK;
        } catch (err) {
            logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);
            return FALLBACK;
        }
    }

    isConfigured(): boolean {
        return !!this.apiKey;
    }

    async healthCheck(): Promise<boolean> {
        try {
            if (!this.apiKey) return false;
            const response = await fetch(`${this.baseUrl}/v1/messages`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'x-api-key': this.apiKey,
                    'anthropic-version': '2023-06-01',
                },
                body: JSON.stringify({
                    model: 'claude-haiku-4-20250414',
                    max_tokens: 1,
                    messages: [{ role: 'user', content: 'ping' }],
                }),
            });
            return response.ok || response.status === 400; // 400 = valid auth but bad request
        } catch {
            return false;
        }
    }
}
