<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Collection;
use App\Models\CollectionMergeAction;
use App\Models\User;
use App\Services\CollectionCanonicalService;
use App\Services\CollectionHealthService;
use App\Services\CollectionHistoryService;
use App\Services\CollectionService;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class CollectionMergeService
{
    public function __construct(
        private readonly CollectionCanonicalService $canonical,
        private readonly CollectionService $collections,
    ) {
    }

    public function duplicateCandidates(Collection $collection, int $limit = 5): EloquentCollection
    {
        $normalizedTitle = mb_strtolower(trim((string) $collection->title));

        return Collection::query()
            ->where('id', '!=', $collection->id)
            ->whereNotExists(function ($query) use ($collection): void {
                $query->select(DB::raw('1'))
                    ->from('collection_merge_actions as cma')
                    ->where('cma.action_type', 'rejected')
                    ->where(function ($pair) use ($collection): void {
                        $pair->where(function ($forward) use ($collection): void {
                            $forward->where('cma.source_collection_id', $collection->id)
                                ->whereColumn('cma.target_collection_id', 'collections.id');
                        })->orWhere(function ($reverse) use ($collection): void {
                            $reverse->where('cma.target_collection_id', $collection->id)
                                ->whereColumn('cma.source_collection_id', 'collections.id');
                        });
                    });
            })
            ->where(function ($query) use ($collection, $normalizedTitle): void {
                $query->where('user_id', $collection->user_id)
                    ->orWhere(function ($inner) use ($collection, $normalizedTitle): void {
                        $inner->whereRaw('LOWER(title) = ?', [$normalizedTitle])
                            ->when(filled($collection->campaign_key), fn ($builder) => $builder->orWhere('campaign_key', $collection->campaign_key))
                            ->when(filled($collection->series_key), fn ($builder) => $builder->orWhere('series_key', $collection->series_key));
                    });
            })
            ->with(['user:id,username,name', 'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at'])
            ->orderByDesc('updated_at')
            ->limit(max(1, min($limit, 10)))
            ->get();
    }

    public function reviewCandidates(Collection $collection, bool $ownerView = true, int $limit = 5): array
    {
        $candidates = $this->duplicateCandidates($collection, $limit);
        $candidateIds = $candidates->pluck('id')->map(static fn ($id): int => (int) $id)->values()->all();
        $cards = collect($this->collections->mapCollectionCardPayloads($candidates, $ownerView))->keyBy('id');
        $latestActions = $this->latestActionsForPair($collection, $candidateIds);

        return $candidates->map(function (Collection $candidate) use ($collection, $cards, $latestActions): array {
            return [
                'collection' => $cards->get((int) $candidate->id),
                'comparison' => $this->comparisonForCollections($collection, $candidate),
                'decision' => $latestActions[$this->pairKey((int) $collection->id, (int) $candidate->id)] ?? null,
                'is_current_canonical_target' => (int) ($collection->canonical_collection_id ?? 0) === (int) $candidate->id,
            ];
        })->values()->all();
    }

    public function queueOverview(bool $ownerView = true, int $pendingLimit = 8, int $recentLimit = 8): array
    {
        $latestActions = CollectionMergeAction::query()
            ->with([
                'sourceCollection.user:id,username,name',
                'sourceCollection.coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
                'targetCollection.user:id,username,name',
                'targetCollection.coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
                'actor:id,username,name',
            ])
            ->orderByDesc('id')
            ->get()
            ->filter(fn (CollectionMergeAction $action): bool => $action->sourceCollection !== null && $action->targetCollection !== null)
            ->groupBy(fn (CollectionMergeAction $action): string => $this->pairKey((int) $action->source_collection_id, (int) $action->target_collection_id))
            ->map(fn (SupportCollection $actions): CollectionMergeAction => $actions->first())
            ->values();

        $pending = $latestActions
            ->filter(fn (CollectionMergeAction $action): bool => $action->action_type === 'suggested')
            ->sortByDesc(fn (CollectionMergeAction $action) => optional($action->updated_at)?->timestamp ?? 0)
            ->take($pendingLimit)
            ->values();

        $recent = $latestActions
            ->filter(fn (CollectionMergeAction $action): bool => $action->action_type !== 'suggested')
            ->sortByDesc(fn (CollectionMergeAction $action) => optional($action->updated_at)?->timestamp ?? 0)
            ->take($recentLimit)
            ->values();

        return [
            'summary' => [
                'pending' => $latestActions->where('action_type', 'suggested')->count(),
                'approved' => $latestActions->where('action_type', 'approved')->count(),
                'rejected' => $latestActions->where('action_type', 'rejected')->count(),
                'completed' => $latestActions->where('action_type', 'completed')->count(),
            ],
            'pending' => $pending->map(fn (CollectionMergeAction $action): array => $this->mapQueueAction($action, $ownerView))->values()->all(),
            'recent' => $recent->map(fn (CollectionMergeAction $action): array => $this->mapQueueAction($action, $ownerView))->values()->all(),
        ];
    }

    public function syncSuggestedCandidates(Collection $collection, ?User $actor = null, int $limit = 5): array
    {
        $candidates = $this->duplicateCandidates($collection, $limit);
        $candidateIds = $candidates->pluck('id')->map(static fn ($id): int => (int) $id)->values()->all();

        $staleSuggestions = CollectionMergeAction::query()
            ->where('source_collection_id', $collection->id)
            ->where('action_type', 'suggested');

        if ($candidateIds === []) {
            $staleSuggestions->delete();
        } else {
            $staleSuggestions->whereNotIn('target_collection_id', $candidateIds)->delete();
        }

        foreach ($candidates as $candidate) {
            CollectionMergeAction::query()->updateOrCreate(
                [
                    'source_collection_id' => $collection->id,
                    'target_collection_id' => $candidate->id,
                    'action_type' => 'suggested',
                ],
                [
                    'actor_user_id' => $actor?->id,
                    'summary' => 'Potential duplicate candidate detected.',
                ]
            );
        }

        $this->syncDuplicateClusterKeys($collection, $candidateIds);

        app(CollectionHistoryService::class)->record(
            $collection->fresh(),
            $actor,
            'duplicate_candidates_synced',
            sprintf('Collection duplicate candidates scanned. %d potential matches found.', count($candidateIds)),
            null,
            ['candidate_collection_ids' => $candidateIds]
        );

        return [
            'count' => count($candidateIds),
            'items' => $candidates->map(fn (Collection $candidate): array => [
                'id' => (int) $candidate->id,
                'title' => (string) $candidate->title,
                'slug' => (string) $candidate->slug,
            ])->values()->all(),
        ];
    }

    public function rejectCandidate(Collection $source, Collection $target, ?User $actor = null): Collection
    {
        if ((int) $source->id === (int) $target->id) {
            throw ValidationException::withMessages([
                'target_collection_id' => 'A collection cannot reject itself as a duplicate.',
            ]);
        }

        CollectionMergeAction::query()
            ->where(function ($query) use ($source, $target): void {
                $query->where(function ($forward) use ($source, $target): void {
                    $forward->where('source_collection_id', $source->id)
                        ->where('target_collection_id', $target->id);
                })->orWhere(function ($reverse) use ($source, $target): void {
                    $reverse->where('source_collection_id', $target->id)
                        ->where('target_collection_id', $source->id);
                });
            })
            ->whereIn('action_type', ['suggested'])
            ->delete();

        CollectionMergeAction::query()->updateOrCreate(
            [
                'source_collection_id' => $source->id,
                'target_collection_id' => $target->id,
                'action_type' => 'rejected',
            ],
            [
                'actor_user_id' => $actor?->id,
                'summary' => 'Marked as not a duplicate.',
            ]
        );

        app(CollectionHistoryService::class)->record(
            $source->fresh(),
            $actor,
            'duplicate_rejected',
            'Duplicate candidate dismissed.',
            null,
            ['target_collection_id' => (int) $target->id]
        );

        $this->syncDuplicateClusterKeys($source->fresh(), $this->duplicateCandidates($source->fresh())->pluck('id')->map(static fn ($id): int => (int) $id)->all());
        $this->syncDuplicateClusterKeys($target->fresh(), $this->duplicateCandidates($target->fresh())->pluck('id')->map(static fn ($id): int => (int) $id)->all());

        return app(CollectionHealthService::class)->refresh($source->fresh(), $actor, 'duplicate-rejected');
    }

    public function mergeInto(Collection $source, Collection $target, ?User $actor = null): array
    {
        if ((int) $source->id === (int) $target->id) {
            throw ValidationException::withMessages([
                'target_collection_id' => 'A collection cannot merge into itself.',
            ]);
        }

        if ($target->isSmart()) {
            throw ValidationException::withMessages([
                'target_collection_id' => 'Target collection must be manual so merged artworks can be referenced safely.',
            ]);
        }

        return DB::transaction(function () use ($source, $target, $actor): array {
            $artworkIds = $source->artworks()->pluck('artworks.id')->map(static fn ($id) => (int) $id)->all();
            $this->collections->attachArtworkIds($target, $artworkIds);

            $source = $this->canonical->designate($source->fresh(), $target->fresh(), $actor);
            $source->forceFill([
                'workflow_state' => Collection::WORKFLOW_ARCHIVED,
                'lifecycle_state' => Collection::LIFECYCLE_ARCHIVED,
                'archived_at' => now(),
                'placement_eligibility' => false,
            ])->save();

            CollectionMergeAction::query()->create([
                'source_collection_id' => $source->id,
                'target_collection_id' => $target->id,
                'action_type' => 'completed',
                'actor_user_id' => $actor?->id,
                'summary' => 'Collection merge completed.',
            ]);

            app(CollectionHistoryService::class)->record($target->fresh(), $actor, 'merged_into_target', 'Collection absorbed merge references.', null, [
                'source_collection_id' => (int) $source->id,
                'artwork_ids' => $artworkIds,
            ]);

            app(CollectionHistoryService::class)->record($source->fresh(), $actor, 'merged_into_canonical', 'Collection archived after merge.', null, [
                'target_collection_id' => (int) $target->id,
            ]);

            return [
                'source' => $source->fresh(),
                'target' => $target->fresh(),
                'attached_artwork_ids' => $artworkIds,
            ];
        });
    }

    /**
     * @param  array<int, int>  $candidateIds
     * @return array<string, array<string, mixed>>
     */
    private function latestActionsForPair(Collection $source, array $candidateIds): array
    {
        if ($candidateIds === []) {
            return [];
        }

        return CollectionMergeAction::query()
            ->where(function ($query) use ($source, $candidateIds): void {
                $query->where('source_collection_id', $source->id)
                    ->whereIn('target_collection_id', $candidateIds)
                    ->orWhere(function ($reverse) use ($source, $candidateIds): void {
                        $reverse->where('target_collection_id', $source->id)
                            ->whereIn('source_collection_id', $candidateIds);
                    });
            })
            ->orderByDesc('id')
            ->get()
            ->groupBy(function (CollectionMergeAction $action): string {
                return $this->pairKey((int) $action->source_collection_id, (int) $action->target_collection_id);
            })
            ->map(fn ($actions): array => [
                'action_type' => (string) $actions->first()->action_type,
                'summary' => $actions->first()->summary,
                'updated_at' => optional($actions->first()->updated_at)?->toISOString(),
            ])
            ->all();
    }

    private function pairKey(int $leftId, int $rightId): string
    {
        $pair = [$leftId, $rightId];
        sort($pair);

        return implode(':', $pair);
    }

    private function comparisonForCollections(Collection $source, Collection $target): array
    {
        $sourceArtworkIds = $source->artworks()->pluck('artworks.id')->map(static fn ($id): int => (int) $id)->values()->all();
        $targetArtworkIds = $target->artworks()->pluck('artworks.id')->map(static fn ($id): int => (int) $id)->values()->all();
        $sharedArtworkIds = array_values(array_intersect($sourceArtworkIds, $targetArtworkIds));
        $reasons = array_values(array_filter([
            (int) $target->user_id === (int) $source->user_id ? 'same_owner' : null,
            mb_strtolower(trim((string) $target->title)) === mb_strtolower(trim((string) $source->title)) ? 'same_title' : null,
            filled($source->campaign_key) && $target->campaign_key === $source->campaign_key ? 'same_campaign' : null,
            filled($source->series_key) && $target->series_key === $source->series_key ? 'same_series' : null,
            $sharedArtworkIds !== [] ? 'shared_artworks' : null,
        ]));

        return [
            'same_owner' => (int) $target->user_id === (int) $source->user_id,
            'same_title' => mb_strtolower(trim((string) $target->title)) === mb_strtolower(trim((string) $source->title)),
            'same_campaign' => filled($source->campaign_key) && $target->campaign_key === $source->campaign_key,
            'same_series' => filled($source->series_key) && $target->series_key === $source->series_key,
            'shared_artworks_count' => count($sharedArtworkIds),
            'source_artworks_count' => count($sourceArtworkIds),
            'target_artworks_count' => count($targetArtworkIds),
            'match_reasons' => $reasons,
        ];
    }

    /**
     * @param  array<int, int>  $candidateIds
     */
    private function syncDuplicateClusterKeys(Collection $collection, array $candidateIds): void
    {
        $clusterIds = collect([$collection->id])
            ->merge($candidateIds)
            ->map(static fn ($id): int => (int) $id)
            ->unique()
            ->values();

        if ($clusterIds->count() <= 1) {
            Collection::query()
                ->where('id', $collection->id)
                ->whereNull('canonical_collection_id')
                ->update(['duplicate_cluster_key' => null]);

            return;
        }

        $clusterKey = sprintf('dup:%d:%d', $clusterIds->min(), $clusterIds->count());

        Collection::query()
            ->whereIn('id', $clusterIds->all())
            ->whereNull('canonical_collection_id')
            ->update(['duplicate_cluster_key' => $clusterKey]);
    }

    private function mapQueueAction(CollectionMergeAction $action, bool $ownerView): array
    {
        $source = $action->sourceCollection;
        $target = $action->targetCollection;

        return [
            'id' => (int) $action->id,
            'action_type' => (string) $action->action_type,
            'summary' => $action->summary,
            'updated_at' => optional($action->updated_at)?->toISOString(),
            'source' => $source ? $this->collections->mapCollectionCardPayloads([$source], $ownerView)[0] : null,
            'target' => $target ? $this->collections->mapCollectionCardPayloads([$target], $ownerView)[0] : null,
            'comparison' => ($source && $target) ? $this->comparisonForCollections($source, $target) : null,
            'actor' => $action->actor ? [
                'id' => (int) $action->actor->id,
                'username' => (string) $action->actor->username,
                'name' => $action->actor->name,
            ] : null,
        ];
    }
}