<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Artwork;
use App\Models\Collection;
use App\Models\User;
use App\Services\Maturity\ArtworkMaturityService;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class SmartCollectionService
{
    public function __construct(private readonly ArtworkMaturityService $maturity)
    {
    }

    public function sanitizeRules(?array $input): ?array
    {
        if ($input === null) {
            return null;
        }

        $match = strtolower((string) ($input['match'] ?? 'all'));
        $sort = strtolower((string) ($input['sort'] ?? Collection::SORT_NEWEST));
        $rules = array_values(array_filter((array) ($input['rules'] ?? []), static fn ($rule) => is_array($rule)));

        $allowedMatch = config('collections.smart_rules.allowed_match', ['all', 'any']);
        $allowedSort = config('collections.smart_rules.allowed_sort', ['newest', 'oldest', 'popular']);
        $allowedFields = config('collections.smart_rules.allowed_fields', []);
        $maxRules = (int) config('collections.smart_rules.max_rules', 8);

        if (! in_array($match, $allowedMatch, true)) {
            throw ValidationException::withMessages([
                'smart_rules_json.match' => 'Smart collections must use either all or any matching.',
            ]);
        }

        if (! in_array($sort, $allowedSort, true)) {
            throw ValidationException::withMessages([
                'smart_rules_json.sort' => 'Choose a supported sort for the smart collection.',
            ]);
        }

        if ($rules === []) {
            throw ValidationException::withMessages([
                'smart_rules_json.rules' => 'Add at least one smart collection rule.',
            ]);
        }

        if (count($rules) > $maxRules) {
            throw ValidationException::withMessages([
                'smart_rules_json.rules' => 'Too many smart collection rules were submitted.',
            ]);
        }

        $sanitized = [];

        foreach ($rules as $index => $rule) {
            $field = strtolower(trim((string) ($rule['field'] ?? '')));
            $operator = strtolower(trim((string) ($rule['operator'] ?? '')));
            $value = $rule['value'] ?? null;

            if (! in_array($field, $allowedFields, true)) {
                throw ValidationException::withMessages([
                    "smart_rules_json.rules.$index.field" => 'This smart collection field is not supported.',
                ]);
            }

            $sanitized[] = [
                'field' => $field,
                'operator' => $this->validateOperator($field, $operator, $index),
                'value' => $this->sanitizeValue($field, $value, $index),
            ];
        }

        return [
            'match' => $match,
            'sort' => $sort,
            'rules' => $sanitized,
        ];
    }

    public function resolveArtworks(Collection $collection, bool $ownerView, int $perPage = 24): LengthAwarePaginator
    {
        $query = $this->queryForCollection($collection, $ownerView)
            ->with($this->artworkRelations())
            ->select('artworks.*');

        return $query->paginate($perPage)->withQueryString();
    }

    public function preview(User $owner, array $rules, bool $ownerView = true, int $perPage = 12): LengthAwarePaginator
    {
        $query = $this->queryForOwner($owner, $rules, $ownerView)
            ->with($this->artworkRelations())
            ->select('artworks.*');

        return $query->paginate($perPage)->withQueryString();
    }

    public function countMatching(Collection|User $subject, ?array $rules = null, bool $ownerView = true): int
    {
        $query = $subject instanceof Collection
            ? $this->queryForCollection($subject, $ownerView)
            : $this->queryForOwner($subject, $rules, $ownerView);

        return (int) $query->toBase()->getCountForPagination();
    }

    public function firstArtwork(Collection $collection, bool $ownerView): ?Artwork
    {
        return $this->queryForCollection($collection, $ownerView)
            ->with($this->artworkRelations())
            ->select('artworks.*')
            ->first();
    }

    public function smartSummary(?array $rules): ?string
    {
        if (! is_array($rules) || empty($rules['rules'])) {
            return null;
        }

        $glue = ($rules['match'] ?? 'all') === 'any' ? ' or ' : ' and ';
        $parts = [];

        foreach ((array) $rules['rules'] as $rule) {
            $field = $rule['field'] ?? null;
            $value = $rule['value'] ?? null;

            if ($field === 'created_at' && is_array($value)) {
                $from = $value['from'] ?? null;
                $to = $value['to'] ?? null;
                if ($from && $to) {
                    $parts[] = sprintf('created between %s and %s', $from, $to);
                }
                continue;
            }

            if ($field === 'is_featured') {
                $parts[] = ((bool) $value) ? 'marked as featured artworks' : 'not marked as featured artworks';
                continue;
            }

            if ($field === 'is_mature') {
                $parts[] = ((bool) $value) ? 'marked as mature artworks' : 'not marked as mature artworks';
                continue;
            }

            if (is_string($value) && $value !== '') {
                $label = match ($field) {
                    'tags' => 'tagged ' . $value,
                    'category' => 'in category ' . $value,
                    'subcategory' => 'in subcategory ' . $value,
                    'medium' => 'in medium ' . $value,
                    'style' => 'matching style ' . $value,
                    'color' => 'using color palette ' . $value,
                    'ai_tag' => 'matching AI tag ' . $value,
                    default => $value,
                };
                $parts[] = $label;
            }
        }

        if ($parts === []) {
            return null;
        }

        return 'Includes artworks ' . implode($glue, $parts) . '.';
    }

    public function ruleOptionsForOwner(User $owner): array
    {
        $tagOptions = DB::table('artwork_tag as at')
            ->join('artworks as a', 'a.id', '=', 'at.artwork_id')
            ->join('tags as t', 't.id', '=', 'at.tag_id')
            ->where('a.user_id', $owner->id)
            ->whereNull('a.deleted_at')
            ->where('t.is_active', true)
            ->groupBy('t.slug', 't.name')
            ->orderByRaw('COUNT(*) DESC')
            ->limit(20)
            ->get(['t.slug', 't.name'])
            ->map(fn ($row) => ['value' => $row->slug, 'label' => $row->name])
            ->all();

        $rootCategories = DB::table('artwork_category as ac')
            ->join('artworks as a', 'a.id', '=', 'ac.artwork_id')
            ->join('categories as c', 'c.id', '=', 'ac.category_id')
            ->where('a.user_id', $owner->id)
            ->whereNull('a.deleted_at')
            ->where('c.is_active', true)
            ->whereNull('c.parent_id')
            ->groupBy('c.slug', 'c.name')
            ->orderByRaw('COUNT(*) DESC')
            ->limit(20)
            ->get(['c.slug', 'c.name'])
            ->map(fn ($row) => ['value' => $row->slug, 'label' => $row->name])
            ->all();

        $subcategories = DB::table('artwork_category as ac')
            ->join('artworks as a', 'a.id', '=', 'ac.artwork_id')
            ->join('categories as c', 'c.id', '=', 'ac.category_id')
            ->where('a.user_id', $owner->id)
            ->whereNull('a.deleted_at')
            ->where('c.is_active', true)
            ->whereNotNull('c.parent_id')
            ->groupBy('c.slug', 'c.name')
            ->orderByRaw('COUNT(*) DESC')
            ->limit(20)
            ->get(['c.slug', 'c.name'])
            ->map(fn ($row) => ['value' => $row->slug, 'label' => $row->name])
            ->all();

        $mediumOptions = DB::table('artwork_category as ac')
            ->join('artworks as a', 'a.id', '=', 'ac.artwork_id')
            ->join('categories as c', 'c.id', '=', 'ac.category_id')
            ->join('content_types as ct', 'ct.id', '=', 'c.content_type_id')
            ->where('a.user_id', $owner->id)
            ->whereNull('a.deleted_at')
            ->groupBy('ct.slug', 'ct.name')
            ->orderBy('ct.name')
            ->get(['ct.slug', 'ct.name'])
            ->map(fn ($row) => ['value' => $row->slug, 'label' => $row->name])
            ->all();

        $styleOptions = $this->aiTagOptionsForOwner($owner, (array) config('collections.smart_rules.style_terms', []));
        $colorOptions = $this->aiTagOptionsForOwner($owner, (array) config('collections.smart_rules.color_terms', []));

        return [
            'fields' => [
                ['value' => 'tags', 'label' => 'Tag'],
                ['value' => 'category', 'label' => 'Category'],
                ['value' => 'subcategory', 'label' => 'Subcategory'],
                ['value' => 'medium', 'label' => 'Medium'],
                ['value' => 'style', 'label' => 'Style'],
                ['value' => 'color', 'label' => 'Color palette'],
                ['value' => 'ai_tag', 'label' => 'AI tag'],
                ['value' => 'created_at', 'label' => 'Created date'],
                ['value' => 'is_featured', 'label' => 'Featured artwork'],
                ['value' => 'is_mature', 'label' => 'Mature artwork'],
            ],
            'tag_options' => $tagOptions,
            'category_options' => $rootCategories,
            'subcategory_options' => $subcategories,
            'medium_options' => $mediumOptions,
            'style_options' => $styleOptions,
            'color_options' => $colorOptions,
            'sort_options' => [
                ['value' => 'newest', 'label' => 'Newest first'],
                ['value' => 'oldest', 'label' => 'Oldest first'],
                ['value' => 'popular', 'label' => 'Most viewed'],
            ],
        ];
    }

    public function queryForCollection(Collection $collection, bool $ownerView): Builder
    {
        return $this->queryForOwner($collection->user, $collection->smart_rules_json, $ownerView);
    }

    public function queryForOwner(User $owner, ?array $rules, bool $ownerView): Builder
    {
        if ($rules === null) {
            throw ValidationException::withMessages([
                'smart_rules_json' => 'Smart collections require at least one valid rule.',
            ]);
        }

        $sanitized = $this->sanitizeRules($rules);
        $query = Artwork::query()
            ->where('artworks.user_id', $owner->id)
            ->whereNull('artworks.deleted_at');

        if (! $ownerView) {
            $query->where('artworks.is_public', true)
                ->where('artworks.is_approved', true)
                ->whereNotNull('artworks.published_at')
                ->where('artworks.published_at', '<=', now());

            $this->maturity->applyViewerFilter($query, request()->user());
        }

        $method = ($sanitized['match'] ?? 'all') === 'any' ? 'orWhere' : 'where';

        $query->where(function (Builder $outer) use ($sanitized, $method): void {
            foreach ((array) ($sanitized['rules'] ?? []) as $index => $rule) {
                $callback = function (Builder $builder) use ($rule): void {
                    $this->applyRule($builder, $rule);
                };

                if ($index === 0 || $method === 'where') {
                    $outer->where($callback);
                } else {
                    $outer->orWhere($callback);
                }
            }
        });

        return $this->applySort($query, (string) ($sanitized['sort'] ?? Collection::SORT_NEWEST));
    }

    /**
     * @return array<int, string|array>
     */
    private function artworkRelations(): array
    {
        return [
            'user:id,name,username',
            'stats:artwork_id,views,downloads,favorites',
            'categories' => function ($query) {
                $query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order')
                    ->with(['contentType:id,slug,name']);
            },
        ];
    }

    /**
     * @param array{field:string,operator:string,value:mixed} $rule
     */
    private function applyRule(Builder $query, array $rule): void
    {
        $field = $rule['field'];
        $value = $rule['value'];

        match ($field) {
            'tags' => $query->whereHas('tags', function (Builder $builder) use ($value): void {
                $builder->where('tags.slug', (string) $value)
                    ->orWhere('tags.name', 'like', '%' . (string) $value . '%');
            }),
            'category' => $query->whereHas('categories', function (Builder $builder) use ($value): void {
                $builder->where('categories.slug', (string) $value)
                    ->whereNull('categories.parent_id');
            }),
            'subcategory' => $query->whereHas('categories', function (Builder $builder) use ($value): void {
                $builder->where('categories.slug', (string) $value)
                    ->whereNotNull('categories.parent_id');
            }),
            'medium' => $query->whereHas('categories.contentType', function (Builder $builder) use ($value): void {
                $builder->where('content_types.slug', (string) $value);
            }),
            'style' => $this->applyAiPivotTagRule($query, (string) $value),
            'color' => $this->applyAiPivotTagRule($query, (string) $value),
            'ai_tag' => $query->where(function (Builder $builder) use ($value): void {
                $builder->where('artworks.blip_caption', 'like', '%' . (string) $value . '%')
                    ->orWhere('artworks.clip_tags_json', 'like', '%' . (string) $value . '%')
                    ->orWhere('artworks.yolo_objects_json', 'like', '%' . (string) $value . '%');
            }),
            'created_at' => $query->whereBetween('artworks.created_at', [
                Carbon::parse((string) ($value['from'] ?? now()->subYear()->toDateString()))->startOfDay(),
                Carbon::parse((string) ($value['to'] ?? now()->toDateString()))->endOfDay(),
            ]),
            'is_featured' => $this->applyFeaturedRule($query, (bool) $value),
            'is_mature' => $query->where('artworks.is_mature', (bool) $value),
            default => null,
        };
    }

    private function applyFeaturedRule(Builder $query, bool $value): void
    {
        if ($value) {
            $query->whereExists(function ($sub): void {
                $sub->selectRaw('1')
                    ->from('artwork_features as af')
                    ->whereColumn('af.artwork_id', 'artworks.id')
                    ->where('af.is_active', true)
                    ->whereNull('af.deleted_at');
            });

            return;
        }

        $query->whereNotExists(function ($sub): void {
            $sub->selectRaw('1')
                ->from('artwork_features as af')
                ->whereColumn('af.artwork_id', 'artworks.id')
                ->where('af.is_active', true)
                ->whereNull('af.deleted_at');
        });
    }

    private function applySort(Builder $query, string $sort): Builder
    {
        return match ($sort) {
            Collection::SORT_OLDEST => $query->orderBy('artworks.published_at')->orderBy('artworks.id'),
            Collection::SORT_POPULAR => $query->orderByDesc('artworks.view_count')->orderByDesc('artworks.id'),
            default => $query->orderByDesc('artworks.published_at')->orderByDesc('artworks.id'),
        };
    }

    private function validateOperator(string $field, string $operator, int $index): string
    {
        $allowed = match ($field) {
            'created_at' => ['between'],
            'is_featured', 'is_mature' => ['equals'],
            default => ['contains', 'equals'],
        };

        if (! in_array($operator, $allowed, true)) {
            throw ValidationException::withMessages([
                "smart_rules_json.rules.$index.operator" => 'This operator is not supported for the selected field.',
            ]);
        }

        return $operator;
    }

    private function sanitizeValue(string $field, mixed $value, int $index): mixed
    {
        if ($field === 'created_at') {
            if (! is_array($value) || empty($value['from']) || empty($value['to'])) {
                throw ValidationException::withMessages([
                    "smart_rules_json.rules.$index.value" => 'Provide a valid start and end date for this rule.',
                ]);
            }

            return [
                'from' => Carbon::parse((string) $value['from'])->toDateString(),
                'to' => Carbon::parse((string) $value['to'])->toDateString(),
            ];
        }

        if ($field === 'is_featured' || $field === 'is_mature') {
            return (bool) $value;
        }

        $stringValue = trim((string) $value);
        if ($stringValue === '' || mb_strlen($stringValue) > 80) {
            throw ValidationException::withMessages([
                "smart_rules_json.rules.$index.value" => 'Provide a shorter value for this smart rule.',
            ]);
        }

        return $stringValue;
    }

    private function applyAiPivotTagRule(Builder $query, string $value): void
    {
        $query->whereHas('tags', function (Builder $builder) use ($value): void {
            $builder->where('artwork_tag.source', 'ai')
                ->where(function (Builder $match) use ($value): void {
                    $match->where('tags.slug', str($value)->slug()->value())
                        ->orWhere('tags.name', 'like', '%' . $value . '%');
                });
        });
    }

    /**
     * @param array<int, string> $allowedTerms
     * @return array<int, array{value:string,label:string}>
     */
    private function aiTagOptionsForOwner(User $owner, array $allowedTerms): array
    {
        if ($allowedTerms === []) {
            return [];
        }

        $allowedSlugs = collect($allowedTerms)
            ->map(static fn (string $term) => str($term)->slug()->value())
            ->filter()
            ->unique()
            ->values();

        if ($allowedSlugs->isEmpty()) {
            return [];
        }

        return DB::table('artwork_tag as at')
            ->join('artworks as a', 'a.id', '=', 'at.artwork_id')
            ->join('tags as t', 't.id', '=', 'at.tag_id')
            ->where('a.user_id', $owner->id)
            ->whereNull('a.deleted_at')
            ->where('at.source', 'ai')
            ->whereIn('t.slug', $allowedSlugs->all())
            ->groupBy('t.slug', 't.name')
            ->orderByRaw('COUNT(*) DESC')
            ->orderBy('t.name')
            ->get(['t.slug', 't.name'])
            ->map(fn ($row) => ['value' => $row->slug, 'label' => $row->name])
            ->all();
    }
}
