<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Artwork;
use App\Services\ArtworkSearchService;
use App\Services\Recommendations\HybridSimilarArtworksService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

/**
 * GET /api/art/{id}/similar
 *
 * Returns up to 12 similar artworks using the hybrid recommender (precomputed lists)
 * with a Meilisearch-based fallback if no precomputed data exists.
 *
 * Query params:
 *   ?type=similar (default) | visual | tags | behavior
 *
 * Priority (default):
 *   1. Hybrid precomputed (tag + behavior + optional vector)
 *   2. Meilisearch tag-overlap fallback (legacy)
 */
final class SimilarArtworksController extends Controller
{
    private const LIMIT = 12;

    public function __construct(
        private readonly ArtworkSearchService $search,
        private readonly HybridSimilarArtworksService $hybridService,
    ) {}

    public function __invoke(Request $request, int $id): JsonResponse
    {
        $artwork = Artwork::public()
            ->published()
            ->with(['tags:id,slug', 'categories:id,slug'])
            ->find($id);

        if (! $artwork) {
            return response()->json(['error' => 'Artwork not found'], 404);
        }

        $type = $request->query('type');
        $validTypes = ['similar', 'visual', 'tags', 'behavior'];
        if ($type !== null && ! in_array($type, $validTypes, true)) {
            $type = null; // ignore invalid, fall through to default
        }

        // Service handles its own caching (6h TTL), no extra controller-level cache
        $hybridResults = $this->hybridService->forArtwork($artwork->id, self::LIMIT, $type);

        if ($hybridResults->isNotEmpty()) {
            // Eager-load relations needed for formatting
            $ids = $hybridResults->pluck('id')->all();
            $loaded = Artwork::query()
                ->whereIn('id', $ids)
                ->with(['tags:id,slug', 'stats', 'user:id,name', 'user.profile:user_id,avatar_hash'])
                ->get()
                ->keyBy('id');

            $items = $hybridResults->values()->map(function (Artwork $a) use ($loaded) {
                $full = $loaded->get($a->id) ?? $a;
                return $this->formatArtwork($full);
            })->all();

            return response()->json(['data' => $items]);
        }

        // Fall back to Meilisearch tag-overlap search
        $items = $this->findSimilarViaSearch($artwork);

        return response()->json(['data' => $items]);
    }

    private function formatArtwork(Artwork $artwork): array
    {
        return [
            'id'            => $artwork->id,
            'title'         => $artwork->title,
            'slug'          => $artwork->slug,
            'thumb'         => $artwork->thumbUrl('md'),
            'url'           => '/art/' . $artwork->id . '/' . $artwork->slug,
            'author'        => $artwork->user?->name ?? 'Artist',
            'author_avatar' => $artwork->user?->profile?->avatar_url,
            'author_id'     => $artwork->user_id,
            'orientation'   => $this->orientation($artwork),
            'width'         => $artwork->width,
            'height'        => $artwork->height,
        ];
    }

    /**
     * Legacy Meilisearch-based similar artworks (fallback).
     */
    private function findSimilarViaSearch(Artwork $artwork): array
    {
        $tagSlugs      = $artwork->tags->pluck('slug')->values()->all();
        $categorySlugs = $artwork->categories->pluck('slug')->values()->all();
        $srcOrientation = $this->orientation($artwork);

        $filterParts = [
            'is_public = true',
            'is_approved = true',
            'id != ' . $artwork->id,
            'author_id != ' . $artwork->user_id,
        ];

        if ($tagSlugs !== []) {
            $tagFilter = implode(' OR ', array_map(
                fn (string $t): string => 'tags = "' . addslashes($t) . '"',
                $tagSlugs
            ));
            $filterParts[] = '(' . $tagFilter . ')';
        } elseif ($categorySlugs !== []) {
            $catFilter = implode(' OR ', array_map(
                fn (string $c): string => '(category = "' . addslashes($c) . '" OR categories = "' . addslashes($c) . '")',
                $categorySlugs
            ));
            $filterParts[] = '(' . $catFilter . ')';
        }

        $results = Artwork::search('')
            ->options([
                'filter' => implode(' AND ', $filterParts),
                'sort'   => ['trending_score_7d:desc', 'created_at:desc'],
            ])
            ->paginate(200, 'page', 1);

        $collection = $results->getCollection();
        $collection->load(['tags:id,slug', 'stats', 'user:id,name', 'user.profile:user_id,avatar_hash']);

        $srcTagSet = array_flip($tagSlugs);
        $srcW      = (int) ($artwork->width  ?? 0);
        $srcH      = (int) ($artwork->height ?? 0);

        $scored = $collection->map(function (Artwork $candidate) use (
            $srcTagSet, $tagSlugs, $srcOrientation, $srcW, $srcH
        ): array {
            $cTagSlugs  = $candidate->tags->pluck('slug')->all();
            $cTagSet    = array_flip($cTagSlugs);

            $common     = count(array_intersect_key($srcTagSet, $cTagSet));
            $total      = max(1, count($srcTagSet) + count($cTagSet) - $common);
            $tagOverlap = $common / $total;

            $orientBonus = $this->orientation($candidate) === $srcOrientation ? 0.10 : 0.0;

            $cW = (int) ($candidate->width  ?? 0);
            $cH = (int) ($candidate->height ?? 0);
            $resBonus = ($srcW > 0 && $srcH > 0 && $cW > 0 && $cH > 0
                && abs($cW - $srcW) / $srcW <= 0.25
                && abs($cH - $srcH) / $srcH <= 0.25
            ) ? 0.05 : 0.0;

            $views      = max(0, (int) ($candidate->stats?->views ?? 0));
            $popularity = min(0.15, log(1 + $views) / 13.0);

            $publishedAt = $candidate->published_at ?? $candidate->created_at ?? now();
            $ageDays     = max(0.0, (float) $publishedAt->diffInSeconds(now()) / 86400);
            $freshness   = exp(-$ageDays / 60.0) * 0.10;

            $score = $tagOverlap * 0.60
                   + $orientBonus
                   + $resBonus
                   + $popularity
                   + $freshness;

            return ['score' => $score, 'artwork' => $candidate];
        })->all();

        usort($scored, fn ($a, $b) => $b['score'] <=> $a['score']);

        return array_values(
            array_map(fn (array $item): array => array_merge(
                $this->formatArtwork($item['artwork']),
                ['score' => round((float) $item['score'], 5)]
            ), array_slice($scored, 0, self::LIMIT))
        );
    }

    private function orientation(Artwork $artwork): string
    {
        if (! $artwork->width || ! $artwork->height) {
            return 'square';
        }

        return match (true) {
            $artwork->width > $artwork->height => 'landscape',
            $artwork->height > $artwork->width => 'portrait',
            default => 'square',
        };
    }
}
