<?php

namespace App\Services\AI;

use App\Contracts\VideoAvatarProvider;
use App\DTOs\VideoRenderResult;
use App\Exceptions\ProviderNotConfiguredException;
use App\Models\Avatar;
use App\Models\Voice;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;

/**
 * Renders video creatives with Google's Veo model via the raw Gemini API
 * (laravel/ai has no Veo/video-generation support to wrap, unlike its
 * Image/Audio facades — so this calls generativelanguage.googleapis.com directly).
 *
 * Caveat: Veo is a text-to-video model with no concept of a persistent "avatar
 * identity" or of speaking in one of our own synthesized `Voice` catalogue
 * entries. `$avatar`/`$voice` are folded into the prompt as descriptive hints
 * (appearance/gender/accent/tone) only — not a guaranteed likeness or an
 * actual audio track from our `VoiceSynthesizer`. If a recurring avatar
 * identity + our own TTS voice is required, a dedicated avatar-video vendor
 * (HeyGen/Synthesia/D-ID) is still the better fit; this adapter exists
 * because a Gemini API key was already available.
 */
class GeminiVeoVideoAvatarProvider implements VideoAvatarProvider
{
    /** Veo only accepts these exact values; anything else is rejected by the API. */
    private const SUPPORTED_DURATIONS = [4, 6, 8];

    public function render(Avatar $avatar, Voice $voice, array $script, array $storyboard, int $duration, string $ratio, ?string $style = null): VideoRenderResult
    {
        $apiKey = config('ai.providers.gemini.key');

        if (blank($apiKey)) {
            throw new ProviderNotConfiguredException(
                'GEMINI_API_KEY is not set — cannot render video with the Gemini Veo provider.'
            );
        }

        $baseUrl = rtrim(config('ai.providers.gemini.url'), '/');
        $model = config('creative.gemini_veo.model');
        $durationSeconds = $this->clampDuration($duration);
        $prompt = $this->buildPrompt($avatar, $voice, $script, $storyboard, $durationSeconds, $style);
        $aspectRatio = $ratio === '9:16' ? '9:16' : '16:9';

        $operationName = $this->startGeneration($baseUrl, $apiKey, $model, $prompt, $durationSeconds, $aspectRatio);
        $videoUri = $this->pollUntilDone($baseUrl, $apiKey, $operationName);
        $path = $this->downloadAndStore($videoUri, $apiKey);

        return new VideoRenderResult(
            assetPath: $path,
            meta: ['provider' => 'gemini-veo', 'model' => $model, 'operation' => $operationName, 'duration' => $durationSeconds],
        );
    }

    /** Veo 3.1 only generates 4/6/8-second clips (no 15/30/60s option) — pick the closest supported value. */
    private function clampDuration(int $duration): int
    {
        return collect(self::SUPPORTED_DURATIONS)->sortBy(fn (int $d) => abs($d - $duration))->first();
    }

    private function buildPrompt(Avatar $avatar, Voice $voice, array $script, array $storyboard, int $duration, ?string $style = null): string
    {
        $sceneLines = collect($storyboard)
            ->map(fn (array $scene, int $i) => sprintf('Scene %d: %s', $i + 1, $scene['scene'] ?? ''))
            ->implode("\n");

        // Veo (like Sora) only voices lines placed in an explicit "Dialogue:" block with
        // quoted speech — dumping labeled script fields into the prose produces a silent video.
        // The spoken line must also fit the clip's actual length: cramming the whole
        // script into one line for a short clip makes the model skip/mumble the audio
        // entirely rather than speed it up unnaturally, so it's budgeted by duration.
        $spokenLine = $this->buildSpokenLine($script, $duration);

        // UGC swaps the polished-ad framing for an organic, phone-shot testimonial look —
        // same avatar/voice/script/storyboard, just different cinematography instructions.
        $sceneDescription = $style === 'UGC'
            ? 'A short UGC-style talking-avatar video, shot like an authentic phone selfie video — handheld framing, natural home/room lighting, casual unpolished delivery, no studio gloss.'
            : 'A short high-quality talking-avatar advertisement video, studio-polished.';

        return trim(sprintf(
            "%s Presenter: %s, %s, %s accent, speaking in a %s tone, addressing the camera directly.\n\n".
            "Dialogue:\n- %s: \"%s\"\n\n".
            "Storyboard:\n%s",
            $sceneDescription,
            $avatar->name,
            $avatar->gender ?? 'presenter',
            $avatar->accent ?? 'neutral',
            $voice->tone ?? 'friendly',
            $avatar->name,
            $spokenLine,
            $sceneLines,
        ));
    }

    /**
     * Budgets the spoken dialogue to roughly match natural speech pace for the
     * clip's duration (~2.2 words/second), prioritizing the hook and call to
     * action first, then filling remaining time with supporting script fields.
     */
    private function buildSpokenLine(array $script, int $duration): string
    {
        $wordBudget = max(6, (int) floor($duration * 2.2));

        $prioritized = collect([
            $script['hook'] ?? '',
            $script['cta'] ?? '',
            $script['product'] ?? '',
            $script['problem'] ?? '',
            $script['benefits'] ?? '',
        ])->filter();

        $words = [];

        foreach ($prioritized as $line) {
            foreach (preg_split('/\s+/', trim($line)) as $word) {
                if (count($words) >= $wordBudget) {
                    break 2;
                }

                $words[] = $word;
            }
        }

        return implode(' ', $words);
    }

    private function startGeneration(string $baseUrl, string $apiKey, string $model, string $prompt, int $durationSeconds, string $aspectRatio): string
    {
        $response = Http::withHeaders(['x-goog-api-key' => $apiKey])
            ->timeout(30)
            ->post("{$baseUrl}/models/{$model}:predictLongRunning", [
                'instances' => [['prompt' => $prompt]],
                'parameters' => [
                    'durationSeconds' => $durationSeconds,
                    'aspectRatio' => $aspectRatio,
                    // Veo 3.1 generates audio natively and rejects this param entirely
                    // (400 INVALID_ARGUMENT) — do not re-add it.
                ],
            ]);

        if ($response->failed()) {
            logger()->error('Veo generation request failed', ['body' => $response->body()]);

            throw new RuntimeException('Veo generation request failed. See logs for the vendor response.');
        }

        $operationName = $response->json('name');

        if (blank($operationName)) {
            throw new RuntimeException('Veo generation did not return an operation name.');
        }

        return $operationName;
    }

    private function pollUntilDone(string $baseUrl, string $apiKey, string $operationName): string
    {
        $interval = (int) config('creative.gemini_veo.poll_interval_seconds', 10);
        $maxAttempts = (int) config('creative.gemini_veo.max_poll_attempts', 30);

        for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
            $response = Http::withHeaders(['x-goog-api-key' => $apiKey])
                ->timeout(30)
                ->get("{$baseUrl}/{$operationName}");

            if ($response->failed()) {
                logger()->error('Veo operation polling failed', ['body' => $response->body()]);

                throw new RuntimeException('Veo operation polling failed. See logs for the vendor response.');
            }

            if ($response->json('done') === true) {
                if ($response->json('error')) {
                    logger()->error('Veo generation failed', ['error' => $response->json('error')]);

                    throw new RuntimeException('Veo generation failed. See logs for the vendor error.');
                }

                $uri = $response->json('response.generateVideoResponse.generatedSamples.0.video.uri');

                if (blank($uri)) {
                    throw new RuntimeException('Veo operation completed without a video URI.');
                }

                return $uri;
            }

            if ($attempt < $maxAttempts - 1) {
                sleep($interval);
            }
        }

        throw new RuntimeException("Veo video generation timed out after {$maxAttempts} polling attempts.");
    }

    private function downloadAndStore(string $videoUri, string $apiKey): string
    {
        // Stream straight to a temp file instead of buffering the whole clip in memory via ->body().
        $tmpPath = tempnam(sys_get_temp_dir(), 'veo-');

        $response = Http::withHeaders(['x-goog-api-key' => $apiKey])->timeout(60)->sink($tmpPath)->get($videoUri);

        if ($response->failed()) {
            @unlink($tmpPath);

            throw new RuntimeException('Failed to download the generated video from Gemini.');
        }

        $filename = 'creatives/videos/'.Str::ulid().'.mp4';
        $stream = fopen($tmpPath, 'r');
        $stored = Storage::disk('public')->put($filename, $stream);
        fclose($stream);
        @unlink($tmpPath);

        if ($stored === false) {
            throw new RuntimeException("Failed to store the rendered video at {$filename}.");
        }

        return $filename;
    }
}
