/**
 * Bedrock Service
 *
 * AWS Bedrock service wrapper for AI model invocations.
 * Handles prompt formatting, API calls, and response parsing
 * for the Claude model family.
 *
 * @module services/bedrock.service
 */

import {
    BedrockRuntimeClient,
    InvokeModelCommand,
    type InvokeModelCommandInput,
} from '@aws-sdk/client-bedrock-runtime';
import type { ConversationMessage, RelevantDocument } from '../types/chat.types';

/**
 * Parameters for invoking the Bedrock model.
 */
export interface InvokeParams {
    /** The current user message to process */
    message: string;
    /** Previous conversation history for context */
    history: ConversationMessage[];
    /** Relevant documents from vector search for RAG */
    relevant: RelevantDocument[];
}

/**
 * Claude model request body structure.
 * Follows the Anthropic Messages API format.
 */
interface ClaudeRequestBody {
    anthropic_version: string;
    max_tokens: number;
    system?: string;
    messages: Array<{
        role: 'user' | 'assistant';
        content: string;
    }>;
}

/**
 * Claude model response body structure.
 */
interface ClaudeResponseBody {
    id: string;
    type: string;
    role: string;
    content: Array<{
        type: string;
        text: string;
    }>;
    model: string;
    stop_reason: string;
    stop_sequence: string | null;
    usage: {
        input_tokens: number;
        output_tokens: number;
    };
}

/**
 * Default model ID for Claude 3 Sonnet.
 */
const DEFAULT_MODEL_ID = 'anthropic.claude-3-sonnet-20240229-v1:0';

/**
 * Default maximum tokens for response generation.
 */
const DEFAULT_MAX_TOKENS = 4096;

/**
 * Anthropic API version for Claude models.
 */
const ANTHROPIC_VERSION = 'bedrock-2023-05-31';

/**
 * BedrockService provides AI model invocation capabilities
 * using AWS Bedrock Runtime with Claude models.
 *
 * Features:
 * - Configurable model ID via environment variable
 * - Conversation history support for multi-turn chat
 * - RAG context injection from vector search results
 * - Proper error handling with meaningful messages
 *
 * @example
 * ```typescript
 * const bedrock = new BedrockService();
 * const response = await bedrock.invoke({
 *   message: 'What is the capital of France?',
 *   history: [],
 *   relevant: [],
 * });
 * ```
 */
export class BedrockService {
    private readonly client: BedrockRuntimeClient;
    private readonly modelId: string;
    private readonly maxTokens: number;

    /**
     * Creates a new BedrockService instance.
     * Initializes the Bedrock Runtime client and reads configuration
     * from environment variables.
     */
    constructor() {
        this.client = new BedrockRuntimeClient({});
        this.modelId = process.env.BEDROCK_MODEL_ID || DEFAULT_MODEL_ID;
        this.maxTokens = parseInt(process.env.BEDROCK_MAX_TOKENS || String(DEFAULT_MAX_TOKENS), 10);
    }

    /**
     * Invokes the Bedrock model with the given parameters.
     *
     * Orchestrates the full invocation flow:
     * 1. Formats the prompt with history and relevant context
     * 2. Calls the Bedrock InvokeModel API
     * 3. Parses and returns the response text
     *
     * @param params - The invocation parameters
     * @returns The AI-generated response text
     * @throws Error if the Bedrock API call fails or response parsing fails
     *
     * @example
     * ```typescript
     * const response = await bedrock.invoke({
     *   message: 'Hello!',
     *   history: [
     *     { role: 'user', content: 'Hi', timestamp: '...' },
     *     { role: 'assistant', content: 'Hello!', timestamp: '...' },
     *   ],
     *   relevant: [
     *     { id: '1', content: 'Relevant info...', score: 0.95 },
     *   ],
     * });
     * ```
     */
    async invoke(params: InvokeParams): Promise<string> {
        const requestBody = this.buildRequestBody(params);

        const input: InvokeModelCommandInput = {
            modelId: this.modelId,
            contentType: 'application/json',
            accept: 'application/json',
            body: JSON.stringify(requestBody),
        };

        try {
            const command = new InvokeModelCommand(input);
            const response = await this.client.send(command);

            return this.parseResponse(response.body);
        } catch (error) {
            throw this.handleError(error);
        }
    }

    /**
     * Builds the Claude request body with formatted messages.
     *
     * @param params - The invocation parameters
     * @returns The formatted request body for Claude
     */
    private buildRequestBody(params: InvokeParams): ClaudeRequestBody {
        const systemPrompt = this.buildSystemPrompt(params.relevant);
        const messages = this.buildMessages(params.history, params.message);

        const requestBody: ClaudeRequestBody = {
            anthropic_version: ANTHROPIC_VERSION,
            max_tokens: this.maxTokens,
            messages,
        };

        if (systemPrompt) {
            requestBody.system = systemPrompt;
        }

        return requestBody;
    }

    /**
     * Builds the system prompt with relevant context from RAG.
     *
     * @param relevant - Relevant documents from vector search
     * @returns The system prompt string, or undefined if no context
     */
    private buildSystemPrompt(relevant: RelevantDocument[]): string | undefined {
        if (relevant.length === 0) {
            return undefined;
        }

        const contextParts = relevant.map((doc, index) => {
            return `[Document ${index + 1}]\n${doc.content}`;
        });

        return `You are a helpful AI assistant. Use the following context to answer questions accurately. If the context doesn't contain relevant information, say so and provide a general response.

Context:
${contextParts.join('\n\n')}`;
    }

    /**
     * Builds the messages array for the Claude API.
     * Converts conversation history and current message to Claude format.
     *
     * @param history - Previous conversation messages
     * @param currentMessage - The current user message
     * @returns Array of messages in Claude format
     */
    private buildMessages(
        history: ConversationMessage[],
        currentMessage: string
    ): Array<{ role: 'user' | 'assistant'; content: string }> {
        const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [];

        // Add conversation history
        for (const msg of history) {
            messages.push({
                role: msg.role,
                content: msg.content,
            });
        }

        // Add current user message
        messages.push({
            role: 'user',
            content: currentMessage,
        });

        return messages;
    }

    /**
     * Formats the prompt for Claude model compatibility.
     * This method is exposed for testing and debugging purposes.
     *
     * @param params - The invocation parameters
     * @returns The formatted prompt string representation
     */
    formatPrompt(params: InvokeParams): string {
        const requestBody = this.buildRequestBody(params);
        return JSON.stringify(requestBody, null, 2);
    }

    /**
     * Parses the Bedrock response body and extracts the text content.
     *
     * @param body - The raw response body from Bedrock
     * @returns The extracted text response
     * @throws Error if the response format is invalid
     */
    private parseResponse(body: Uint8Array | undefined): string {
        if (!body) {
            throw new Error('Bedrock response body is empty');
        }

        const responseText = new TextDecoder().decode(body);
        let responseBody: ClaudeResponseBody;

        try {
            responseBody = JSON.parse(responseText) as ClaudeResponseBody;
        } catch {
            throw new Error(`Failed to parse Bedrock response: ${responseText.substring(0, 200)}`);
        }

        if (!responseBody.content || responseBody.content.length === 0) {
            throw new Error('Bedrock response contains no content');
        }

        const textContent = responseBody.content.find((c) => c.type === 'text');
        if (!textContent || !textContent.text) {
            throw new Error('Bedrock response contains no text content');
        }

        return textContent.text;
    }

    /**
     * Handles errors from Bedrock API calls and returns meaningful error messages.
     *
     * @param error - The caught error
     * @returns A new Error with a descriptive message
     */
    private handleError(error: unknown): Error {
        if (error instanceof Error) {
            const errorName = error.name;
            const errorMessage = error.message;

            // Handle specific Bedrock errors
            if (errorName === 'ValidationException') {
                return new Error(`Bedrock validation error: ${errorMessage}`);
            }

            if (errorName === 'ModelTimeoutException') {
                return new Error('Bedrock model timed out. Please try again.');
            }

            if (errorName === 'ModelNotReadyException') {
                return new Error('Bedrock model is not ready. Please try again later.');
            }

            if (errorName === 'ThrottlingException') {
                return new Error('Bedrock request was throttled. Please try again later.');
            }

            if (errorName === 'ServiceQuotaExceededException') {
                return new Error('Bedrock service quota exceeded. Please try again later.');
            }

            if (errorName === 'AccessDeniedException') {
                return new Error('Access denied to Bedrock model. Check IAM permissions.');
            }

            if (errorName === 'ResourceNotFoundException') {
                return new Error(`Bedrock model not found: ${this.modelId}`);
            }

            // Generic error with original message
            return new Error(`Bedrock invocation failed: ${errorMessage}`);
        }

        return new Error('An unexpected error occurred while invoking Bedrock');
    }
}
