<?php

declare(strict_types=1);

namespace App\Services\AiAssistant;

use App\Models\AiConversation;
use App\Models\AiMessage;
use App\Models\AiProvider;
use App\Models\User;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Prism\Prism\Enums\StreamEventType;
use Prism\Prism\Facades\Prism;
use Prism\Prism\ValueObjects\Messages\UserMessage;
use Symfony\Component\HttpFoundation\StreamedResponse;

class AiAssistantService
{
    public function __construct(
        protected Context\ConversationContext $conversationContext,
        protected Context\TenantContext $tenantContext,
        protected ToolManager $toolManager,
        protected Prompts\SystemPrompt $systemPrompt,
    ) {}

    public function chat(
        string $message,
        ?string $conversationUlid = null,
        ?User $user = null
    ): array {
        $user ??= auth()->user();

        $conversation = $this->getOrCreateConversation($conversationUlid, $user);
        $userMessage = $this->saveUserMessage($conversation, $message);

        $context = $this->conversationContext->build($conversation);
        $systemPrompt = $this->systemPrompt->build();

        // Get provider from database, fallback to config
        $providerConfig = $this->getProviderConfig();
        $provider = $providerConfig['code'];
        $model = $providerConfig['model'];
        $maxSteps = $providerConfig['max_steps'];
        $isGemini = $provider === 'gemini';

        // Set context for tools (conversation ID and user ID)
        $this->toolManager->setContext($conversation->id, $user?->id);

        $messages = array_merge(
            $context['messages'],
            [new UserMessage($message)],
        );

        $request = Prism::text()
            ->using($provider, $model)
            ->withMessages($messages)
            ->withTools($this->toolManager->getAvailableTools())
            ->withMaxSteps($maxSteps);

        // Add system prompt differently for Gemini vs other providers
        if ($isGemini) {
            $request = $request->withProviderOptions([
                'systemInstruction' => [
                    'parts' => [['text' => $systemPrompt]],
                    'role' => 'system',
                ],
            ]);
        } else {
            // For non-Gemini, prepend system message
            array_unshift($messages, new \Prism\Prism\ValueObjects\Messages\SystemMessage($systemPrompt));
            $request = Prism::text()
                ->using($provider, $model)
                ->withMessages($messages)
                ->withTools($this->toolManager->getAvailableTools())
                ->withMaxSteps($maxSteps);
        }

        try {
            $response = $request->asText();

            return $this->processResponse($response, $conversation, $userMessage);
        } catch (\Throwable $e) {
            Log::error('AI chat error', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);

            return $this->handleError($e, $conversation);
        }
    }

    public function stream(
        string $message,
        ?string $conversationUlid = null,
        ?User $user = null
    ): StreamedResponse {
        $user ??= auth()->user();

        $conversation = $this->getOrCreateConversation($conversationUlid, $user);
        $userMessage = $this->saveUserMessage($conversation, $message);

        $context = $this->conversationContext->build($conversation);
        $systemPrompt = $this->systemPrompt->build();

        // Get provider from database, fallback to config
        $providerConfig = $this->getProviderConfig();
        $provider = $providerConfig['code'];
        $model = $providerConfig['model'];
        $maxSteps = $providerConfig['max_steps'];
        $isGemini = $provider === 'gemini';

        // Set context for tools (conversation ID and user ID)
        $this->toolManager->setContext($conversation->id, $user?->id);

        $toolManager = $this->toolManager;
        $service = $this;

        return new StreamedResponse(
            function () use ($context, $systemPrompt, $provider, $model, $maxSteps, $conversation, $userMessage, $toolManager, $service, $isGemini, $message) {
                $fullContent = '';
                $toolCalls = [];
                $toolCallsWithHtml = [];

                try {
                    $availableTools = $toolManager->getAvailableTools();

                    Log::info('Starting AI stream', [
                        'provider' => $provider,
                        'model' => $model,
                        'is_gemini' => $isGemini,
                        'tools_count' => count($availableTools),
                    ]);

                    // Build messages array
                    $messages = array_merge(
                        $context['messages'],
                        [new UserMessage($message)],
                    );

                    // Build request
                    $request = Prism::text()
                        ->using($provider, $model)
                        ->withMessages($messages)
                        ->withTools($availableTools)
                        ->withMaxSteps($maxSteps);

                    // Add system prompt differently for Gemini vs other providers
                    if ($isGemini) {
                        $request = $request->withProviderOptions([
                            'systemInstruction' => [
                                'parts' => [['text' => $systemPrompt]],
                                'role' => 'system',
                            ],
                        ]);
                    } else {
                        // For non-Gemini, prepend system message
                        array_unshift($messages, new \Prism\Prism\ValueObjects\Messages\SystemMessage($systemPrompt));
                        $request = Prism::text()
                            ->using($provider, $model)
                            ->withMessages($messages)
                            ->withTools($availableTools)
                            ->withMaxSteps($maxSteps);
                    }

                    $stream = $request->asStream();

                    foreach ($stream as $event) {
                        Log::debug('AI stream event', ['type' => $event->type()]);
                        // Handle text delta events (content chunks)
                        if ($event->type() === StreamEventType::TextDelta) {
                            $fullContent .= $event->delta;
                            echo 'data: '.json_encode(['type' => 'content', 'content' => $event->delta])."\n\n";
                            if (ob_get_level() > 0) {
                                ob_flush();
                            }
                            flush();
                        }

                        // Handle tool call events
                        if ($event->type() === StreamEventType::ToolCall) {
                            $arguments = $event->toolCall->arguments();
                            $toolCalls[] = [
                                'id' => $event->toolCall->id,
                                'name' => $event->toolCall->name,
                                'arguments' => $arguments,
                            ];
                            echo 'data: '.json_encode([
                                'type' => 'tool_call',
                                'tool' => $event->toolCall->name,
                                'arguments' => $arguments,
                            ])."\n\n";
                            if (ob_get_level() > 0) {
                                ob_flush();
                            }
                            flush();
                        }

                        // Handle tool result events
                        if ($event->type() === StreamEventType::ToolResult) {
                            $result = $event->toolResult->result;
                            $toolName = $event->toolResult->toolName;
                            $toolArgs = $event->toolResult->args;

                            // Log for debugging
                            Log::debug('Tool result arguments', [
                                'tool' => $toolName,
                                'args' => $toolArgs,
                                'args_type' => gettype($toolArgs),
                            ]);

                            // Check if there's stored HTML for this tool call
                            $argsKey = \App\Services\AiAssistant\ToolManager::generateArgumentsKey($toolArgs);
                            $storedResult = $toolManager->getStoredResult($toolName, $argsKey);

                            // If not found, try to find by iterating through all stored results for this tool
                            if (! $storedResult) {
                                $allStored = $toolManager->getAllStoredForTool($toolName);
                                Log::debug('Tool result check fallback', [
                                    'tool' => $toolName,
                                    'args_key' => $argsKey,
                                    'has_stored' => $allStored !== null,
                                    'stored_count' => $allStored ? count($allStored) : 0,
                                ]);

                                // Try to find the first matching result (fallback)
                                if ($allStored && count($allStored) > 0) {
                                    $fallbackKey = array_key_first($allStored);
                                    $storedResult = $allStored[$fallbackKey];
                                    // Remove this specific result to prevent duplicate display
                                    $toolManager->getAndRemoveStoredResult($toolName, str_replace($toolName.':', '', $fallbackKey));
                                    Log::debug('Using fallback stored result', [
                                        'tool' => $toolName,
                                        'fallback_key' => $fallbackKey,
                                        'found' => $storedResult !== null,
                                    ]);
                                }
                            }

                            if ($storedResult && isset($storedResult['html'])) {
                                // Send just the text result to the AI (so it doesn't see the HTML JSON)
                                echo 'data: '.json_encode([
                                    'type' => 'tool_result',
                                    'tool' => $toolName,
                                    'result' => $result,
                                ])."\n\n";

                                // Track HTML for later display at end of message
                                $toolCallsWithHtml[] = [
                                    'tool' => $toolName,
                                    'html' => $storedResult['html'],
                                    'metadata' => $storedResult['metadata'] ?? null,
                                ];
                            } else {
                                // Regular result without HTML
                                echo 'data: '.json_encode([
                                    'type' => 'tool_result',
                                    'tool' => $toolName,
                                    'result' => $result,
                                ])."\n\n";
                            }

                            if (ob_get_level() > 0) {
                                ob_flush();
                            }
                            flush();
                        }

                        // Check for stream end
                        if ($event->type() === StreamEventType::StreamEnd) {
                            break;
                        }
                    }

                    $service->saveAssistantMessage($conversation, $fullContent, $userMessage, toolCalls: $toolCalls, visualContent: $toolCallsWithHtml);

                    // Send all HTML results at the end
                    foreach ($toolCallsWithHtml as $toolHtml) {
                        echo 'data: '.json_encode([
                            'type' => 'tool_html_display',
                            'tool' => $toolHtml['tool'],
                            'html' => $toolHtml['html'],
                            'metadata' => $toolHtml['metadata'] ?? null,
                        ])."\n\n";
                    }

                    echo 'data: '.json_encode(['type' => 'done', 'conversation_id' => $conversation->ulid])."\n\n";
                    if (ob_get_level() > 0) {
                        ob_flush();
                    }
                    flush();
                } catch (\Throwable $e) {
                    Log::error('AI stream error', [
                        'message' => $e->getMessage(),
                        'trace' => $e->getTraceAsString(),
                    ]);
                    echo 'data: '.json_encode(['type' => 'error', 'error' => $e->getMessage()])."\n\n";
                    if (ob_get_level() > 0) {
                        ob_flush();
                    }
                    flush();
                }
            },
            200,
            [
                'Content-Type' => 'text/event-stream',
                'Cache-Control' => 'no-cache',
                'X-Accel-Buffering' => 'no',
            ]
        );
    }

    public function getConversations(?User $user = null): array
    {
        $user ??= auth()->user();

        return AiConversation::query()
            ->when($user, fn ($q) => $q->where('user_id', $user->id))
            ->orderByDesc('last_message_at')
            ->get()
            ->map(fn ($c) => [
                'id' => $c->ulid,
                'title' => $c->title ?? 'New Conversation',
                'last_message_at' => $c->last_message_at?->toIso8601String(),
                'message_count' => $c->messages()->count(),
            ])
            ->toArray();
    }

    public function getConversationMessages(string $ulid): array
    {
        $conversation = AiConversation::where('ulid', $ulid)->firstOrFail();

        return $conversation->messages
            ->map(fn ($m) => [
                'id' => $m->id,
                'role' => $m->role,
                'content' => $m->content,
                'created_at' => $m->created_at->toIso8601String(),
            ])
            ->toArray();
    }

    public function deleteConversation(string $ulid): bool
    {
        $conversation = AiConversation::where('ulid', $ulid)->firstOrFail();

        return $conversation->delete();
    }

    protected function getOrCreateConversation(?string $ulid, ?User $user): AiConversation
    {
        if ($ulid) {
            return AiConversation::where('ulid', $ulid)->firstOrFail();
        }

        return AiConversation::create([
            'user_id' => $user?->id,
            'ulid' => (string) Str::ulid(),
            'title' => null,
            'last_message_at' => now(),
        ]);
    }

    protected function saveUserMessage(AiConversation $conversation, string $message): AiMessage
    {
        $msg = $conversation->messages()->create([
            'role' => 'user',
            'content' => $message,
        ]);

        $conversation->update(['last_message_at' => now()]);

        return $msg;
    }

    protected function saveAssistantMessage(
        AiConversation $conversation,
        string $content,
        AiMessage $userMessage,
        array $toolCalls = [],
        array $visualContent = []
    ): AiMessage {
        $msg = $conversation->messages()->create([
            'role' => 'assistant',
            'content' => $content,
            'tool_calls' => ! empty($toolCalls) ? $toolCalls : null,
            'visual_content' => ! empty($visualContent) ? json_encode($visualContent) : null,
        ]);

        // Create individual tool call records
        foreach ($toolCalls as $toolCall) {
            $msg->toolCalls()->create([
                'tool_call_id' => $toolCall['id'] ?? '',
                'tool_name' => $toolCall['name'] ?? '',
                'arguments' => $toolCall['arguments'] ?? [],
                'status' => 'success',
                'result' => null,
            ]);
        }

        $conversation->update([
            'last_message_at' => now(),
            'title' => $conversation->title ?? $this->generateTitle($userMessage->content),
        ]);

        return $msg;
    }

    protected function processResponse($response, AiConversation $conversation, AiMessage $userMessage): array
    {
        $content = $response->text ?? '';
        $toolCalls = $this->extractToolCalls($response);

        $this->saveAssistantMessage($conversation, $content, $userMessage, toolCalls: $toolCalls);

        $providerConfig = $this->getProviderConfig();

        return [
            'content' => $content,
            'conversation_id' => $conversation->ulid,
            'provider' => $providerConfig['code'],
            'tools_used' => array_column($toolCalls, 'name'),
        ];
    }

    protected function extractToolCalls($response): array
    {
        $toolCalls = [];

        if (method_exists($response, 'steps')) {
            foreach ($response->steps as $step) {
                if (method_exists($step, 'toolCalls') && $step->toolCalls) {
                    foreach ($step->toolCalls as $toolCall) {
                        $toolCalls[] = [
                            'id' => $toolCall->id,
                            'name' => $toolCall->name,
                            'arguments' => $toolCall->arguments,
                        ];
                    }
                }
            }
        }

        return $toolCalls;
    }

    protected function generateTitle(string $firstMessage): string
    {
        return Str::limit($firstMessage, 50);
    }

    protected function handleError(\Throwable $e, AiConversation $conversation): array
    {
        return [
            'content' => 'I apologize, but I\'m having trouble connecting right now. Please try again in a moment.',
            'conversation_id' => $conversation->ulid,
            'error' => config('app.debug') ? $e->getMessage() : null,
        ];
    }

    /**
     * Get the AI provider configuration from database or fallback to config.
     * Returns array with keys: code, model, max_steps, api_key, api_url
     */
    protected function getProviderConfig(): array
    {
        // Try to get from database first
        $provider = AiProvider::getDefault();

        if ($provider) {
            return [
                'code' => $provider->code,
                'model' => $provider->model ?? config("prism.models.{$provider->code}", 'gpt-4o'),
                'max_steps' => $provider->max_steps ?? config('prism.max_steps', 5),
                'api_key' => $provider->api_key,
                'api_url' => $provider->api_url,
            ];
        }

        // Fallback to config
        $provider = config('prism.default_provider', 'deepseek');

        return [
            'code' => $provider,
            'model' => config("prism.models.{$provider}", 'deepseek-chat'),
            'max_steps' => config('prism.max_steps', 5),
            'api_key' => null,
            'api_url' => null,
        ];
    }
}
