<?php

namespace App\Domains\Catalog\Models;

use App\Domains\Attachable\Models\Attachment;
use App\Domains\Event\Models\EventDate;
use App\Domains\Ticket\Models\Ticket;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Str;

#[Fillable([
    'catalog_item_id',
    'event_date_id',
    'inventory_id',
    'descripcion',
    'precio',
])]
class Variant extends Model
{
    use HasFactory, SoftDeletes;

    public $timestamps = false;

    protected $table = 'variantes';

    protected function casts(): array
    {
        return [
            'catalog_item_id' => 'integer',
            'event_date_id' => 'integer',
            'inventory_id' => 'integer',
            'precio' => 'decimal:2',
        ];
    }

    /** @return BelongsTo<CatalogItem, $this> */
    public function catalogItem(): BelongsTo
    {
        return $this->belongsTo(CatalogItem::class)->withTrashed();
    }

    /** @return BelongsTo<EventDate, $this> */
    public function eventDate(): BelongsTo
    {
        return $this->belongsTo(EventDate::class);
    }

    /** @return BelongsToMany<EventDate, $this> */
    public function eventDates(): BelongsToMany
    {
        return $this->belongsToMany(
            EventDate::class,
            'variant_event_dates',
            'variant_id',
            'event_date_id',
        )->orderBy('date')->orderBy('time_start');
    }

    /** @return HasMany<Ticket, $this> */
    public function sourceTickets(): HasMany
    {
        return $this->hasMany(Ticket::class, 'source_variant_id');
    }

    /** @return BelongsTo<Inventory, $this> */
    public function inventory(): BelongsTo
    {
        return $this->belongsTo(Inventory::class);
    }

    /** @return HasMany<VariantDefinition, $this> */
    public function definitions(): HasMany
    {
        return $this->hasMany(VariantDefinition::class);
    }

    /** @return BelongsToMany<Attachment, $this> */
    public function attachments(): BelongsToMany
    {
        return $this->allAttachments()->wherePivot('is_enabled', true);
    }

    /** @return BelongsToMany<Attachment, $this> */
    public function allAttachments(): BelongsToMany
    {
        $relation = $this->belongsToMany(
            Attachment::class,
            'catalog_items_attachments',
            'variant_id',
            'attachment_id'
        )
            ->withPivot(['orden', 'is_enabled'])
            ->orderByPivot('orden');

        if ($this->catalog_item_id !== null) {
            $relation->withPivotValue('catalog_item_id', $this->catalog_item_id);
        }

        return $relation;
    }

    public function getPrice(): float
    {
        return (float) ($this->precio ?? $this->catalogItem->precio);
    }

    public function getDescription(): ?string
    {
        return $this->descripcion ?? $this->catalogItem->descripcion;
    }

    public function getName(): string
    {
        return $this->catalogItem->nombre;
    }

    public function getSelectionLabel(): string
    {
        $this->loadMissing([
            'catalogItem.itemAttributes.attribute.options',
            'definitions.itemAttribute.attribute.options',
            'eventDates',
            'eventDate',
        ]);

        $itemAttributes = $this->catalogItem->itemAttributes;

        $label = $this->selectionOptions($itemAttributes)
            ->map(function (array $option, string $attributeCode) use ($itemAttributes): ?string {
                $itemAttribute = $itemAttributes->first(
                    fn (ItemAttribute $candidate): bool => $candidate->attribute?->codigo === $attributeCode,
                );
                $translationKey = "api.catalog.attribute_labels.{$attributeCode}";
                $attributeName = Lang::has($translationKey)
                    ? __($translationKey)
                    : ($itemAttribute?->attribute?->nombre ?? Str::headline($attributeCode));
                $selectedOptions = array_is_list($option) ? $option : [$option];
                $selectedLabels = collect($selectedOptions)
                    ->pluck('label')
                    ->filter()
                    ->implode(', ');

                return $selectedLabels === ''
                    ? null
                    : "{$attributeName} {$selectedLabels}";
            })
            ->filter()
            ->implode(' · ');

        return $label !== '' ? $label : $this->getName();
    }

    /** @return Collection<string, string|array<int, string>> */
    public function selectionValues(): Collection
    {
        $values = $this->definitions
            ->groupBy('item_attribute_id')
            ->mapWithKeys(function (Collection $definitions): array {
                $itemAttribute = $definitions->first()?->itemAttribute;
                $attributeCode = $itemAttribute?->attribute?->codigo;

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

                $definitionValues = $definitions->pluck('value')->values();

                return [
                    $attributeCode => $itemAttribute->allow_multi_select
                        ? $definitionValues->all()
                        : $definitionValues->first(),
                ];
            });

        $eventDateIds = $this->selectedEventDates()
            ->pluck('id')
            ->map(fn ($id): string => (string) $id)
            ->values();

        if ($eventDateIds->count() === 1) {
            $values->put('event_date', $eventDateIds->first());
        } elseif ($eventDateIds->isNotEmpty()) {
            $values->put('event_date', $eventDateIds->all());
        }

        return $values;
    }

    /**
     * @return Collection<string, array{value: string, label: string}|array<int, array{value: string, label: string}>>
     */
    public function selectionOptions(?Collection $itemAttributes = null): Collection
    {
        $options = $this->definitions
            ->groupBy('item_attribute_id')
            ->mapWithKeys(function (Collection $definitions): array {
                $itemAttribute = $definitions->first()?->itemAttribute;
                $attribute = $itemAttribute?->attribute;
                $attributeCode = $attribute?->codigo;

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

                $values = $definitions
                    ->pluck('value')
                    ->values()
                    ->map(function (string $value) use ($attribute): array {
                        $attributeOption = $attribute->options->firstWhere('value', $value);

                        return [
                            'value' => $value,
                            'label' => $attributeOption?->label ?? $value,
                        ];
                    });

                return [
                    $attributeCode => $itemAttribute->allow_multi_select
                        ? $values->all()
                        : $values->first(),
                ];
            });

        $eventDateOptions = $this->selectedEventDates()
            ->map(fn (EventDate $eventDate): array => [
                'value' => (string) $eventDate->id,
                'label' => $eventDate->date->format('d/m/Y'),
            ])
            ->values();

        if ($eventDateOptions->count() === 1) {
            $options->put('event_date', $eventDateOptions->first());
        } elseif ($eventDateOptions->isNotEmpty()) {
            $options->put('event_date', $eventDateOptions->all());
        }

        if ($itemAttributes === null) {
            $itemAttributes = $this->definitions
                ->map(fn (VariantDefinition $definition) => $definition->itemAttribute)
                ->filter()
                ->unique('id')
                ->values();

            if ($this->catalogItem !== null) {
                $itemAttributes = $itemAttributes
                    ->merge($this->catalogItem->itemAttributes)
                    ->unique('id')
                    ->values();
            }
        }

        $ordering = $itemAttributes->mapWithKeys(function (ItemAttribute $itemAttribute): array {
            $attribute = $itemAttribute->attribute;

            return $attribute === null
                ? []
                : [$attribute->codigo => [$itemAttribute->sort_order, mb_strtolower($attribute->nombre)]];
        });

        return $options->sortKeysUsing(function (string $left, string $right) use ($ordering): int {
            [$leftOrder, $leftLabel] = $ordering->get($left, [0, mb_strtolower($left)]);
            [$rightOrder, $rightLabel] = $ordering->get($right, [0, mb_strtolower($right)]);

            return $leftOrder <=> $rightOrder
                ?: $leftLabel <=> $rightLabel
                ?: $left <=> $right;
        });
    }

    /**
     * @param  Collection<int, ItemAttribute>  $itemAttributes
     * @return Collection<string, array{value: string, label: string}|array<int, array{value: string, label: string}>>
     */
    public function selectorOptions(Collection $itemAttributes): Collection
    {
        $visibleAttributeCodes = $itemAttributes
            ->filter(fn (ItemAttribute $itemAttribute): bool => $itemAttribute->show_in_selector)
            ->map(fn (ItemAttribute $itemAttribute): ?string => $itemAttribute->attribute?->codigo)
            ->filter()
            ->values();

        return $this->selectionOptions($itemAttributes)
            ->filter(
                fn (array $option, string $attributeCode): bool => $visibleAttributeCodes
                    ->contains($attributeCode)
            );
    }

    /** @return Collection<int, EventDate> */
    public function selectedEventDates(): Collection
    {
        $eventDates = $this->eventDates;

        if ($eventDates->isEmpty() && $this->event_date_id !== null && $this->eventDate !== null) {
            return collect([$this->eventDate]);
        }

        return $eventDates;
    }
}
