<?php

namespace App\Domains\Desfile\Services;

use App\Domains\Attachable\Models\Attachment;
use App\Domains\Attachable\Services\AttachmentService;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\ItemAttribute;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Throwable;

class EntryService
{
    private const ATTRIBUTE_MAP = [
        'type' => 'tipo',
        'sector' => 'sector',
        'row' => 'fila',
        'seat' => 'asiento',
    ];

    private const ROW_ATTRIBUTE_MAP = [
        'type' => 'tipo',
        'sector' => 'sector',
        'row' => 'fila',
    ];

    public function __construct(private readonly AttachmentService $attachmentService) {}

    public function current(Tenant $tenant): CatalogItem
    {
        return $this->entryQuery($tenant)
            ->with([
                'allAttachments',
                'itemAttributes.attribute.options',
                'variants' => fn ($query) => $query->orderBy('id'),
                'variants.inventory',
                'variants.definitions.itemAttribute.attribute',
            ])
            ->firstOrFail();
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    public function syncRows(Tenant $tenant, array $rows): CatalogItem
    {
        DB::transaction(function () use ($tenant, $rows): void {
            $entry = $this->entryQuery($tenant)->lockForUpdate()->firstOrFail();
            $itemAttributes = $this->itemAttributes($entry);
            $existingVariants = $entry->variants()
                ->with(['inventory', 'definitions.itemAttribute.attribute'])
                ->lockForUpdate()
                ->get();
            $existingBySelection = $existingVariants->keyBy(
                fn (Variant $variant): string => $this->selectionKey($variant->selectionValues()->all()),
            );
            $desiredSelections = collect();

            foreach (array_values($rows) as $index => $data) {
                $rowValues = $this->resolveRowValues($itemAttributes, $data, $index);

                foreach (range(1, (int) $data['max_seat']) as $seat) {
                    $values = [
                        ...$rowValues,
                        'asiento' => $this->resolveAttributeValue(
                            $itemAttributes,
                            'asiento',
                            (string) $seat,
                            "rows.{$index}.max_seat",
                        ),
                    ];
                    $selectionKey = $this->selectionKey($values);
                    $desiredSelections->put($selectionKey, true);
                    $variant = $existingBySelection->get($selectionKey);

                    if ($variant === null) {
                        $variant = $entry->variants()->create([
                            'inventory_id' => Inventory::query()->create(['real_stock' => 1])->id,
                            'descripcion' => $this->description($values),
                            'precio' => $data['price'],
                        ]);
                        $variant->definitions()->createMany(
                            collect($values)->map(
                                fn (string $value, string $code): array => [
                                    'item_attribute_id' => $itemAttributes[$code]->id,
                                    'value' => $value,
                                ],
                            )->values()->all(),
                        );
                    } else {
                        $variant->update([
                            'descripcion' => $this->description($values),
                            'precio' => $data['price'],
                        ]);
                    }
                }
            }

            foreach ($existingVariants as $variant) {
                $selectionKey = $this->selectionKey($variant->selectionValues()->all());
                if ($desiredSelections->has($selectionKey)) {
                    continue;
                }

                $this->assertVariantCanChangeIdentity($variant, 'rows');
                $variant->delete();
            }

            $minimumPrice = $entry->variants()->min('precio');
            if ($minimumPrice !== null) {
                $entry->update(['precio' => $minimumPrice]);
            }
        });

        return $this->current($tenant);
    }

    public function replaceImage(
        Tenant $tenant,
        UploadedFile $image,
        bool $isEnabled,
    ): CatalogItem {
        $attachment = $this->attachmentService->store($image, 'catalog-items');
        $previousAttachments = collect();

        try {
            DB::transaction(function () use ($tenant, $attachment, $isEnabled, &$previousAttachments): void {
                $entry = $this->entryQuery($tenant)->lockForUpdate()->firstOrFail();
                $previousAttachments = $entry->allAttachments()->get();
                $entry->allAttachments()->sync([
                    $attachment->id => [
                        'orden' => 0,
                        'is_enabled' => $isEnabled,
                    ],
                ]);
            });
        } catch (Throwable $throwable) {
            $this->deleteAttachmentQuietly($attachment);
            throw $throwable;
        }

        $previousAttachments->each(fn (Attachment $previous) => $this->deleteIfUnused($previous));

        return $this->current($tenant);
    }

    public function updateImage(Tenant $tenant, bool $isEnabled): CatalogItem
    {
        DB::transaction(function () use ($tenant, $isEnabled): void {
            $entry = $this->entryQuery($tenant)->lockForUpdate()->firstOrFail();
            $attachment = $entry->allAttachments()->lockForUpdate()->firstOrFail();

            $entry->allAttachments()->updateExistingPivot($attachment->id, [
                'is_enabled' => $isEnabled,
            ]);
        });

        return $this->current($tenant);
    }

    /** @return Collection<string, ItemAttribute> */
    private function itemAttributes(CatalogItem $entry): Collection
    {
        $attributes = $entry->itemAttributes()
            ->with('attribute.options')
            ->get()
            ->filter(fn (ItemAttribute $itemAttribute): bool => $itemAttribute->attribute !== null)
            ->keyBy(fn (ItemAttribute $itemAttribute): string => $itemAttribute->attribute->codigo);
        $missing = collect(self::ATTRIBUTE_MAP)->diff($attributes->keys());

        if ($missing->isNotEmpty()) {
            throw ValidationException::withMessages([
                'rows' => [
                    'Faltan atributos requeridos para las entradas del desfile: '.$missing->implode(', ').'.',
                ],
            ]);
        }

        return $attributes;
    }

    /**
     * @param  Collection<string, ItemAttribute>  $itemAttributes
     * @param  array<string, mixed>  $data
     * @return array<string, string>
     */
    private function resolveRowValues(Collection $itemAttributes, array $data, int $index): array
    {
        $values = [];

        foreach (self::ROW_ATTRIBUTE_MAP as $input => $code) {
            $values[$code] = $this->resolveAttributeValue(
                $itemAttributes,
                $code,
                (string) $data[$input],
                "rows.{$index}.{$input}",
            );
        }

        return $values;
    }

    /** @param Collection<string, ItemAttribute> $itemAttributes */
    private function resolveAttributeValue(
        Collection $itemAttributes,
        string $code,
        string $requestedValue,
        string $errorKey,
    ): string {
        $requestedValue = trim($requestedValue);
        $option = $itemAttributes[$code]->attribute->options->first(
            fn ($candidate): bool => $this->normalize($candidate->value) === $this->normalize($requestedValue),
        );

        if ($option === null) {
            throw ValidationException::withMessages([
                $errorKey => ['La opción seleccionada no es válida.'],
            ]);
        }

        return $option->value;
    }

    /** @param array<string, mixed> $values */
    private function selectionKey(array $values): string
    {
        return collect(self::ATTRIBUTE_MAP)
            ->map(fn (string $code): string => $this->normalize((string) ($values[$code] ?? '')))
            ->implode('|');
    }

    private function assertVariantCanChangeIdentity(Variant $variant, string $key): void
    {
        $inventory = $variant->inventory;

        if (($inventory?->reserved_stock ?? 0) > 0 || ($inventory?->sold_units ?? 0) > 0) {
            throw ValidationException::withMessages([
                $key => [
                    'No se puede modificar ni eliminar un asiento con ventas o reservas.',
                ],
            ]);
        }
    }

    /** @param array<string, string> $values */
    private function description(array $values): string
    {
        return "Sector {$values['sector']} - Fila {$values['fila']} - Asiento {$values['asiento']} - {$values['tipo']}";
    }

    private function normalize(string $value): string
    {
        return Str::ascii(mb_strtolower(trim($value)));
    }

    /** @return Builder<CatalogItem> */
    private function entryQuery(Tenant $tenant): Builder
    {
        return CatalogItem::query()
            ->where('tenant_code', $tenant->codigo)
            ->where('slug', 'entrada');
    }

    private function deleteIfUnused(Attachment $attachment): void
    {
        if (DB::table('catalog_items_attachments')->where('attachment_id', $attachment->id)->exists()) {
            return;
        }

        $this->deleteAttachmentQuietly($attachment);
    }

    private function deleteAttachmentQuietly(Attachment $attachment): void
    {
        try {
            $this->attachmentService->delete($attachment);
        } catch (Throwable $throwable) {
            report($throwable);
        }
    }
}
