<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Collection;
use App\Models\CollectionSurfaceDefinition;
use App\Models\CollectionSurfacePlacement;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection as SupportCollection;

class CollectionSurfaceService
{
    public function definitions(): SupportCollection
    {
        return CollectionSurfaceDefinition::query()->orderBy('surface_key')->get();
    }

    public function placements(?string $surfaceKey = null): SupportCollection
    {
        $query = CollectionSurfacePlacement::query()
            ->with([
                'collection.user:id,username,name',
                'collection.coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
            ])
            ->orderBy('surface_key')
            ->orderByDesc('priority')
            ->orderBy('id');

        if ($surfaceKey !== null && $surfaceKey !== '') {
            $query->where('surface_key', $surfaceKey);
        }

        return $query->get();
    }

    public function placementConflicts(?string $surfaceKey = null): SupportCollection
    {
        return $this->placements($surfaceKey)
            ->where('is_active', true)
            ->groupBy('surface_key')
            ->flatMap(function (SupportCollection $placements, string $key): array {
                $conflicts = [];
                $values = $placements->values();
                $count = $values->count();

                for ($leftIndex = 0; $leftIndex < $count; $leftIndex++) {
                    $left = $values[$leftIndex];

                    for ($rightIndex = $leftIndex + 1; $rightIndex < $count; $rightIndex++) {
                        $right = $values[$rightIndex];

                        if (! $this->placementsOverlap($left, $right)) {
                            continue;
                        }

                        $conflicts[] = [
                            'surface_key' => $key,
                            'placement_ids' => [(int) $left->id, (int) $right->id],
                            'collection_ids' => [(int) $left->collection_id, (int) $right->collection_id],
                            'collection_titles' => [
                                $left->collection?->title ?? 'Unknown collection',
                                $right->collection?->title ?? 'Unknown collection',
                            ],
                            'summary' => sprintf(
                                '%s overlaps with %s on %s.',
                                $left->collection?->title ?? 'Unknown collection',
                                $right->collection?->title ?? 'Unknown collection',
                                $key,
                            ),
                            'window' => [
                                'starts_at' => $this->earliestStart($left, $right)?->toISOString(),
                                'ends_at' => $this->latestEnd($left, $right)?->toISOString(),
                            ],
                        ];
                    }
                }

                return $conflicts;
            })
            ->values();
    }

    public function upsertDefinition(array $attributes): CollectionSurfaceDefinition
    {
        return CollectionSurfaceDefinition::query()->updateOrCreate(
            ['surface_key' => (string) $attributes['surface_key']],
            [
                'title' => (string) $attributes['title'],
                'description' => $attributes['description'] ?? null,
                'mode' => (string) ($attributes['mode'] ?? 'manual'),
                'rules_json' => $attributes['rules_json'] ?? null,
                'ranking_mode' => (string) ($attributes['ranking_mode'] ?? 'ranking_score'),
                'max_items' => (int) ($attributes['max_items'] ?? 12),
                'is_active' => (bool) ($attributes['is_active'] ?? true),
                'starts_at' => $attributes['starts_at'] ?? null,
                'ends_at' => $attributes['ends_at'] ?? null,
                'fallback_surface_key' => $attributes['fallback_surface_key'] ?? null,
            ]
        );
    }

    public function populateSurface(string $surfaceKey, int $fallbackLimit = 12): SupportCollection
    {
        return $this->resolveSurfaceItems($surfaceKey, $fallbackLimit);
    }

    public function resolveSurfaceItems(string $surfaceKey, int $fallbackLimit = 12): SupportCollection
    {
        return $this->resolveSurfaceItemsInternal($surfaceKey, $fallbackLimit, []);
    }

    public function syncPlacements(): int
    {
        return CollectionSurfacePlacement::query()
            ->where('is_active', true)
            ->where(function ($query): void {
                $query->whereNotNull('ends_at')->where('ends_at', '<=', now());
            })
            ->update([
                'is_active' => false,
                'updated_at' => now(),
            ]);
    }

    public function upsertPlacement(array $attributes): CollectionSurfacePlacement
    {
        $placementId = isset($attributes['id']) ? (int) $attributes['id'] : null;

        $payload = [
            'surface_key' => (string) $attributes['surface_key'],
            'collection_id' => (int) $attributes['collection_id'],
            'placement_type' => (string) ($attributes['placement_type'] ?? 'manual'),
            'priority' => (int) ($attributes['priority'] ?? 0),
            'starts_at' => $attributes['starts_at'] ?? null,
            'ends_at' => $attributes['ends_at'] ?? null,
            'is_active' => (bool) ($attributes['is_active'] ?? true),
            'campaign_key' => $attributes['campaign_key'] ?? null,
            'notes' => $attributes['notes'] ?? null,
            'created_by_user_id' => isset($attributes['created_by_user_id']) ? (int) $attributes['created_by_user_id'] : null,
        ];

        if ($placementId) {
            $placement = CollectionSurfacePlacement::query()->findOrFail($placementId);
            $placement->fill($payload)->save();

            return $placement->refresh();
        }

        return CollectionSurfacePlacement::query()->create($payload);
    }

    public function deletePlacement(CollectionSurfacePlacement $placement): void
    {
        $placement->delete();
    }

    private function resolveSurfaceItemsInternal(string $surfaceKey, int $fallbackLimit, array $visited): SupportCollection
    {
        if (in_array($surfaceKey, $visited, true)) {
            return collect();
        }

        $visited[] = $surfaceKey;
        $definition = CollectionSurfaceDefinition::query()->where('surface_key', $surfaceKey)->first();
        $limit = max(1, min((int) ($definition?->max_items ?? $fallbackLimit), 24));
        $mode = (string) ($definition?->mode ?? 'manual');

        if ($definition && ! $this->definitionIsActive($definition)) {
            return $this->resolveFallbackSurface($definition, $fallbackLimit, $visited);
        }

        $manual = CollectionSurfacePlacement::query()
            ->with([
                'collection.user:id,username,name',
                'collection.coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
            ])
            ->where('surface_key', $surfaceKey)
            ->where('is_active', true)
            ->where(function ($query): void {
                $query->whereNull('starts_at')->orWhere('starts_at', '<=', now());
            })
            ->where(function ($query): void {
                $query->whereNull('ends_at')->orWhere('ends_at', '>', now());
            })
            ->orderByDesc('priority')
            ->orderBy('id')
            ->limit($limit)
            ->get()
            ->pluck('collection')
            ->filter(fn (?Collection $collection) => $collection && $collection->isFeatureablePublicly())
            ->values();

        if ($mode === 'manual') {
            return $manual->isEmpty()
                ? $this->resolveFallbackSurface($definition, $fallbackLimit, $visited)
                : $manual;
        }

        $query = Collection::query()->publicEligible();
        $rules = is_array($definition?->rules_json) ? $definition->rules_json : [];

        $this->applyAutomaticRules($query, $rules);

        $rankingMode = (string) ($definition?->ranking_mode ?? 'ranking_score');
        if ($rankingMode === 'recent_activity') {
            $query->orderByDesc('last_activity_at');
        } elseif ($rankingMode === 'quality_score') {
            $query->orderByDesc('quality_score');
        } else {
            $query->orderByDesc('ranking_score');
        }

        $auto = $query
            ->when($mode === 'hybrid', fn ($builder) => $builder->whereNotIn('id', $manual->pluck('id')->all()))
            ->with([
                'user:id,username,name',
                'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
            ])
            ->limit($mode === 'hybrid' ? max(0, $limit - $manual->count()) : $limit)
            ->get();

        if ($mode === 'automatic') {
            return $auto->isEmpty()
                ? $this->resolveFallbackSurface($definition, $fallbackLimit, $visited)
                : $auto->values();
        }

        if ($manual->count() >= $limit) {
            return $manual;
        }

        $resolved = $manual->concat($auto)->values();

        return $resolved->isEmpty()
            ? $this->resolveFallbackSurface($definition, $fallbackLimit, $visited)
            : $resolved;
    }

    private function applyAutomaticRules(Builder $query, array $rules): void
    {
        $this->applyExactOrListRule($query, 'type', $rules['type'] ?? null);
        $this->applyExactOrListRule($query, 'campaign_key', $rules['campaign_key'] ?? null);
        $this->applyExactOrListRule($query, 'event_key', $rules['event_key'] ?? null);
        $this->applyExactOrListRule($query, 'season_key', $rules['season_key'] ?? null);
        $this->applyExactOrListRule($query, 'presentation_style', $rules['presentation_style'] ?? null);
        $this->applyExactOrListRule($query, 'theme_token', $rules['theme_token'] ?? null);
        $this->applyExactOrListRule($query, 'collaboration_mode', $rules['collaboration_mode'] ?? null);
        $this->applyExactOrListRule($query, 'promotion_tier', $rules['promotion_tier'] ?? null);

        if ($this->ruleEnabled($rules['featured_only'] ?? false)) {
            $query->where('is_featured', true);
        }

        if ($this->ruleEnabled($rules['commercial_eligible_only'] ?? false)) {
            $query->where('commercial_eligibility', true);
        }

        if ($this->ruleEnabled($rules['analytics_enabled_only'] ?? false)) {
            $query->where('analytics_enabled', true);
        }

        if (($minQualityScore = $this->numericRule($rules['min_quality_score'] ?? null)) !== null) {
            $query->where('quality_score', '>=', $minQualityScore);
        }

        if (($minRankingScore = $this->numericRule($rules['min_ranking_score'] ?? null)) !== null) {
            $query->where('ranking_score', '>=', $minRankingScore);
        }

        $includeIds = $this->integerRuleList($rules['include_collection_ids'] ?? null);
        if ($includeIds !== []) {
            $query->whereIn('id', $includeIds);
        }

        $excludeIds = $this->integerRuleList($rules['exclude_collection_ids'] ?? null);
        if ($excludeIds !== []) {
            $query->whereNotIn('id', $excludeIds);
        }

        $ownerUsernames = $this->stringRuleList($rules['owner_usernames'] ?? ($rules['owner_username'] ?? null));
        if ($ownerUsernames !== []) {
            $normalized = array_map(static fn (string $value): string => mb_strtolower($value), $ownerUsernames);
            $query->whereHas('user', function (Builder $builder) use ($normalized): void {
                $builder->whereIn('username', $normalized);
            });
        }
    }

    private function applyExactOrListRule(Builder $query, string $column, mixed $value): void
    {
        $values = $this->stringRuleList($value);

        if ($values === []) {
            return;
        }

        if (count($values) === 1) {
            $query->where($column, $values[0]);

            return;
        }

        $query->whereIn($column, $values);
    }

    private function stringRuleList(mixed $value): array
    {
        $values = is_array($value) ? $value : [$value];

        return array_values(array_unique(array_filter(array_map(static function ($item): ?string {
            if (! is_string($item) && ! is_numeric($item)) {
                return null;
            }

            $normalized = trim((string) $item);

            return $normalized !== '' ? $normalized : null;
        }, $values))));
    }

    private function integerRuleList(mixed $value): array
    {
        $values = is_array($value) ? $value : [$value];

        return array_values(array_unique(array_filter(array_map(static function ($item): ?int {
            if (! is_numeric($item)) {
                return null;
            }

            $normalized = (int) $item;

            return $normalized > 0 ? $normalized : null;
        }, $values))));
    }

    private function numericRule(mixed $value): ?float
    {
        if (! is_numeric($value)) {
            return null;
        }

        return (float) $value;
    }

    private function ruleEnabled(mixed $value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        if (is_numeric($value)) {
            return (int) $value === 1;
        }

        if (is_string($value)) {
            return in_array(mb_strtolower(trim($value)), ['1', 'true', 'yes', 'on'], true);
        }

        return false;
    }

    private function definitionIsActive(CollectionSurfaceDefinition $definition): bool
    {
        if (! $definition->is_active) {
            return false;
        }

        if ($definition->starts_at && $definition->starts_at->isFuture()) {
            return false;
        }

        if ($definition->ends_at && $definition->ends_at->lessThanOrEqualTo(now())) {
            return false;
        }

        return true;
    }

    private function resolveFallbackSurface(?CollectionSurfaceDefinition $definition, int $fallbackLimit, array $visited): SupportCollection
    {
        $fallbackKey = $definition?->fallback_surface_key;

        if (! is_string($fallbackKey) || trim($fallbackKey) === '') {
            return collect();
        }

        return $this->resolveSurfaceItemsInternal(trim($fallbackKey), $fallbackLimit, $visited);
    }

    private function placementsOverlap(CollectionSurfacePlacement $left, CollectionSurfacePlacement $right): bool
    {
        $leftStart = $left->starts_at?->getTimestamp() ?? PHP_INT_MIN;
        $leftEnd = $left->ends_at?->getTimestamp() ?? PHP_INT_MAX;
        $rightStart = $right->starts_at?->getTimestamp() ?? PHP_INT_MIN;
        $rightEnd = $right->ends_at?->getTimestamp() ?? PHP_INT_MAX;

        return $leftStart < $rightEnd && $rightStart < $leftEnd;
    }

    private function earliestStart(CollectionSurfacePlacement $left, CollectionSurfacePlacement $right)
    {
        if ($left->starts_at === null) {
            return $right->starts_at;
        }

        if ($right->starts_at === null) {
            return $left->starts_at;
        }

        return $left->starts_at->lessThanOrEqualTo($right->starts_at) ? $left->starts_at : $right->starts_at;
    }

    private function latestEnd(CollectionSurfacePlacement $left, CollectionSurfacePlacement $right)
    {
        if ($left->ends_at === null || $right->ends_at === null) {
            return null;
        }

        return $left->ends_at->greaterThanOrEqualTo($right->ends_at) ? $left->ends_at : $right->ends_at;
    }
}
