<?php

namespace App\Http\Controllers;

use App\Domains\Engine\Enums\EngineEnum;
use App\Domains\Entity\Enums\EntityEnum;
use App\Domains\Entity\Facades\Entity;
use App\Extensions\AzureTTS\System\Services\AzureService;
use App\Helpers\Classes\ApiHelper;
use App\Helpers\Classes\Helper;
use App\Models\OpenAIGenerator;
use App\Models\RateLimit;
use App\Models\Setting;
use App\Models\SettingTwo;
use App\Models\User;
use App\Models\UserOpenai;
use Carbon\Carbon;
use Exception;
use Google\ApiCore\ApiException;
use Google\Cloud\TextToSpeech\V1\AudioConfig;
use Google\Cloud\TextToSpeech\V1\AudioEncoding;
use Google\Cloud\TextToSpeech\V1\SsmlVoiceGender;
use Google\Cloud\TextToSpeech\V1\SynthesisInput;
use Google\Cloud\TextToSpeech\V1\TextToSpeechClient;
use Google\Cloud\TextToSpeech\V1\VoiceSelectionParams;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Psr\Http\Message\StreamInterface;
use Throwable;

// comes from extension

class TTSController extends Controller
{
    private Setting $settings;

    private SettingTwo $settingsTwo;

    private OpenAIGenerator $ai;

    /**
     * TTSController constructor.
     *
     * Initializes the settings by retrieving the first instance from the database.
     */
    public function __construct()
    {
        $this->settings = Setting::getCache();
        $this->settingsTwo = SettingTwo::getCache();
        $this->ai = OpenAIGenerator::where('slug', 'ai_voiceover')->first();
    }

    /**
     * Generate speech from text input based on the specified AI engine and settings.
     *
     * @param  Request  $request  the HTTP request containing speech parameters
     *
     * @return JsonResponse
     *
     * @throws Exception
     * @throws Throwable
     */
    public function generateSpeech(Request $request)
    {
        // if (Helper::appIsDemo()) {
        //    return $this->sendErrorResponse(__('This feature is disabled in Demo version.'));
        // }

        $speeches = json_decode($request->speeches, true, 512, JSON_THROW_ON_ERROR);
        if (empty($speeches)) {
            return $this->sendErrorResponse(__('Please provide inputs.'));
        }

        if ($this->settingsTwo->daily_voice_limit_enabled) {
            $limitResponse = $this->checkDailyVoiceLimit();
            if ($limitResponse !== null) {
                return $limitResponse;
            }
        }

        $resAudio = '';
        $langsAndVoices = [];
        $wordCount = 0;

        $user = Auth::user();
        if (! $user) {
            return $this->sendErrorResponse(__('Unauthorized Access.'));
        }

        $azureService = $this->hasAzureSpeech($speeches) ? new AzureService : null;

        foreach ($speeches as $speech) {
            $model = $this->getAIModel($speech['platform'], $speech['pace']);
            $driver = Entity::driver($model)->inputVoiceCount(1)->calculateCredit();
            $langsAndVoices['language'][] = $speech['lang'];
            $langsAndVoices['voices'][] = $speech['voice'];

            if (! $driver->hasCreditBalanceForInput()) {
                return $this->sendErrorResponse(__('Insufficient credits to generate audio.'));
            }

            try {
                $audioContent = $this->processSpeech($speech, $azureService);
            } catch (ApiException|GuzzleException $e) {
                return $this->sendErrorResponse(__('Failed to connect to the AI service') . ': ' . $e->getMessage());
            }

            $resAudio .= $audioContent;
            $wordCount += $this->countWords($speech['content']);
            $driver->decreaseCredit();
        }

        $audioName = $this->storeAudio($user->id, $resAudio);
        $this->saveSpeechRecord($user, $request, $audioName, $wordCount, $langsAndVoices);

        return $this->buildResponse($request, $audioName, $user);
    }

    /**
     * Determines the appropriate AI model based on the platform and speech pace.
     *
     * @param  string  $platform  The platform identifier (e.g., Google, OpenAI, etc.).
     * @param  string  $pace  the speech pace to determine the model
     *
     * @return EntityEnum|null the corresponding AI model or null if invalid
     *
     * @throws Exception
     */
    private function getAIModel(string $platform, string $pace): ?EntityEnum
    {
        return match ($platform) {
            EngineEnum::GOOGLE->slug()     => EntityEnum::GOOGLE,
            EngineEnum::ELEVENLABS->slug() => EntityEnum::ELEVENLABS,
            EngineEnum::AZURE->slug()      => EntityEnum::AZURE,
            EngineEnum::OPEN_AI->slug()    => EntityEnum::fromSlug($pace),
            default                        => throw new Exception(__('Invalid AI Model.')),
        };
    }

    /**
     * Processes the speech based on the selected AI model and platform.
     *
     * @param  array  $speech  the speech data from the request
     * @param  ?AzureService|null  $azureService  optional Azure service instance
     *
     * @throws ApiException
     * @throws GuzzleException
     */
    private function processSpeech(array $speech, ?AzureService $azureService): StreamInterface|JsonResponse|string
    {
        return match ($speech['platform']) {
            EngineEnum::GOOGLE->value     => $this->processGoogleSpeech($speech),
            EngineEnum::OPEN_AI->value    => $this->processOpenAISpeech($speech),
            EngineEnum::ELEVENLABS->value => $this->processElevenLabsSpeech($speech),
            EngineEnum::AZURE->value      => $this->processAzureSpeech($speech, $azureService),
            default                       => $this->sendErrorResponse(__('Invalid platform.')),
        };
    }

    /**
     * Processes speech using Google Text-to-Speech API.
     *
     * @throws ApiException
     */
    private function processGoogleSpeech(array $speech): string
    {
        if (! $this->checkGoogleGcsFile()) {
            throw new ApiException(__('Google TTS credentials are missing or invalid.'), 419);
        }

        try {
            $client = new TextToSpeechClient([
                'credentials' => storage_path($this->settings->gcs_file),
                'project_id'  => $this->settings->gcs_name,
            ]);
        } catch (Exception $e) {
            throw new ApiException(__('Failed to connect to Google TTS service: ') . $e->getMessage(), 419);
        }

        $ssml = $this->buildSSML($speech);
        $synthesisInputSsml = (new SynthesisInput)->setSsml($ssml);
        $voice = (new VoiceSelectionParams)->setLanguageCode($speech['lang'])->setSsmlGender(SsmlVoiceGender::FEMALE);
        $audioConfig = (new AudioConfig)->setAudioEncoding(AudioEncoding::MP3);

        return $client->synthesizeSpeech($synthesisInputSsml, $voice, $audioConfig)->getAudioContent();
    }

    /**
     * Builds the SSML content for Google Text-to-Speech API.
     *
     * @param  array  $speech  the speech data from the request
     *
     * @return string the SSML content
     */
    private function buildSSML(array $speech): string
    {
        $ssml = '<speak>';
        $ssml .= sprintf(
            '<lang xml:lang="%3$s">
                        <prosody rate="%4$s">
                            <voice name="%1$s">%2$s</voice>
                            <break time="%5$ss"/>
                        </prosody>
                    </lang>',
            $speech['voice'],
            $speech['content'],
            $speech['lang'],
            $speech['pace'],
            $speech['break'],
        );

        $ssml .= '</speak>';

        return $ssml;
    }

    /**
     * Processes speech using OpenAI's API.
     *
     * @param  array  $speech  the speech data from the request
     *
     * @return StreamInterface the audio content
     *
     * @throws GuzzleException
     */
    private function processOpenAISpeech(array $speech): StreamInterface
    {
        $apiKey = $this->getOpenAIApiKey();
        $client = new Client;

        $response = $client->request('POST', 'https://api.openai.com/v1/audio/speech', [
            'headers' => [
                'Authorization' => 'Bearer ' . $apiKey,
                'Content-Type'  => 'application/json',
            ],
            'json' => [
                'language' => $speech['language'],
                'model'    => $speech['pace'],
                'input'    => $speech['content'],
                'voice'    => $speech['voice'],
            ],
        ]);

        return $response->getBody();
    }

    /**
     * Processes speech using ElevenLabs API.
     *
     * @param  array  $speech  the speech data from the request
     *
     * @return StreamInterface the audio content
     *
     * @throws GuzzleException
     */
    private function processElevenLabsSpeech($speech)
    {
        $apiKey = $this->settingsTwo->elevenlabs_api_key;
        $client = new Client;

        $response = $client->request('POST', 'https://api.elevenlabs.io/v1/text-to-speech/' . $speech['voice'], [
            'headers' => [
                'Content-Type' => 'application/json',
                'xi-api-key'   => $apiKey,
            ],
            'json' => [
                'text'           => $speech['content'],
                'model_id'       => 'eleven_multilingual_v2',
                'voice_settings' => [
                    'similarity_boost'  => 0.75,
                    'stability'         => 0.95,
                    'style'             => $speech['pace'] / 100,
                    'use_speaker_boost' => true,
                ],
            ],
        ]);

        return $response->getBody();
    }

    /**
     * Processes speech using Azure's Text-to-Speech API.
     *
     * @param  array  $speech  the speech data from the request
     * @param  AzureService|null  $azureService  the Azure service instance for processing speech
     *
     * @return string the audio content
     */
    private function processAzureSpeech(array $speech, ?AzureService $azureService): string
    {
        return $azureService?->synthesizeSpeech($speech['voice'], $speech['content'], $speech['lang']);
    }

    /**
     * Checks whether the Google Cloud credentials file exists.
     *
     * @return bool returns true if credentials exist, otherwise false
     */
    private function checkGoogleGcsFile(): bool
    {
        return ! empty($this->settings->gcs_file) && ! empty($this->settings->gcs_name);
    }

    /**
     * Checks if any speech in the request is using Azure platform.
     *
     * @param  array  $speeches  array of speech data
     *
     * @return bool true if any speech uses Azure platform, otherwise false
     */
    private function hasAzureSpeech(array $speeches): bool
    {
        return collect($speeches)->contains(fn ($speech) => $speech['platform'] === EngineEnum::AZURE->value);
    }

    /**
     * Counts the number of words in a given text.
     *
     * @param  string  $text  the input text
     *
     * @return int the word count
     */
    private function countWords(string $text): int
    {
        return str_word_count($text);
    }

    /**
     * Stores the generated audio content in storage.
     *
     * @param  int  $userId  the ID of the user
     * @param  string  $audioContent  the generated audio content
     *
     * @return string the name of the stored audio file
     */
    private function storeAudio(int $userId, string $audioContent): string
    {
        $audioName = $userId . '-' . Str::uuid() . '.mp3';
        Storage::disk('public')->put("{$audioName}", $audioContent);

        return $audioName;
    }

    /**
     * Saves the record of the generated speech in the database.
     *
     * @param  User  $user  the authenticated user
     * @param  Request  $request  the original request data
     * @param  string  $audioName  the name of the stored audio file
     * @param  int  $wordCount  the total word count of the speech content
     * @param  array  $langsAndVoices  array of languages and voices used in the speeches
     */
    private function saveSpeechRecord(User $user, Request $request, string $audioName, int $wordCount, array $langsAndVoices): void
    {
        if (! $request->input('preview')) {
            try {
                $speaches = json_decode($request->speeches, true, 512, JSON_THROW_ON_ERROR);

                if (is_array($speaches) && isset($speaches[0]['platform']) && $speaches[0]['platform'] === 'elevenlabs') {
                    if (isset($speaches[0]['name'])) {
                        $langsAndVoices['voices'][0] = $speaches[0]['name'];
                    }
                }
            } catch (Exception $e) {
            }

            UserOpenai::create([
                'team_id'    => $user->team_id,
                'title'      => $request->workbook_title,
                'slug'       => Str::random(20) . Str::slug($user->fullName()) . '-workbook',
                'user_id'    => $user->id,
                'openai_id'  => $this->ai->id,
                'input'      => $request->speeches,
                'response'   => json_encode($langsAndVoices),
                'output'     => $audioName,
                'hash'       => Str::random(256),
                'credits'    => $wordCount,
                'words'      => $wordCount,
            ]);
        }
    }

    /**
     * Builds the response after successful audio generation.
     *
     * @param  Request  $request  the original request data
     * @param  string  $audioName  the name of the generated audio file
     * @param  User  $user  the authenticated user
     *
     * @return JsonResponse the response containing the audio URL or redirect
     *
     * @throws Throwable
     */
    private function buildResponse(Request $request, string $audioName, User $user): JsonResponse
    {
        if ($request->input('preview')) {
            return response()->json(['audioPath' => '/uploads/' . $audioName, 'output' => '<div class="data-audio" data-audio="/uploads/' . $audioName . '"><div class="audio-preview"></div></div>']);
        }

        if ($request->input('from_api')) {
            return response()->json(['audioPath' => '/uploads/' . $audioName, 'output' => '<div class="data-audio" data-audio="/uploads/' . $audioName . '"><div class="audio-preview"></div></div>']);
        }

        $userOpenai = UserOpenai::where('user_id', $user->id)->where('openai_id', $this->ai->id)->orderBy('created_at', 'desc')->paginate(10);
        $userOpenai->withPath(route('dashboard.user.openai.generator', $this->ai->slug));
        $openai = $this->ai;
        $html2 = view('panel.user.openai.components.generator_sidebar_table', compact('userOpenai', 'openai'))->render();

        return response()->json(compact('html2'));
    }

    /**
     * Check if the user has reached the daily voice generation limit.
     */
    private function checkDailyVoiceLimit(): ?JsonResponse
    {
        $ipAddress = $this->getClientIp();
        $dbIpAddress = RateLimit::where('ip_address', $ipAddress)
            ->where('type', 'voice')
            ->first();

        if ($dbIpAddress) {
            // Reset attempts if a new day has started
            if ($this->isNewDay($dbIpAddress->last_attempt_at)) {
                $dbIpAddress->attempts = 0;
            }
        } else {
            // Create a new rate limit entry if none exists
            $dbIpAddress = new RateLimit(['ip_address' => $ipAddress]);
        }

        // Check if the user has exceeded their daily limit
        if ($dbIpAddress->attempts >= $this->settingsTwo->allowed_voice_count) {
            return $this->sendErrorResponse($this->getExceededLimitMessage());
        }

        // Increment the attempts and update the timestamp
        $this->incrementAttempts($dbIpAddress);

        return null; // No limit exceeded, continue with the process
    }

    /**
     * Increment attempts and update the last attempt time.
     */
    private function incrementAttempts(RateLimit $rateLimit): void
    {
        $rateLimit->attempts++;
        $rateLimit->type = 'voice';
        $rateLimit->last_attempt_at = now();
        $rateLimit->save();
    }

    /**
     * Check if the last attempt was made on a different day.
     */
    private function isNewDay(string $lastAttemptAt): bool
    {
        return now()->diffInDays(Carbon::parse($lastAttemptAt)) > 0;
    }

    /**
     * Get the appropriate message when the daily limit is exceeded.
     */
    private function getExceededLimitMessage(): string
    {
        return Helper::appIsDemo()
            ? __('You have reached the maximum number of voice generation allowed on the demo.')
            : __('You have reached the maximum number of voice generation allowed.');
    }

    /**
     * Send an error response with the specified message.
     */
    private function sendErrorResponse(string $message): JsonResponse
    {
        return response()->json(['errors' => [$message]], 429);
    }

    /**
     * Get the client IP address, considering potential use of Cloudflare.
     */
    private function getClientIp(): string
    {
        return request()?->header('CF-Connecting-IP') ?? request()->ip();
    }

    /**
     * Get the OpenAI API key to use for the request.
     */
    private function getOpenAIApiKey(): string
    {
        return ApiHelper::setOpenAiKey();
    }
}
