<?php

namespace App\Domains\Catalog\Services;

use App\Domains\Attachable\Models\Attachment;
use App\Domains\Attachable\Services\AttachmentService;
use App\Domains\Catalog\Enums\CatalogItemType;
use App\Domains\Catalog\Models\Attribute;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Category;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\ItemAttribute;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Event\Models\EventDate;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class CatalogService
{
    public function __construct(protected AttachmentService $attachmentService) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function create(array $data): CatalogItem
    {
        return DB::transaction(function () use ($data): CatalogItem {
            $type = CatalogItemType::from(
                $data['type'] ?? CatalogItemType::Standard->value,
            );
            $variants = $data['variants'] ?? [];
            $images = $data['images'] ?? [];
            $attributeCodes = $data['attribute_codes'] ?? [];
            $multiSelectAttributeCodes = $data['multi_select_attribute_codes'] ?? [];
            $hiddenAttributeCodes = $data['hidden_attribute_codes'] ?? [];
            $ticketAttributeLabels = $data['ticket_attribute_labels'] ?? [];
            $components = $data['components'] ?? [];
            $hasDirectStock = array_key_exists('real_stock', $data);
            $realStock = (int) ($data['real_stock'] ?? 0);

            $hasEventDateVariants = $variants !== [] && collect($variants)->every(
                fn (array $variant): bool => ! empty($variant['event_date_id'])
                    || ! empty($variant['event_date_ids'])
            );

            if ($hasEventDateVariants && ! in_array('event_date', $attributeCodes, true)) {
                throw ValidationException::withMessages([
                    'attribute_codes' => [
                        __('api.catalog.event_date_attribute_required'),
                    ],
                ]);
            }

            $hasVariants = $attributeCodes !== [] || $hasEventDateVariants;

            if (array_diff($multiSelectAttributeCodes, $attributeCodes) !== []) {
                throw ValidationException::withMessages([
                    'multi_select_attribute_codes' => [
                        __('api.catalog.multi_select_attribute_not_on_item'),
                    ],
                ]);
            }

            if (array_diff($hiddenAttributeCodes, $attributeCodes) !== []) {
                throw ValidationException::withMessages([
                    'hidden_attribute_codes' => [
                        __('api.catalog.hidden_attribute_not_on_item'),
                    ],
                ]);
            }

            if (array_diff(array_keys($ticketAttributeLabels), $attributeCodes) !== []) {
                throw ValidationException::withMessages([
                    'ticket_attribute_labels' => [
                        __('api.catalog.ticket_label_attribute_not_on_item'),
                    ],
                ]);
            }

            $this->validateUniqueVariantCombinations($variants, $attributeCodes);

            if ($type === CatalogItemType::Bundle) {
                $this->validateBundleData($data, $components);
            } else {
                if (array_key_exists('components', $data)) {
                    throw ValidationException::withMessages([
                        'components' => [__('api.catalog.standard_with_components')],
                    ]);
                }

                $this->validateInventoryStrategy(
                    $data,
                    $variants,
                    $hasVariants,
                    $hasDirectStock,
                );
            }

            unset(
                $data['variants'],
                $data['images'],
                $data['attribute_codes'],
                $data['multi_select_attribute_codes'],
                $data['hidden_attribute_codes'],
                $data['ticket_attribute_labels'],
                $data['components'],
                $data['real_stock'],
                $data['reserved_stock'],
                $data['sold_units'],
                $data['inventory_id'],
            );

            $data['type'] = $type;

            if ($type === CatalogItemType::Bundle) {
                $data['inventory_id'] = null;
                $data['inventory_policy'] = null;
                $data['has_tickets'] = false;
            } elseif ($hasVariants) {
                $data['inventory_id'] = null;
            } else {
                $data['inventory_id'] = $this->createInventory($realStock)->id;
            }

            $catalogItem = CatalogItem::query()->create($data);
            $itemAttributes = $type === CatalogItemType::Standard
                ? $this->createItemAttributes(
                    $catalogItem,
                    $attributeCodes,
                    $multiSelectAttributeCodes,
                    $hiddenAttributeCodes,
                    $ticketAttributeLabels,
                )
                : [];

            if ($type === CatalogItemType::Bundle) {
                $this->createBundleComponents($catalogItem, $components);
            }

            $createdVariants = [];
            foreach ($variants as $index => $variantData) {
                $createdVariants[] = [
                    'variant' => $this->createVariant(
                        $catalogItem,
                        $variantData,
                        $itemAttributes,
                        $index,
                    ),
                    'images' => $variantData['images'] ?? [],
                    'index' => $index,
                ];
            }

            $this->attachImages($catalogItem, $images, 'images');

            foreach ($createdVariants as $createdVariant) {
                $this->attachImages(
                    $createdVariant['variant'],
                    $createdVariant['images'],
                    "variants.{$createdVariant['index']}.images",
                );
            }

            return $catalogItem->load([
                'attachments',
                'inventory',
                'category',
                'brand',
                'itemAttributes.attribute',
                'variants.inventory',
                'variants.attachments',
                'variants.eventDate',
                'variants.eventDates',
                'variants.definitions.itemAttribute.attribute.options',
                'bundleComponents.catalogItem',
                'bundleComponents.variant.catalogItem',
            ]);
        });
    }

    public function getDetail(CatalogItem $catalogItem, ?int $variantId = null): CatalogItem
    {
        $catalogItem->load([
            'attachments',
            'inventory',
            'category',
            'brand',
            'itemAttributes.attribute.options.validityTime',
            'itemAttributes.attribute.eventDates.validityTime',
            'variants' => fn ($query) => $query->orderBy('id'),
            'variants.inventory',
            'variants.attachments',
            'variants.eventDate',
            'variants.eventDates',
            'variants.definitions' => fn ($query) => $query->orderBy('id'),
            'variants.definitions.itemAttribute.attribute.options',
            'bundleComponents.catalogItem.inventory',
            'bundleComponents.variant.inventory',
            'bundleComponents.variant.definitions.itemAttribute.attribute',
        ]);

        $visibleVariants = $catalogItem->visibleVariants();
        if ($catalogItem->type === CatalogItemType::Standard
            && ($catalogItem->inventory_id !== null || $catalogItem->variants->isNotEmpty())
            && ! $catalogItem->isAvailable()) {
            throw new NotFoundHttpException('Catalog item is out of stock.');
        }

        $catalogItem->setRelation('variants', $visibleVariants);
        $selectedVariant = $variantId === null
            ? $visibleVariants->first()
            : $visibleVariants->firstWhere('id', $variantId);

        if ($variantId !== null && $selectedVariant === null) {
            throw new NotFoundHttpException('Variant not found for catalog item.');
        }

        $catalogItem->setRelation('selectedVariant', $selectedVariant);

        return $catalogItem;
    }

    /** @return LengthAwarePaginator<CatalogItem> */
    public function search(
        Tenant $tenant,
        string $term,
        int $perPage,
        int $page,
    ): LengthAwarePaginator {
        $normalizedTerm = mb_strtolower($term);
        $containsPattern = "%{$normalizedTerm}%";
        $startsWithPattern = "{$normalizedTerm}%";

        $paginator = CatalogItem::query()
            ->where('tenant_code', $tenant->codigo)
            ->whereAvailable()
            ->where(function (Builder $query) use ($containsPattern): void {
                $query
                    ->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
                    ->orWhereRaw('LOWER(descripcion) LIKE ?', [$containsPattern])
                    ->orWhereHas(
                        'brand',
                        fn (Builder $brandQuery) => $brandQuery
                            ->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
                    )
                    ->orWhereHas(
                        'category',
                        fn (Builder $categoryQuery) => $categoryQuery
                            ->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
                    );
            })
            ->with([
                'attachments',
                'inventory',
                'itemAttributes.attribute',
                'variants.inventory',
                'variants.attachments',
                'variants.eventDate',
                'variants.eventDates',
                'variants.definitions.itemAttribute.attribute.options',
                'bundleComponents.catalogItem',
                'bundleComponents.variant.catalogItem',
            ])
            ->orderByRaw(
                'CASE WHEN LOWER(nombre) = ? THEN 0 WHEN LOWER(nombre) LIKE ? THEN 1 ELSE 2 END',
                [$normalizedTerm, $startsWithPattern],
            )
            ->orderBy('nombre')
            ->paginate(perPage: $perPage, pageName: 'page', page: $page);

        return $paginator->withPath(route('catalog-items.index', [
            'tenant' => $tenant->codigo,
        ]));
    }

    /** @return LengthAwarePaginator<CatalogItem> */
    public function categoryItems(
        Tenant $tenant,
        Category $category,
        int $perPage,
        int $page,
    ): LengthAwarePaginator {
        return CatalogItem::query()
            ->where('tenant_code', $tenant->codigo)
            ->where('category_id', $category->id)
            ->whereAvailable()
            ->with([
                'attachments',
                'inventory',
                'itemAttributes.attribute',
                'variants.inventory',
                'variants.attachments',
                'variants.eventDate',
                'variants.eventDates',
                'variants.definitions.itemAttribute.attribute.options',
                'bundleComponents.catalogItem',
                'bundleComponents.variant.catalogItem',
            ])
            ->orderBy('nombre')
            ->paginate(perPage: $perPage, pageName: 'page', page: $page);
    }

    public function delete(CatalogItem $catalogItem): void
    {
        DB::transaction(function () use ($catalogItem): void {
            $catalogItem->variants()->delete();
            $catalogItem->delete();
        });
    }

    public function deleteVariant(Variant $variant): void
    {
        DB::transaction(function () use ($variant): void {
            $variant = Variant::query()
                ->lockForUpdate()
                ->findOrFail($variant->getKey());
            $catalogItem = CatalogItem::query()
                ->lockForUpdate()
                ->findOrFail($variant->catalog_item_id);
            $variant->delete();

            if (! $catalogItem->variants()->exists()) {
                $this->delete($catalogItem);
            } elseif (($minimumPrice = $catalogItem->variants()->min('precio')) !== null) {
                $catalogItem->update(['precio' => $minimumPrice]);
            }

        });
    }

    private function createInventory(int $realStock): Inventory
    {
        return Inventory::query()->create([
            'real_stock' => $realStock,
        ]);
    }

    /**
     * @param  array<int, array<string, mixed>>  $components
     */
    private function createBundleComponents(CatalogItem $bundle, array $components): void
    {
        $seen = [];

        foreach (array_values($components) as $index => $componentData) {
            $catalogItemId = (int) $componentData['catalog_item_id'];
            $variantId = isset($componentData['variant_id'])
                ? (int) $componentData['variant_id']
                : null;
            $key = $catalogItemId.':'.($variantId ?? 'direct');

            if (isset($seen[$key])) {
                throw ValidationException::withMessages([
                    "components.{$index}" => [__('api.catalog.duplicate_component')],
                ]);
            }
            $seen[$key] = true;

            $componentItem = CatalogItem::query()
                ->whereKey($catalogItemId)
                ->where('tenant_code', $bundle->tenant_code)
                ->first();

            if ($componentItem === null) {
                throw ValidationException::withMessages([
                    "components.{$index}.catalog_item_id" => [
                        __('api.catalog.component_wrong_tenant'),
                    ],
                ]);
            }

            if ($componentItem->is($bundle) || $componentItem->type !== CatalogItemType::Standard) {
                throw ValidationException::withMessages([
                    "components.{$index}.catalog_item_id" => [
                        __('api.catalog.invalid_component'),
                    ],
                ]);
            }

            $hasVariants = $componentItem->variants()->exists();
            if ($hasVariants && $variantId === null) {
                throw ValidationException::withMessages([
                    "components.{$index}.variant_id" => [
                        __('api.catalog.component_variant_required'),
                    ],
                ]);
            }

            if (! $hasVariants && $variantId !== null) {
                throw ValidationException::withMessages([
                    "components.{$index}.variant_id" => [
                        __('api.catalog.component_variant_forbidden'),
                    ],
                ]);
            }

            if ($variantId !== null && ! $componentItem->variants()->whereKey($variantId)->exists()) {
                throw ValidationException::withMessages([
                    "components.{$index}.variant_id" => [
                        __('api.catalog.component_variant_invalid'),
                    ],
                ]);
            }

            $bundle->bundleComponents()->create([
                'component_catalog_item_id' => $componentItem->id,
                'component_variant_id' => $variantId,
                'quantity' => (int) $componentData['quantity'],
            ]);
        }

    }

    /**
     * @param  array<string, mixed>  $data
     * @param  array<int, array<string, mixed>>  $components
     */
    private function validateBundleData(array $data, array $components): void
    {
        if ($components === []) {
            throw ValidationException::withMessages([
                'components' => [__('api.catalog.bundle_component_required')],
            ]);
        }

        foreach ([
            'real_stock',
            'inventory_policy',
            'attribute_codes',
            'variants',
            'has_tickets',
        ] as $field) {
            if (array_key_exists($field, $data)) {
                throw ValidationException::withMessages([
                    $field => [__('api.catalog.bundle_field_forbidden', ['field' => $field])],
                ]);
            }
        }
    }

    /**
     * @param  array<int, mixed>  $images
     */
    private function attachImages(
        CatalogItem|Variant $owner,
        array $images,
        string $validationKey,
    ): void {
        foreach (array_values($images) as $order => $image) {
            $attachment = $this->resolveAttachment($image, "{$validationKey}.{$order}");

            $owner->attachments()->attach($attachment->id, ['orden' => $order]);
        }
    }

    private function resolveAttachment(mixed $image, string $validationKey): Attachment
    {
        if (is_string($image) && Str::isUuid($image)) {
            $attachment = Attachment::query()->where('key', $image)->first();

            if ($attachment === null) {
                throw ValidationException::withMessages([
                    $validationKey => [__('api.catalog.attachment_not_found')],
                ]);
            }

            return $attachment;
        }

        return $this->attachmentService->store($image, 'catalog-items');
    }

    /**
     * @param  array<int, string>  $attributeCodes
     * @param  array<int, string>  $multiSelectAttributeCodes
     * @param  array<int, string>  $hiddenAttributeCodes
     * @param  array<string, string>  $ticketAttributeLabels
     * @return array<string, ItemAttribute>
     */
    private function createItemAttributes(
        CatalogItem $catalogItem,
        array $attributeCodes,
        array $multiSelectAttributeCodes = [],
        array $hiddenAttributeCodes = [],
        array $ticketAttributeLabels = [],
    ): array {
        $itemAttributes = [];
        $attributeCodes = array_values(array_unique($attributeCodes));
        $attributes = Attribute::query()
            ->where('tenant_codigo', $catalogItem->tenant_code)
            ->whereIn('codigo', $attributeCodes)
            ->get()
            ->keyBy('codigo');

        foreach ($attributeCodes as $attributeCode) {
            $attribute = $attributes->get($attributeCode);

            if ($attribute === null) {
                throw ValidationException::withMessages([
                    'attribute_codes' => [
                        __('api.catalog.attribute_not_found', ['attribute' => $attributeCode]),
                    ],
                ]);
            }

            $itemAttribute = $catalogItem->itemAttributes()->create([
                'attribute_id' => $attribute->id,
                'allow_multi_select' => in_array($attributeCode, $multiSelectAttributeCodes, true),
                'show_in_selector' => ! in_array($attributeCode, $hiddenAttributeCodes, true),
                'ticket_label' => $ticketAttributeLabels[$attributeCode] ?? null,
            ]);

            $itemAttributes[$attributeCode] = $itemAttribute;
        }

        return $itemAttributes;
    }

    /**
     * @param  array<string, mixed>  $data
     * @param  array<string, ItemAttribute>  $itemAttributes
     */
    private function createVariant(
        CatalogItem $catalogItem,
        array $data,
        array $itemAttributes,
        int $index,
    ): Variant {
        unset($data['images']);

        if (
            array_key_exists('inventory_id', $data)
            || array_key_exists('reserved_stock', $data)
            || array_key_exists('sold_units', $data)
        ) {
            throw ValidationException::withMessages([
                "variants.{$index}.inventory" => [
                    __('api.catalog.managed_inventory_fields'),
                ],
            ]);
        }

        $inventory = $this->createInventory((int) ($data['real_stock'] ?? 0));
        $eventDateIds = collect($data['event_date_ids'] ?? [])
            ->when(
                isset($data['event_date_id']),
                fn ($ids) => $ids->push($data['event_date_id']),
            )
            ->filter(fn ($id): bool => $id !== null)
            ->map(fn ($id): int => (int) $id)
            ->unique()
            ->sort()
            ->values();

        $eventDateItemAttribute = $itemAttributes['event_date'] ?? null;
        if ($eventDateItemAttribute !== null && (
            $eventDateIds->isEmpty()
            || (! $eventDateItemAttribute->allow_multi_select && $eventDateIds->count() !== 1)
        )) {
            throw ValidationException::withMessages([
                "variants.{$index}.event_date_ids" => [
                    $eventDateItemAttribute->allow_multi_select
                        ? __('api.catalog.event_date_selection_required')
                        : __('api.catalog.single_event_date_required'),
                ],
            ]);
        }

        $validEventDateCount = EventDate::query()
            ->whereKey($eventDateIds)
            ->where('tenant_code', $catalogItem->tenant_code)
            ->count();

        if ($validEventDateCount !== $eventDateIds->count()) {
            throw ValidationException::withMessages([
                "variants.{$index}.event_date_ids" => [
                    __('api.catalog.event_date_wrong_tenant'),
                ],
            ]);
        }

        $variant = $catalogItem->variants()->create([
            'inventory_id' => $inventory->id,
            'event_date_id' => $eventDateIds->count() === 1 ? $eventDateIds->first() : null,
            'descripcion' => $data['descripcion'] ?? null,
            'precio' => $data['precio'] ?? null,
        ]);
        $variant->eventDates()->sync($eventDateIds->all());
        $variant->setRelation('catalogItem', $catalogItem);

        foreach ($data['values'] ?? [] as $attributeCode => $value) {
            $itemAttribute = $itemAttributes[$attributeCode] ?? null;

            if ($itemAttribute === null) {
                throw ValidationException::withMessages([
                    "variants.{$index}.values.{$attributeCode}" => [
                        __('api.catalog.attribute_not_on_item'),
                    ],
                ]);
            }

            foreach ($this->validatedVariantValues(
                $itemAttribute,
                $value,
                "variants.{$index}.values.{$attributeCode}",
            ) as $validatedValue) {
                $variant->definitions()->create([
                    'item_attribute_id' => $itemAttribute->id,
                    'value' => $validatedValue,
                ]);
            }
        }

        return $variant;
    }

    /**
     * @param  array<int, array<string, mixed>>  $variants
     * @param  array<int, string>  $attributeCodes
     */
    private function validateUniqueVariantCombinations(array $variants, array $attributeCodes): void
    {
        $seen = [];
        $attributeCodes = array_values(array_unique($attributeCodes));
        sort($attributeCodes);

        foreach (array_values($variants) as $index => $variant) {
            $eventDateIds = collect($variant['event_date_ids'] ?? [])
                ->when(
                    isset($variant['event_date_id']),
                    fn ($ids) => $ids->push($variant['event_date_id']),
                )
                ->map(fn ($id): int => (int) $id)
                ->unique()
                ->sort()
                ->values()
                ->implode(',');
            $combination = [$eventDateIds];

            foreach ($attributeCodes as $attributeCode) {
                $values = $variant['values'][$attributeCode] ?? '';
                $normalizedValues = collect(is_array($values) ? $values : [$values])
                    ->map(fn ($value): string => $this->normalizeVariantValue((string) $value))
                    ->unique()
                    ->sort()
                    ->values()
                    ->implode(',');
                $combination[] = $normalizedValues;
            }

            $key = implode('|', $combination);
            if (isset($seen[$key])) {
                throw ValidationException::withMessages([
                    "variants.{$index}" => [__('api.catalog.duplicate_variant_combination')],
                ]);
            }

            $seen[$key] = true;
        }
    }

    /** @return array<int, string> */
    private function validatedVariantValues(
        ItemAttribute $itemAttribute,
        mixed $value,
        string $validationKey,
    ): array {
        $values = is_array($value) ? array_values($value) : [$value];

        if ($values === [] || (! $itemAttribute->allow_multi_select && count($values) !== 1)) {
            throw ValidationException::withMessages([
                $validationKey => [
                    $itemAttribute->allow_multi_select
                        ? __('api.catalog.multi_value_required')
                        : __('api.catalog.single_value_required'),
                ],
            ]);
        }

        if (collect($values)->contains(fn ($item): bool => ! is_string($item) || trim($item) === '')) {
            throw ValidationException::withMessages([
                $validationKey => [__('api.catalog.selected_values_non_empty')],
            ]);
        }

        $normalizedValues = collect($values)
            ->map(fn (string $item): string => $this->normalizeVariantValue($item));

        if ($normalizedValues->unique()->count() !== count($values)) {
            throw ValidationException::withMessages([
                $validationKey => [__('api.catalog.selected_values_distinct')],
            ]);
        }

        $attribute = $itemAttribute->attribute;
        if ($attribute->type->supportsOptions() && ! $attribute->type->usesDynamicOptions()) {
            $optionsByNormalizedValue = $attribute->options
                ->keyBy(fn ($option): string => $this->normalizeVariantValue($option->value));

            $resolvedOptions = $normalizedValues->map(fn (string $normalizedValue) => $optionsByNormalizedValue->get($normalizedValue));
            if ($resolvedOptions->contains(null)) {
                throw ValidationException::withMessages([
                    $validationKey => [__('api.catalog.invalid_attribute_options')],
                ]);
            }

            return $resolvedOptions->pluck('value')->all();
        }

        return collect($values)->map(fn (string $item): string => trim($item))->all();
    }

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

    /**
     * @param  array<string, mixed>  $data
     * @param  array<int, array<string, mixed>>  $variants
     */
    private function validateInventoryStrategy(
        array $data,
        array $variants,
        bool $hasVariants,
        bool $hasDirectStock,
    ): void {
        if ($hasVariants && $variants === []) {
            throw ValidationException::withMessages([
                'variants' => [
                    __('api.catalog.variants_required'),
                ],
            ]);
        }

        if (! $hasVariants && $variants !== []) {
            throw ValidationException::withMessages([
                'variants' => [
                    __('api.catalog.variants_forbidden'),
                ],
            ]);
        }

        if ($hasVariants && $hasDirectStock) {
            throw ValidationException::withMessages([
                'real_stock' => [
                    __('api.catalog.direct_inventory_forbidden'),
                ],
            ]);
        }

        if (
            array_key_exists('inventory_id', $data)
            || array_key_exists('reserved_stock', $data)
            || array_key_exists('sold_units', $data)
        ) {
            throw ValidationException::withMessages([
                'inventory' => [
                    __('api.catalog.managed_inventory_fields'),
                ],
            ]);
        }
    }
}
