<?php

namespace App\Services;

use App\Ai\Agents\MealMaker;
use App\Enums\AiUsageType;
use App\Enums\IngredientUnit;
use App\Enums\MealPostType;
use App\Enums\MealPostVisibility;
use App\Models\MealPosts;
use App\Models\User;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Ai\Image;
use Laravel\Ai\Responses\AgentResponse;
use Laravel\Ai\Responses\Data\Usage;
use Laravel\Ai\Responses\ImageResponse;
use RuntimeException;
use Throwable;

class AiMealPostGenerator
{
    public function __construct(private AiUsageRecorder $aiUsageRecorder) {}

    public function generate(): MealPosts
    {
        $aiUser = null;
        $input = null;

        try {
            $aiUser = $this->aiUser();
            $textPrompt = $this->mealPrompt();
            $input = ['text_prompt' => $textPrompt];

            [$draft, $draftResponse] = $this->generateMealDraft($textPrompt);

            $imagePrompt = $this->imagePrompt($draft);
            $input = $this->mealGenerationInput($textPrompt, $imagePrompt);

            [$imagePath, $imageResponse] = $this->generateMealImage($imagePrompt);
            $output = $this->mealGenerationOutput($draft, $imagePath);

            $ingredients = $draft['ingredients'];

            unset($draft['ingredients'], $draft['image_prompt']);

            return DB::transaction(function () use ($aiUser, $draft, $draftResponse, $imagePath, $imageResponse, $ingredients, $input, $output): MealPosts {
                $mealPost = MealPosts::create([
                    ...$draft,
                    'user_id' => $aiUser->getKey(),
                    'image_url' => $imagePath,
                    'visibility' => MealPostVisibility::Public,
                    'ai_generated' => true,
                    'eaten_at' => now(),
                ]);

                if ($ingredients !== []) {
                    $mealPost->ingredients()->createMany($ingredients);
                }

                $this->aiUsageRecorder->success(
                    user: $aiUser,
                    type: AiUsageType::MEAL_POST_GENERATION,
                    input: $input,
                    output: $output,
                    provider: $draftResponse->meta->provider ?? $imageResponse->meta->provider,
                    model: $draftResponse->meta->model ?? $imageResponse->meta->model,
                    promptTokens: $this->promptTokens($draftResponse->usage, $imageResponse->usage),
                    completionTokens: $this->completionTokens($draftResponse->usage, $imageResponse->usage),
                    totalTokens: $this->totalTokens($draftResponse->usage, $imageResponse->usage),
                    related: $mealPost,
                );

                return $mealPost->loadMissing('user:id,name,avatar_url', 'ingredients');
            });
        } catch (Throwable $exception) {
            $this->aiUsageRecorder->failed(
                user: $aiUser,
                type: AiUsageType::MEAL_POST_GENERATION,
                exception: $exception,
                input: $input,
            );

            throw $exception;
        }
    }

    /**
     * @return array{0: array<string, mixed>, 1: AgentResponse}
     */
    private function generateMealDraft(string $prompt): array
    {
        $response = MealMaker::make()->prompt($prompt, timeout: $this->textTimeout());

        return [
            $this->normalizeDraft(
                $response instanceof Arrayable
                    ? $response->toArray()
                    : (json_decode($response->text ?? '', true) ?: [])
            ),
            $response,
        ];
    }

    /**
     * @return array{0: string, 1: ImageResponse}
     */
    private function generateMealImage(string $prompt): array
    {
        $response = Image::of($prompt)
            ->landscape()
            ->quality($this->imageQuality())
            ->timeout($this->imageTimeout())
            ->generate();

        $path = $response->store($this->imageStoragePath());

        if (! is_string($path)) {
            throw new RuntimeException("L'image du repas IA n'a pas pu etre stockee.");
        }

        return [$path, $response];
    }

    /**
     * @return array<string, mixed>
     */
    private function mealGenerationInput(string $textPrompt, string $imagePrompt): array
    {
        return [
            'text_prompt' => $textPrompt,
            'image_prompt' => $imagePrompt,
            'image' => [
                'size' => '3:2',
                'quality' => $this->imageQuality(),
                'storage_path' => $this->imageStoragePath(),
            ],
        ];
    }

    /**
     * @param  array<string, mixed>  $draft
     * @return array<string, mixed>
     */
    private function mealGenerationOutput(array $draft, string $imagePath): array
    {
        return [
            'title' => $draft['title'],
            'caption' => $draft['caption'],
            'type' => $draft['type']->value,
            'calories' => $draft['calories'],
            'proteins' => $draft['proteins'],
            'carbs' => $draft['carbs'],
            'fats' => $draft['fats'],
            'ingredients' => array_map(fn (array $ingredient): array => [
                'position' => $ingredient['position'],
                'ingredient' => $ingredient['ingredient'],
                'quantity' => $ingredient['quantity'],
                'unit' => $ingredient['unit']->value,
            ], $draft['ingredients']),
            'image_prompt' => $draft['image_prompt'],
            'image_url' => $imagePath,
        ];
    }

    private function promptTokens(Usage ...$usages): int
    {
        $tokens = 0;

        foreach ($usages as $usage) {
            $tokens += $usage->promptTokens;
        }

        return $tokens;
    }

    private function completionTokens(Usage ...$usages): int
    {
        $tokens = 0;

        foreach ($usages as $usage) {
            $tokens += $usage->completionTokens;
        }

        return $tokens;
    }

    private function totalTokens(Usage ...$usages): int
    {
        return $this->promptTokens(...$usages) + $this->completionTokens(...$usages);
    }

    private function aiUser(): User
    {
        $email = $this->configString('user_email', 'ai@daily-meal.local');

        return User::query()->firstOrCreate(
            ['email' => $email],
            [
                'name' => $this->configString('user_name', 'Daily Meal AI'),
                'password' => Hash::make(Str::random(64)),
                'bio' => 'Compte utilise pour publier des repas generes automatiquement.',
            ],
        );
    }

    private function mealPrompt(): string
    {
        $recentMainIngredients = $this->recentMainIngredientsToAvoid();
        $avoidRecentIngredients = $recentMainIngredients === []
            ? '- Aucun ingredient recent a eviter pour cette generation.'
            : '- Evite les ingredients principaux deja utilises dans les 10 derniers repas: '.implode(', ', $recentMainIngredients).'.';

        return <<<PROMPT
Genere un plat completement aleatoire, sans tenir compte du mois actuel, de la saison ou des produits de saison.

Contraintes:
- Le plat doit etre realiste, appetissant et suffisamment different des classiques trop evidents.
- Choisis librement la cuisine, le pays, le moment de la journee et les ingredients.
- Retourne une seule portion avec calories, macros et ingredients coherents.
- Evite les aliments impossibles a representer clairement en photo.
{$avoidRecentIngredients}
- Aucun texte sur l'image.
PROMPT;
    }

    /**
     * @return string[]
     */
    private function recentMainIngredientsToAvoid(): array
    {
        return MealPosts::query()
            ->with([
                'ingredients' => function (HasMany $query): void {
                    $query->where('position', '<=', 3);
                },
            ])
            ->latest('eaten_at')
            ->limit(10)
            ->get()
            ->flatMap(fn (MealPosts $mealPost): array => $mealPost->ingredients
                ->pluck('ingredient')
                ->all())
            ->map(fn (mixed $ingredient): string => $this->cleanString($ingredient, 255))
            ->filter()
            ->unique(fn (string $ingredient): string => Str::lower($ingredient))
            ->values()
            ->all();
    }

    private function imagePrompt(array $draft): string
    {
        $prompt = $this->cleanString($draft['image_prompt'] ?? '', 1500);

        if ($prompt === '') {
            $prompt = "Photorealistic food photography of {$draft['title']}, single serving, natural light, restaurant plating.";
        }

        return $prompt.' No text, no watermark, no people, no hands.';
    }

    private function normalizeDraft(array $draft): array
    {
        $type = MealPostType::tryFrom($this->cleanString($draft['type'] ?? '', 32))
            ?? MealPostType::OTHER;

        return [
            'title' => $this->cleanString($draft['title'] ?? 'Repas IA', 255) ?: 'Repas IA',
            'caption' => $this->cleanString($draft['caption'] ?? '', 1000),
            'type' => $type,
            'calories' => $this->positiveInteger($draft['calories'] ?? 0),
            'proteins' => $this->positiveNumber($draft['proteins'] ?? 0),
            'carbs' => $this->positiveNumber($draft['carbs'] ?? 0),
            'fats' => $this->positiveNumber($draft['fats'] ?? 0),
            'ingredients' => $this->normalizeIngredients($draft['ingredients'] ?? []),
            'image_prompt' => $this->cleanString($draft['image_prompt'] ?? '', 1500),
        ];
    }

    private function normalizeIngredients(mixed $ingredients): array
    {
        if (! is_array($ingredients)) {
            return [];
        }

        $normalized = [];

        foreach (array_slice($ingredients, 0, 12) as $ingredient) {
            if (! is_array($ingredient)) {
                continue;
            }

            $name = $this->cleanString($ingredient['ingredient'] ?? '', 255);

            if ($name === '') {
                continue;
            }

            $unit = IngredientUnit::tryFrom($this->cleanString($ingredient['unit'] ?? '', 16))
                ?? IngredientUnit::GRAM;

            $normalized[] = [
                'position' => count($normalized) + 1,
                'ingredient' => $name,
                'quantity' => max(1, $this->positiveInteger($ingredient['quantity'] ?? 1)),
                'unit' => $unit,
            ];
        }

        return $normalized;
    }

    private function cleanString(mixed $value, int $limit): string
    {
        if (! is_string($value) && ! is_numeric($value)) {
            return '';
        }

        $cleaned = trim((string) preg_replace('/\s+/', ' ', (string) $value));

        return Str::limit($cleaned, $limit, '');
    }

    private function positiveInteger(mixed $value): int
    {
        if (! is_numeric($value)) {
            return 0;
        }

        return max(0, (int) round((float) $value));
    }

    private function positiveNumber(mixed $value): float
    {
        if (! is_numeric($value)) {
            return 0;
        }

        return round(max(0, (float) $value), 1);
    }

    private function imageQuality(): string
    {
        $quality = $this->configString('image_quality', 'medium');

        return in_array($quality, ['low', 'medium', 'high'], true) ? $quality : 'medium';
    }

    private function imageStoragePath(): string
    {
        return $this->configString('image_path', 'meal-posts/ai-generated');
    }

    private function textTimeout(): int
    {
        return max(1, (int) config('meal_posts.ai_generation.text_timeout', 90));
    }

    private function imageTimeout(): int
    {
        return max(1, (int) config('meal_posts.ai_generation.image_timeout', 120));
    }

    private function configString(string $key, string $fallback): string
    {
        $value = trim((string) config("meal_posts.ai_generation.{$key}", $fallback));

        return $value !== '' ? $value : $fallback;
    }
}
