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

/**
 * Bedrock Stream Service
 *
 * AWS Bedrock integration service for streaming AI model invocations.
 * Uses InvokeModelWithResponseStreamCommand for token-by-token streaming.
 *
 * Supports multiple streaming formats:
 * - OpenAI-compatible format (choices[].delta.content)
 * - Anthropic Messages API format (content_block_delta)
 *
 * @module backend-services/bedrock-stream.service
 */

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

/**
 * Parameters for streaming invocation.
 */
export interface StreamInvokeParams {
    /** The current user message to process */
    message: string;
    /** Previous conversation history for context */
    history: ConversationMessage[];
    /** Temperature for response generation (0.0-1.0) */
    temperature?: number;
}

/**
 * Anthropic streaming response chunk format.
 */
interface AnthropicStreamChunk {
    type: string;
    index?: number;
    delta?: {
        type: string;
        text?: string;
    };
    content_block?: {
        type: string;
        text: string;
    };
}

/**
 * OpenAI-compatible streaming chunk format.
 */
interface OpenAIStreamChunk {
    choices?: Array<{
        delta?: {
            content?: string;
        };
        finish_reason?: string;
    }>;
}

/**
 * Combined stream chunk type supporting multiple formats.
 */
type StreamChunk = AnthropicStreamChunk & OpenAIStreamChunk;

/**
 * Service for streaming responses from AWS Bedrock AI models.
 *
 * Provides async generator for token-by-token streaming,
 * enabling real-time response rendering in the UI.
 *
 * @example
 * ```typescript
 * const bedrockStream = new BedrockStreamService();
 * for await (const token of bedrockStream.invokeStream({
 *   message: 'Tell me a story',
 *   history: [],
 *   temperature: 0.9,
 * })) {
 *   process.stdout.write(token);
 * }
 * ```
 */
export class BedrockStreamService {
    private client: BedrockRuntimeClient;
    private modelId: string;

    /**
     * Creates a new BedrockStreamService instance.
     */
    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 Bedrock with streaming response.
     *
     * Returns an async generator that yields token chunks as they arrive.
     *
     * @param params - The streaming invocation parameters
     * @yields Token strings as they are generated
     * @throws Error if the Bedrock API call fails
     */
    async *invokeStream(params: StreamInvokeParams): AsyncGenerator<string> {
        const { message, history, temperature = 0.5 } = params;

        // Format messages for chat completion
        const messages = [
            ...history.map((msg) => ({
                role: msg.role,
                content: msg.content,
            })),
            {
                role: 'user',
                content: message,
            },
        ];

        const requestBody = {
            anthropic_version: 'bedrock-2023-05-31',
            max_tokens: 4096,
            temperature,
            system: 'You are a helpful AI assistant. Answer questions accurately and concisely.',
            messages,
        };

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

        try {
            console.log(JSON.stringify({
                service: 'BedrockStreamService',
                status: 'sending_command',
                modelId: this.modelId,
                messagesCount: messages.length,
            }));

            const response = await this.client.send(command);

            console.log(JSON.stringify({
                service: 'BedrockStreamService',
                status: 'response_received',
                hasBody: !!response.body,
            }));

            if (response.body) {
                let eventCount = 0;
                let yieldCount = 0;

                for await (const event of response.body) {
                    eventCount++;

                    if (event.chunk?.bytes) {
                        const chunkText = new TextDecoder().decode(event.chunk.bytes);
                        const chunk = JSON.parse(chunkText) as StreamChunk;

                        // Log first few events for debugging
                        if (eventCount <= 5) {
                            console.log(JSON.stringify({
                                service: 'BedrockStreamService',
                                eventCount,
                                chunkType: chunk.type,
                                hasDelta: !!chunk.delta,
                                deltaType: chunk.delta?.type,
                                hasChoices: !!chunk.choices,
                                chunkPreview: chunkText.substring(0, 200),
                            }));
                        }

                        // Handle Anthropic format: content_block_delta events with text
                        if (chunk.type === 'content_block_delta' && chunk.delta?.text) {
                            yieldCount++;
                            yield chunk.delta.text;
                        }
                        // Handle OpenAI-compatible format: choices[].delta.content
                        else if (chunk.choices?.[0]?.delta?.content) {
                            yieldCount++;
                            yield chunk.choices[0].delta.content;
                        }
                    }
                }

                console.log(JSON.stringify({
                    service: 'BedrockStreamService',
                    status: 'stream_complete',
                    totalEvents: eventCount,
                    totalYields: yieldCount,
                }));
            } else {
                console.log(JSON.stringify({
                    service: 'BedrockStreamService',
                    status: 'no_response_body',
                }));
            }
        } catch (error) {
            console.error(JSON.stringify({
                service: 'BedrockStreamService',
                status: 'error',
                errorName: error instanceof Error ? error.name : 'Unknown',
                errorMessage: error instanceof Error ? error.message : 'Unknown',
            }));
            const errorMessage = this.formatError(error);
            throw new Error(`Bedrock streaming failed: ${errorMessage}`);
        }
    }

    /**
     * Formats an error into a meaningful error message.
     *
     * @param error - The error to format
     * @returns A formatted error message
     */
    private formatError(error: unknown): string {
        if (error instanceof Error) {
            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.';
                default:
                    return error.message;
            }
        }

        return 'Unknown error occurred';
    }
}
