<?php

declare(strict_types=1);

namespace App\Services\Studio;

use App\Jobs\AnalyzeArtworkAiAssistJob;
use App\Models\Artwork;
use App\Models\ArtworkAiAssist;
use App\Models\Category;
use App\Models\ContentType;
use App\Services\TagNormalizer;
use App\Services\TagService;
use App\Services\Vision\AiArtworkVectorSearchService;
use App\Services\Vision\ArtworkLlmTagSuggestionService;
use App\Services\Vision\VisionService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

final class StudioAiAssistService
{
    public function __construct(
        private readonly VisionService $vision,
        private readonly StudioAiSuggestionBuilder $builder,
        private readonly StudioAiCategoryMapper $categoryMapper,
        private readonly AiArtworkVectorSearchService $similarity,
        private readonly TagService $tagService,
        private readonly TagNormalizer $tagNormalizer,
        private readonly ArtworkLlmTagSuggestionService $llmTagSuggestions,
        private readonly StudioAiAssistEventService $eventService,
    ) {
    }

    public function queueAnalysis(Artwork $artwork, bool $force = false, ?string $intent = null, ?string $provider = null): ArtworkAiAssist
    {
        $assist = $this->assistRecord($artwork);
        $mode = $assist->mode ?: $this->builder->detectMode($artwork->loadMissing(['tags', 'categories.contentType']), []);

        $assist->forceFill([
            'status' => ArtworkAiAssist::STATUS_QUEUED,
            'mode' => $mode,
            'error_message' => null,
        ])->save();

        $artwork->forceFill(['ai_status' => ArtworkAiAssist::STATUS_QUEUED])->saveQuietly();
        $meta = ['force' => $force, 'direct' => false, 'intent' => $intent, 'provider' => $provider];
        $this->appendAction($assist, 'analysis_requested', $meta);
        $this->eventService->record($artwork, 'analysis_requested', $meta, $assist);

        AnalyzeArtworkAiAssistJob::dispatch($artwork->id, $force, $intent, $provider)->afterCommit();

        return $assist->fresh();
    }

    public function analyzeDirect(Artwork $artwork, bool $force = false, ?string $intent = null, ?string $provider = null): ArtworkAiAssist
    {
        $assist = $this->assistRecord($artwork);
        $meta = ['force' => $force, 'direct' => true, 'intent' => $intent, 'provider' => $provider];
        $this->appendAction($assist, 'analysis_requested', $meta);
        $this->eventService->record($artwork, 'analysis_requested', $meta, $assist);

        return $this->analyze($artwork, $force, $intent, $provider);
    }

    public function analyze(Artwork $artwork, bool $force = false, ?string $intent = null, ?string $provider = null): ArtworkAiAssist
    {
        $artwork->loadMissing(['tags', 'categories.contentType', 'user']);

        $assist = $this->assistRecord($artwork);
        $assist->forceFill([
            'status' => ArtworkAiAssist::STATUS_PROCESSING,
            'error_message' => null,
        ])->save();
        $artwork->forceFill(['ai_status' => ArtworkAiAssist::STATUS_PROCESSING])->saveQuietly();

        $hash = strtolower((string) preg_replace('/[^a-z0-9]/', '', (string) ($artwork->hash ?? '')));
        if ($hash === '') {
            return $this->failAssist($assist, $artwork, 'Artwork hash is missing, so AI analysis could not start.');
        }

        if (! $this->vision->isEnabled()) {
            return $this->failAssist($assist, $artwork, 'Vision analysis is disabled in the current environment.');
        }

        try {
            $visionResult = $this->vision->analyzeArtworkDetailed($artwork, $hash);
            $analysis = (array) ($visionResult['analysis'] ?? []);
            $visionDebug = (array) ($visionResult['debug'] ?? []);
            $this->vision->persistVisionMetadata(
                $artwork,
                (array) ($analysis['clip_tags'] ?? []),
                isset($analysis['blip_caption']) ? (string) $analysis['blip_caption'] : null,
                (array) ($analysis['yolo_objects'] ?? [])
            );

            $mode = $this->builder->detectMode($artwork, $analysis);
            $signals = $this->builder->buildSignals($artwork, $analysis);
            $primaryCategory = $artwork->categories->sortBy('sort_order')->first();
            $categorySuggestions = $this->categoryMapper->map($signals, $primaryCategory instanceof Category ? $primaryCategory : null);

            $titleSuggestions = $this->builder->buildTitleSuggestions($artwork, $analysis, $mode);
            $descriptionSuggestions = $this->builder->buildDescriptionSuggestions($artwork, $analysis, $mode);
            $fallbackTagSuggestions = $this->builder->buildTagSuggestions($artwork, $analysis, $mode);
            $llmTagGeneration = $this->llmTagSuggestions->suggestForArtwork($artwork, 10, 15, $provider);
            $tagSuggestions = $this->mergeTagSuggestions($llmTagGeneration, $fallbackTagSuggestions);
            $similarCandidates = $this->buildSimilarCandidates($artwork);

            $assist->forceFill([
                'status' => ArtworkAiAssist::STATUS_READY,
                'mode' => $mode,
                'title_suggestions_json' => $titleSuggestions,
                'description_suggestions_json' => $descriptionSuggestions,
                'tag_suggestions_json' => $tagSuggestions,
                'category_suggestions_json' => $categorySuggestions,
                'similar_candidates_json' => $similarCandidates,
                'raw_response_json' => [
                    'request' => [
                        'artwork_id' => (int) $artwork->id,
                        'hash' => $hash,
                        'intent' => $intent,
                        'provider' => $provider,
                        'force' => $force,
                        'current_title' => (string) ($artwork->title ?? ''),
                        'current_description' => (string) ($artwork->description ?? ''),
                        'current_tags' => $artwork->tags->pluck('slug')->values()->all(),
                    ],
                    'vision_debug' => $visionDebug,
                    'analysis' => $analysis,
                    'tag_generation' => $llmTagGeneration,
                    'generated_at' => \now()->toIso8601String(),
                    'force' => $force,
                ],
                'error_message' => null,
                'processed_at' => \now(),
            ])->save();

            $artwork->forceFill(['ai_status' => ArtworkAiAssist::STATUS_READY])->saveQuietly();
            $meta = [
                'force' => $force,
                'mode' => $mode,
                'intent' => $intent,
                'provider' => $llmTagGeneration['provider'] ?? $provider,
                'title_suggestion_count' => count($titleSuggestions),
                'description_suggestion_count' => count($descriptionSuggestions),
                'tag_suggestion_count' => count($tagSuggestions),
                'similar_candidate_count' => count($similarCandidates),
            ];
            $this->appendAction($assist, 'analysis_completed', $meta);
            $this->eventService->record($artwork, 'analysis_completed', $meta, $assist);

            return $assist->fresh();
        } catch (\Throwable $exception) {
            return $this->failAssist($assist, $artwork, $exception->getMessage());
        }
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    public function applySuggestions(Artwork $artwork, array $payload): array
    {
        $artwork->loadMissing(['tags', 'categories.contentType']);
        $assist = $this->assistRecord($artwork);
        $updated = false;
        $applied = [];

        DB::transaction(function () use ($artwork, $payload, &$updated, &$applied): void {
            if (\filled($payload['title'] ?? null)) {
                $mode = (string) ($payload['title_mode'] ?? 'replace');
                $incoming = trim((string) $payload['title']);
                $artwork->title = $mode === 'insert' && $artwork->title
                    ? trim($artwork->title . ' ' . $incoming)
                    : $incoming;
                $artwork->title_source = 'ai_applied';
                $updated = true;
                $applied[] = 'title';
            }

            if (\filled($payload['description'] ?? null)) {
                $mode = (string) ($payload['description_mode'] ?? 'replace');
                $incoming = trim((string) $payload['description']);
                $artwork->description = $mode === 'append' && \filled($artwork->description)
                    ? trim((string) $artwork->description . "\n\n" . $incoming)
                    : $incoming;
                $artwork->description_source = 'ai_applied';
                $updated = true;
                $applied[] = 'description';
            }

            if (array_key_exists('tags', $payload) && is_array($payload['tags'])) {
                $tagMode = (string) ($payload['tag_mode'] ?? 'add');
                $tags = array_values(array_filter(array_map(fn (mixed $tag): string => $this->tagNormalizer->normalize((string) $tag), $payload['tags'])));

                if ($tagMode === 'replace') {
                    $currentTags = $artwork->tags->pluck('slug')->all();
                    if ($currentTags !== []) {
                        $this->tagService->detachTags($artwork, $currentTags);
                    }
                    $this->tagService->attachAiTags($artwork, array_map(fn (string $tag): array => ['tag' => $tag], $tags));
                } elseif ($tagMode === 'remove') {
                    $this->tagService->detachTags($artwork, $tags);
                } else {
                    $this->tagService->attachAiTags($artwork, array_map(fn (string $tag): array => ['tag' => $tag], $tags));
                }

                $artwork->tags_source = 'ai_applied';
                $updated = true;
                $applied[] = 'tags';
            }

            $categoryId = $this->resolveCategoryId($payload);

            if ($categoryId !== null) {
                $artwork->categories()->sync([$categoryId]);
                $artwork->category_source = 'ai_applied';
                $updated = true;
                $applied[] = 'category';

                if (isset($payload['content_type_id']) && $payload['content_type_id'] !== null) {
                    $applied[] = 'content_type';
                }
            }

            if ($updated) {
                $artwork->save();
                $artwork->load(['tags', 'categories.contentType']);
            }
        });

        if (! empty($payload['similar_actions']) && is_array($payload['similar_actions'])) {
            $this->applySimilarActions($assist, $payload['similar_actions']);
            $applied[] = 'similar_candidates';
            $this->eventService->record($artwork, 'similar_candidates_updated', [
                'count' => count($payload['similar_actions']),
                'states' => array_values(array_unique(array_map(
                    static fn (array $action): string => (string) ($action['state'] ?? 'unknown'),
                    array_filter($payload['similar_actions'], 'is_array')
                ))),
            ], $assist);

            foreach (array_filter($payload['similar_actions'], 'is_array') as $action) {
                $state = (string) ($action['state'] ?? 'unknown');
                $candidateId = (int) ($action['artwork_id'] ?? 0);
                if ($candidateId <= 0) {
                    continue;
                }

                $eventType = match ($state) {
                    'ignored' => 'duplicate_candidate_ignored',
                    'reviewed' => 'duplicate_candidate_reviewed',
                    default => 'duplicate_candidate_updated',
                };

                $this->eventService->record($artwork, $eventType, [
                    'candidate_artwork_id' => $candidateId,
                    'state' => $state,
                ], $assist);
            }
        }

        if ($applied !== []) {
            $fields = array_values(array_unique($applied));
            $meta = ['fields' => $fields];
            $this->appendAction($assist, 'suggestions_applied', $meta);
            $this->eventService->record($artwork, 'suggestions_applied', $meta, $assist);

            foreach ($fields as $field) {
                $eventType = match ($field) {
                    'title' => 'title_suggestion_applied',
                    'description' => 'description_suggestion_applied',
                    'tags' => 'tags_suggestion_applied',
                    'content_type' => 'content_type_suggestion_applied',
                    'category' => 'category_suggestion_applied',
                    default => null,
                };

                if ($eventType === null) {
                    continue;
                }

                $this->eventService->record($artwork, $eventType, [
                    'fields' => $fields,
                ], $assist);
            }
        }

        return $this->payloadFor($artwork->fresh(['tags', 'categories.contentType', 'artworkAiAssist']));
    }

    /**
     * @return array<string, mixed>
     */
    public function payloadFor(Artwork $artwork): array
    {
        $artwork->loadMissing(['artworkAiAssist', 'tags', 'categories.contentType']);
        $assist = $artwork->artworkAiAssist;

        $primaryCategory = $artwork->categories->first();

        if (! $assist) {
            return [
                'status' => 'not_analyzed',
                'mode' => null,
                'title_suggestions' => [],
                'description_suggestions' => [],
                'tag_suggestions' => [],
                'content_type' => null,
                'category' => null,
                'similar_candidates' => [],
                'processed_at' => null,
                'error_message' => null,
                'current' => $this->currentPayload($artwork, $primaryCategory),
            ];
        }

        $categorySuggestions = is_array($assist->category_suggestions_json) ? $assist->category_suggestions_json : [];

        return [
            'status' => (string) $assist->status,
            'mode' => $assist->mode,
            'title_suggestions' => array_values((array) ($assist->title_suggestions_json ?? [])),
            'description_suggestions' => array_values((array) ($assist->description_suggestions_json ?? [])),
            'tag_suggestions' => array_values((array) ($assist->tag_suggestions_json ?? [])),
            'content_type' => $categorySuggestions['content_type'] ?? null,
            'category' => $categorySuggestions['category'] ?? null,
            'similar_candidates' => array_values((array) ($assist->similar_candidates_json ?? [])),
            'processed_at' => optional($assist->processed_at)?->toIso8601String(),
            'error_message' => $assist->error_message,
            'current' => $this->currentPayload($artwork, $primaryCategory),
            'debug' => is_array($assist->raw_response_json) ? [
                'request' => $assist->raw_response_json['request'] ?? null,
                'vision_debug' => $assist->raw_response_json['vision_debug'] ?? null,
                'analysis' => $assist->raw_response_json['analysis'] ?? null,
                'tag_generation' => $assist->raw_response_json['tag_generation'] ?? null,
                'generated_at' => $assist->raw_response_json['generated_at'] ?? null,
            ] : null,
        ];
    }

    /**
     * @param array{tags: list<string>, model: ?string, endpoint: ?string, image_url: ?string, variant: string, raw_content?: string, reason?: string, error?: string} $llmResult
     * @param array<int, array{tag: string, confidence: float|null}> $fallback
     * @return array<int, array{tag: string, confidence: float|null, source: string}>
     */
    private function mergeTagSuggestions(array $llmResult, array $fallback, int $min = 10, int $max = 15): array
    {
        $rows = collect();

        foreach (array_values($llmResult['tags'] ?? []) as $index => $tag) {
            $rows->push([
                'tag' => $tag,
                'confidence' => round(max(0.55, 0.94 - ($index * 0.03)), 2),
                'source' => 'llm',
            ]);
        }

        foreach ($fallback as $row) {
            $rows->push([
                'tag' => (string) ($row['tag'] ?? ''),
                'confidence' => isset($row['confidence']) && is_numeric($row['confidence']) ? (float) $row['confidence'] : null,
                'source' => 'vision',
            ]);
        }

        $merged = $rows
            ->filter(fn (array $row): bool => trim((string) ($row['tag'] ?? '')) !== '')
            ->unique('tag')
            ->take($max)
            ->values();

        if ($merged->count() < $min && count($fallback) > $merged->count()) {
            $merged = $rows
                ->filter(fn (array $row): bool => trim((string) ($row['tag'] ?? '')) !== '')
                ->unique('tag')
                ->take(max($min, min($max, $rows->count())))
                ->values();
        }

        return $merged->all();
    }

    private function assistRecord(Artwork $artwork): ArtworkAiAssist
    {
        return ArtworkAiAssist::query()->firstOrCreate(
            ['artwork_id' => (int) $artwork->id],
            ['status' => ArtworkAiAssist::STATUS_PENDING]
        );
    }

    private function failAssist(ArtworkAiAssist $assist, Artwork $artwork, string $message): ArtworkAiAssist
    {
        $assist->forceFill([
            'status' => ArtworkAiAssist::STATUS_FAILED,
            'error_message' => Str::limit($message, 1500, ''),
        ])->save();
        $artwork->forceFill(['ai_status' => ArtworkAiAssist::STATUS_FAILED])->saveQuietly();
        $meta = ['message' => Str::limit($message, 240, '')];
        $this->appendAction($assist, 'analysis_failed', $meta);
        $this->eventService->record($artwork, 'analysis_failed', $meta, $assist);

        return $assist->fresh();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function buildSimilarCandidates(Artwork $artwork): array
    {
        $exactMatches = Artwork::query()
            ->with('user:id,name')
            ->where('id', '!=', $artwork->id)
            ->whereNotNull('hash')
            ->where('hash', $artwork->hash)
            ->latest('id')
            ->limit(5)
            ->get()
            ->map(fn (Artwork $candidate): array => [
                'artwork_id' => (int) $candidate->id,
                'title' => (string) $candidate->title,
                'thumbnail_url' => $candidate->thumbUrl('md'),
                'match_type' => 'exact_hash',
                'score' => 1.0,
                'owner' => $candidate->user?->name,
                'url' => '/art/' . $candidate->id . '/' . $candidate->slug,
                'review_state' => null,
            ])
            ->values()
            ->all();

        $vectorMatches = [];
        if ($this->similarity->isConfigured()) {
            try {
                foreach ($this->similarity->similarToArtwork($artwork, 5) as $candidate) {
                    $vectorMatches[] = [
                        'artwork_id' => (int) ($candidate['id'] ?? 0),
                        'title' => (string) ($candidate['title'] ?? ''),
                        'thumbnail_url' => $candidate['thumb'] ?? null,
                        'match_type' => (string) ($candidate['source'] ?? 'vector_gateway'),
                        'score' => (float) ($candidate['score'] ?? 0.0),
                        'owner' => $candidate['author'] ?? null,
                        'url' => $candidate['url'] ?? null,
                        'review_state' => null,
                    ];
                }
            } catch (\Throwable $exception) {
                Log::warning('Studio AI assist similar lookup failed', [
                    'artwork_id' => (int) $artwork->id,
                    'error' => $exception->getMessage(),
                ]);
            }
        }

        return collect($exactMatches)
            ->merge($vectorMatches)
            ->unique('artwork_id')
            ->take(5)
            ->values()
            ->all();
    }

    /**
     * @param array<int, array<string, mixed>> $similarActions
     */
    private function applySimilarActions(ArtworkAiAssist $assist, array $similarActions): void
    {
        $current = collect((array) ($assist->similar_candidates_json ?? []));
        if ($current->isEmpty()) {
            return;
        }

        $indexedActions = collect($similarActions)
            ->filter(fn (mixed $item): bool => is_array($item) && isset($item['artwork_id'], $item['state']))
            ->keyBy(fn (array $item): int => (int) $item['artwork_id']);

        $updated = $current->map(function (array $candidate) use ($indexedActions): array {
            $action = $indexedActions->get((int) ($candidate['artwork_id'] ?? 0));
            if (! $action) {
                return $candidate;
            }

            $candidate['review_state'] = (string) $action['state'];
            return $candidate;
        })->values()->all();

        $assist->forceFill(['similar_candidates_json' => $updated])->save();
    }

    /**
     * @param array<string, mixed> $meta
     */
    private function appendAction(ArtworkAiAssist $assist, string $type, array $meta = []): void
    {
        $log = collect((array) ($assist->action_log_json ?? []))
            ->take(-24)
            ->push([
                'type' => $type,
                'meta' => $meta,
                'created_at' => \now()->toIso8601String(),
            ])
            ->values()
            ->all();

        $assist->forceFill(['action_log_json' => $log])->save();
    }

    /**
     * @return array<string, mixed>
     */
    private function currentPayload(Artwork $artwork, mixed $primaryCategory): array
    {
        return [
            'title' => (string) $artwork->title,
            'description' => (string) ($artwork->description ?? ''),
            'tags' => $artwork->tags->pluck('slug')->values()->all(),
            'category_id' => $primaryCategory?->id,
            'content_type_id' => $primaryCategory?->contentType?->id,
            'sources' => [
                'title' => $artwork->title_source ?: 'manual',
                'description' => $artwork->description_source ?: 'manual',
                'tags' => $artwork->tags_source ?: 'manual',
                'category' => $artwork->category_source ?: 'manual',
            ],
        ];
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function resolveCategoryId(array $payload): ?int
    {
        if (isset($payload['category_id']) && $payload['category_id'] !== null) {
            return (int) $payload['category_id'];
        }

        if (! isset($payload['content_type_id']) || $payload['content_type_id'] === null) {
            return null;
        }

        $contentType = ContentType::query()->find((int) $payload['content_type_id']);
        if (! $contentType) {
            return null;
        }

        $category = $contentType->rootCategories()
            ->where('is_active', true)
            ->orderBy('sort_order')
            ->orderBy('name')
            ->first();

        if (! $category) {
            $category = Category::query()
                ->where('content_type_id', $contentType->id)
                ->where('is_active', true)
                ->orderByRaw('CASE WHEN parent_id IS NULL THEN 0 ELSE 1 END')
                ->orderBy('sort_order')
                ->orderBy('name')
                ->first();
        }

        return $category?->id;
    }
}