<?php

namespace App\Services\AI;

use App\Contracts\ImageCreativeGenerator;
use App\DTOs\CreativeBrief;
use App\DTOs\ImageGenerationResult;
use App\Services\AI\Concerns\BuildsProductionSafePrompts;
use App\Support\StyleCatalogue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use Laravel\Ai\Image;
use RuntimeException;
use Throwable;

class LaravelAiImageGenerator implements ImageCreativeGenerator
{
    use BuildsProductionSafePrompts;

    private const MAX_CUSTOM_DIMENSION = 8000;

    private const MAX_CUSTOM_PIXELS = 8_294_400;

    private const MIN_OPENAI_PIXELS = 655_360;

    private const MAX_OPENAI_DIMENSION = 3840;

    /** @var array<string, array{0: int, 1: int}> */
    private const PRESET_DIMENSIONS = [
        '1:1' => [1200, 1200],
        '9:16' => [900, 1600],
        '16:9' => [1600, 900],
        '4:5' => [960, 1200],
        '1.91:1' => [1200, 628],
    ];

    public function generate(CreativeBrief $brief, string $ratio, ?string $style = null): ImageGenerationResult
    {
        $customSize = $this->isCustomSize($ratio);
        $provider = ($customSize || $ratio === '1.91:1') ? 'openai' : config('ai.default_for_images');
        $targetDimensions = $this->targetDimensions($ratio);
        $size = $this->resolveSize($ratio, $provider, $targetDimensions);
        $prompt = $this->buildPrompt($brief, $style, $ratio);

        Log::channel('ai_image')->info('Image generation request', [
            'provider' => $provider,
            'ratio' => $ratio,
            'size' => $size,
            'prompt' => $prompt,
        ]);

        try {
            $path = Image::of($prompt)
                ->size($size)
                ->quality('high')
                ->timeout($provider === 'openai' ? 300 : 120)
                ->generate(provider: $provider)
                ->storePublicly('creatives/images', 'public');
        } catch (Throwable $e) {
            Log::channel('ai_image')->error('Image generation request failed', ['exception' => $e]);

            throw $e;
        }

        if ($path === false) {
            Log::channel('ai_image')->error('Image generation succeeded but storing the result failed', ['prompt' => $prompt]);

            throw new RuntimeException('Failed to store the generated image.');
        }

        if ($customSize || $provider !== 'openai' || $size !== implode('x', $targetDimensions)) {
            $this->resizeToExactDimensions($path, ...$targetDimensions);
        }

        Log::channel('ai_image')->info('Image generation response', ['path' => $path]);

        return new ImageGenerationResult(path: $path, prompt: $prompt, provider: $provider);
    }

    /**
     * Small paired numbers are aspect ratios; all other inputs are literal pixel sizes.
     *
     * @return array{0: int, 1: int}
     */
    public function normalizeCustomDimensions(string $ratio): array
    {
        [$width, $height] = array_map('intval', explode('x', $ratio));

        if ($width < 1 || $height < 1) {
            throw new InvalidArgumentException('Custom image dimensions must be positive.');
        }

        if ($width <= 100 && $height <= 100) {
            $aspect = $width / $height;

            if ($aspect >= 1) {
                $height = 1080;
                $width = (int) round(1080 * $aspect);
            } else {
                $width = 1080;
                $height = (int) round(1080 / $aspect);
            }

            if (max($width, $height) > self::MAX_CUSTOM_DIMENSION) {
                $scale = self::MAX_CUSTOM_DIMENSION / max($width, $height);
                $width = max(1, (int) round($width * $scale));
                $height = max(1, (int) round($height * $scale));
            }
        }

        return [$width, $height];
    }

    /** @return array{0: int, 1: int} */
    private function targetDimensions(string $ratio): array
    {
        if ($this->isCustomSize($ratio)) {
            [$width, $height] = $this->normalizeCustomDimensions($ratio);

            if ($width > self::MAX_CUSTOM_DIMENSION || $height > self::MAX_CUSTOM_DIMENSION || $width * $height > self::MAX_CUSTOM_PIXELS) {
                throw new InvalidArgumentException('Custom image dimensions exceed the supported pixel limit.');
            }

            return [$width, $height];
        }

        return self::PRESET_DIMENSIONS[$ratio]
            ?? throw new InvalidArgumentException('Unsupported image aspect ratio.');
    }

    /** @param array{0: int, 1: int} $targetDimensions */
    private function resolveSize(string $ratio, string $provider, array $targetDimensions): string
    {
        if ($provider !== 'openai') {
            return $ratio;
        }

        [$width, $height] = $targetDimensions;

        if ($this->isValidOpenAiSize($width, $height)) {
            return "{$width}x{$height}";
        }

        $roundedWidth = max(16, (int) round($width / 16) * 16);
        $roundedHeight = max(16, (int) round($height / 16) * 16);

        if ($this->isValidOpenAiSize($roundedWidth, $roundedHeight)) {
            return "{$roundedWidth}x{$roundedHeight}";
        }

        $aspect = min(3, max(1, max($width, $height) / min($width, $height)));
        $longEdge = (int) round(1024 * $aspect / 16) * 16;

        return $width >= $height ? "{$longEdge}x1024" : "1024x{$longEdge}";
    }

    private function isValidOpenAiSize(int $width, int $height): bool
    {
        $pixels = $width * $height;

        return $width % 16 === 0
            && $height % 16 === 0
            && max($width, $height) <= self::MAX_OPENAI_DIMENSION
            && max($width, $height) <= min($width, $height) * 3
            && $pixels >= self::MIN_OPENAI_PIXELS
            && $pixels <= self::MAX_CUSTOM_PIXELS;
    }

    private function isCustomSize(string $ratio): bool
    {
        return preg_match('/^\d+x\d+$/', $ratio) === 1;
    }

    private function resizeToExactDimensions(string $path, int $targetWidth, int $targetHeight): void
    {
        $disk = Storage::disk('public');
        $source = imagecreatefromstring($disk->get($path));

        if ($source === false) {
            throw new RuntimeException('Generated image could not be decoded for custom sizing.');
        }

        $sourceWidth = imagesx($source);
        $sourceHeight = imagesy($source);

        if ($sourceWidth === $targetWidth && $sourceHeight === $targetHeight) {
            imagedestroy($source);

            return;
        }

        $output = imagecreatetruecolor($targetWidth, $targetHeight);
        $aspectDifference = abs(($sourceWidth / $sourceHeight) - ($targetWidth / $targetHeight)) / ($targetWidth / $targetHeight);

        if ($aspectDifference <= 0.01) {
            imagecopyresampled($output, $source, 0, 0, 0, 0, $targetWidth, $targetHeight, $sourceWidth, $sourceHeight);
        } else {
            $scale = min($targetWidth / $sourceWidth, $targetHeight / $sourceHeight);
            $fittedWidth = max(1, (int) round($sourceWidth * $scale));
            $fittedHeight = max(1, (int) round($sourceHeight * $scale));
            $targetX = (int) floor(($targetWidth - $fittedWidth) / 2);
            $targetY = (int) floor(($targetHeight - $fittedHeight) / 2);
            $cornerColors = [
                imagecolorat($source, 0, 0),
                imagecolorat($source, $sourceWidth - 1, 0),
                imagecolorat($source, 0, $sourceHeight - 1),
                imagecolorat($source, $sourceWidth - 1, $sourceHeight - 1),
            ];
            $red = (int) round(collect($cornerColors)->avg(fn (int $color) => ($color >> 16) & 0xFF));
            $green = (int) round(collect($cornerColors)->avg(fn (int $color) => ($color >> 8) & 0xFF));
            $blue = (int) round(collect($cornerColors)->avg(fn (int $color) => $color & 0xFF));
            $background = imagecolorallocate($output, $red, $green, $blue);
            imagefill($output, 0, 0, $background);
            imagecopyresampled($output, $source, $targetX, $targetY, 0, 0, $fittedWidth, $fittedHeight, $sourceWidth, $sourceHeight);
        }

        ob_start();

        match (strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
            'jpg', 'jpeg' => imagejpeg($output, null, 90),
            'webp' => imagewebp($output, null, 90),
            default => imagepng($output, null, 9),
        };

        $resizedBytes = ob_get_clean();
        imagedestroy($source);
        imagedestroy($output);

        if ($resizedBytes === false || ! $disk->put($path, $resizedBytes)) {
            throw new RuntimeException('Failed to store the custom-sized generated image.');
        }
    }

    private function buildPrompt(CreativeBrief $brief, ?string $style, string $ratio): string
    {
        // Mirrors the video providers' style handling: look the pick up in the shared
        // catalogue for its actual mood/lighting description instead of dropping the bare
        // style name into the prompt, which the image model mostly ignored.
        $styleDescription = StyleCatalogue::descriptionFor($style);
        $styleLine = $styleDescription !== null
            ? "Visual style & Art Direction: {$styleDescription}"
            : ($style ? "Visual style & Art Direction: {$style}." : 'Visual style & Art Direction: Modern, realistic commercial advertising photography with clean studio lighting and natural textures.');
        [$canvas, $orientation] = $this->canvasDescription($ratio);
        $headline = $brief->benefits[0] ?? $brief->messaging;

        $contextLines = [];
        $contextLines[] = filled($brief->projectName)
            ? "Brand/Project: {$brief->projectName}. Product: {$brief->productName}."
            : "Product: {$brief->productName}.";

        if (filled($brief->visualDescription)) {
            $contextLines[] = "Exact Product Physical Appearance: {$brief->visualDescription}. Strictly preserve these visual details.";
        }

        if (! empty($brief->features)) {
            $contextLines[] = 'Key product features and physical details to showcase: '.implode('; ', $brief->features).'.';
        }

        if (! empty($brief->benefits)) {
            $contextLines[] = 'Supporting benefits to communicate visually: '.implode('; ', $brief->benefits).'.';
        }

        if (filled($brief->messaging)) {
            $contextLines[] = "Brand tone and campaign messaging: {$brief->messaging}.";
        }

        $contextLines[] = $styleLine;
        $campaignContext = implode("\n- ", $contextLines);

        $ctaText = $brief->cta ?: 'Learn More';

        return trim(sprintf(
            "Create a finished, conversion-focused paid-social ad creative for %s — not a plain lifestyle photo, product photo, or unbranded illustration.\n\n".
            "Campaign Context & Inputs:\n- %s\n\n".
            "Photographic & Realism Direction:\n".
            "- Commercial Photography Standard: Photorealistic, 8k resolution commercial photograph, natural studio or location lighting with soft shadows, authentic material textures and surface details, and believable depth of field. Avoid cartoonish rendering, CGI plastic gloss, airbrushed skin, or artificial 3D graphics.\n".
            "- Scene Staging: Present the hero product in an authentic, realistic setting or premium minimalist commercial set where its real-world features and benefits are in active, believable context.\n\n".
            "Final canvas: %s. Compose specifically for this %s placement. Keep every headline, logo-like product name, CTA, face, and product inside the central 80%% safe area. Use a simple background color or texture that can extend naturally to the canvas edges.\n\n".
            "Build a clear advertising hierarchy:\n".
            "1. Small product identifier, rendered exactly: \"%s\"\n".
            "2. One prominent benefit headline, rendered exactly: \"%s\"\n".
            "3. A compelling product or service visual that demonstrates the benefit.\n".
            "4. One high-contrast CTA button, rendered exactly: \"%s\"\n".
            "Do not add any other words, prices, badges, symbols, fake logos, or invented offers. Typography must be legible, correctly spelled, high contrast, and professionally aligned.\n\n".
            '%s',
            $brief->productName,
            $campaignContext,
            $canvas,
            $orientation,
            $brief->productName,
            $headline,
            $ctaText,
            'Depict only the stated product facts and benefits. Do not invent statistics, certifications, awards, guarantees, discounts, or claims. Use no third-party names, logos, or trademarks. Keep the composition polished and publication-ready with no garbled text, duplicated elements, anatomy errors, or visual artifacts.',
        ));
    }

    /** @return array{0: string, 1: string} */
    private function canvasDescription(string $ratio): array
    {
        if ($this->isCustomSize($ratio)) {
            [$width, $height] = $this->normalizeCustomDimensions($ratio);
            $orientation = match (true) {
                $width > $height => 'landscape',
                $width < $height => 'portrait',
                default => 'square',
            };

            return ["{$width} by {$height} pixels", $orientation];
        }

        return [$ratio.' aspect ratio', match ($ratio) {
            '16:9', '1.91:1' => 'landscape',
            '9:16', '4:5' => 'portrait',
            default => 'square',
        }];
    }
}
