/*
 * Runtime Web – Unified Serverless Framework for Full-Stack TypeScript Applications
 * Documentation: https://runtimeweb.com/docs/getting-started
 *
 * @template: ai
 */

/**
 * Bedrock Service
 *
 * AWS Bedrock integration service 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 Messages API request format.
 */
interface ClaudeMessage {
    role: 'user' | 'assistant';
    content: string;
}

/**
 * Claude Messages API response format.
 */
interface ClaudeResponse {
    id: string;
    type: 'message';
    role: 'assistant';
    content: Array<{
        type: 'text';
        text: string;
    }>;
    model: string;
    stop_reason: string;
    stop_sequence: string | null;
    usage: {
        input_tokens: number;
        output_tokens: number;
    };
}

/**
 * Service for interacting with AWS Bedrock AI models.
 *
 * Provides methods to invoke Claude models with conversation history
 * and relevant context from vector search (RAG pattern).
 *
 * @example
 * ```typescript
 * const bedrock = new BedrockService();
 * const response = await bedrock.invoke({
 *   message: 'What is the capital of France?',
 *   history: [],
 *   relevant: [],
 * });
 * ```
 */
export class BedrockService {
    private client: BedrockRuntimeClient;
    private modelId: string;

    /**
     * Creates a new BedrockService instance.
     *
     * Initializes the Bedrock client and configures the model ID
     * from environment variables or uses the default Claude model.
     */
    constructor() {
        this.client = new BedrockRuntimeClient({
          region: process.env.AWS_REGION || 'us-east-1',
        });
        this.modelId =
            process.env.BEDROCK_MODEL_ID || 'openai.gpt-oss-120b-1:0';
    }

    /**
     * Invokes the Bedrock model with the given parameters.
     *
     * Formats the prompt with conversation history and relevant context,
     * calls the Bedrock InvokeModel API, and parses the response.
     *
     * @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: '2024-01-01T00:00:00Z' },
     *     { role: 'assistant', content: 'Hello!', timestamp: '2024-01-01T00:00:01Z' },
     *   ],
     *   relevant: [
     *     { id: '1', content: 'Relevant info', score: 0.95 },
     *   ],
     * });
     * ```
     */
    async invoke(params: InvokeParams): Promise<string> {
        const { systemPrompt, messages } = this.formatPrompt(params);

        const requestBody = {
            anthropic_version: 'bedrock-2023-05-31',
            max_tokens: 4096,
            system: systemPrompt,
            messages,
        };

        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) {
            const errorMessage = this.formatError(error);
            throw new Error(`Bedrock invocation failed: ${errorMessage}`);
        }
    }

    /**
     * Formats the prompt for the Claude model.
     *
     * Builds a system prompt with relevant context and converts
     * conversation history to Claude's message format.
     *
     * @param params - The invocation parameters
     * @returns The formatted system prompt and messages array
     */
    private formatPrompt(params: InvokeParams): {
        systemPrompt: string;
        messages: ClaudeMessage[];
    } {
        const { message, history, relevant } = params;

        // Build system prompt with relevant context
        let systemPrompt =
            'You are a helpful AI assistant. Answer questions accurately and concisely.';

        if (relevant.length > 0) {
            const contextSection = relevant
                .map((doc, index) => `[${index + 1}] ${doc.content}`)
                .join('\n\n');

            systemPrompt += `\n\nUse the following relevant context to help answer the user's question:\n\n${contextSection}\n\nIf the context is relevant, incorporate it into your response. If not, answer based on your general knowledge.`;
        }

        // Convert history to Claude message format
        const messages: ClaudeMessage[] = history.map((msg) => ({
            role: msg.role,
            content: msg.content,
        }));

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

        return { systemPrompt, messages };
    }

    /**
     * Parses the response from the Bedrock API.
     *
     * Extracts the text content from Claude's response format.
     *
     * @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('Empty response body from Bedrock');
        }

        const responseText = new TextDecoder().decode(body);
        const response: ClaudeResponse = JSON.parse(responseText);

        if (!response.content || response.content.length === 0) {
            throw new Error('No content in Bedrock response');
        }

        const textContent = response.content.find((c) => c.type === 'text');
        if (!textContent) {
            throw new Error('No text content in Bedrock response');
        }

        return textContent.text;
    }

    /**
     * Formats an error into a meaningful error message.
     *
     * Handles various error types from the Bedrock API and
     * provides user-friendly error messages.
     *
     * @param error - The error to format
     * @returns A formatted error message
     */
    private formatError(error: unknown): string {
        if (error instanceof Error) {
            // Handle specific Bedrock error types
            const errorName = error.name;

            switch (errorName) {
                case 'AccessDeniedException':
                    return 'Access denied to Bedrock model. Check IAM permissions.';
                case 'ResourceNotFoundException':
                    return `Model not found: ${this.modelId}. Verify the model ID is correct.`;
                case 'ThrottlingException':
                    return 'Request throttled. Please retry after a short delay.';
                case 'ModelTimeoutException':
                    return 'Model invocation timed out. Try a shorter prompt.';
                case 'ModelStreamErrorException':
                    return 'Error streaming model response.';
                case 'ValidationException':
                    return `Invalid request: ${error.message}`;
                case 'ServiceQuotaExceededException':
                    return 'Service quota exceeded. Request a quota increase.';
                default:
                    return error.message;
            }
        }

        return 'Unknown error occurred';
    }
}
