<?php

namespace App\Domains\Desfile\Services;

use DateTimeInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use RuntimeException;

class InvitationPurchaseProvisioner
{
    public const TENANT_CODE = 'desfile_pura_tendencia';

    public const USER_EMAIL = 'invitados@puratendencia.com';

    private const PAYMENT_METHOD = 'invitation';

    /**
     * @var list<array{sector: string, row: int, first_seat: int, last_seat: int, type: string}>
     */
    private const ALLOCATIONS = [
        ['sector' => 'A', 'row' => 1, 'first_seat' => 1, 'last_seat' => 16, 'type' => 'NORMAL'],
        ['sector' => 'A', 'row' => 3, 'first_seat' => 1, 'last_seat' => 14, 'type' => 'NORMAL'],
        ['sector' => 'C', 'row' => 1, 'first_seat' => 1, 'last_seat' => 16, 'type' => 'VIP + LUNCH'],
        ['sector' => 'C', 'row' => 3, 'first_seat' => 6, 'last_seat' => 7, 'type' => 'NORMAL'],
    ];

    /**
     * @param  list<array{sector: string, row: int, first_seat: int, last_seat: int, type: string}>|null  $allocations
     */
    public function provision(?array $allocations = null): void
    {
        if (! $this->prerequisitesExist()) {
            return;
        }

        DB::transaction(function () use ($allocations): void {
            $now = now();
            $userId = $this->userId($now);
            $purchaseId = $this->purchaseId($userId, $now);
            $catalogItem = DB::table('catalog_items')
                ->where('tenant_code', self::TENANT_CODE)
                ->where('slug', 'entrada')
                ->first();

            if ($catalogItem === null) {
                throw new RuntimeException('No se encontró el catálogo de entradas del desfile.');
            }

            foreach ($allocations ?? self::ALLOCATIONS as $allocation) {
                foreach (range($allocation['first_seat'], $allocation['last_seat']) as $seat) {
                    $variant = $this->variant(
                        (int) $catalogItem->id,
                        $allocation['sector'],
                        $allocation['row'],
                        $seat,
                        $allocation['type'],
                    );

                    $this->createPurchaseItem(
                        $purchaseId,
                        $catalogItem,
                        $variant,
                        $allocation['sector'],
                        $allocation['row'],
                        $seat,
                        $allocation['type'],
                        $now,
                    );
                    $this->createTicketAndCommitStock(
                        $purchaseId,
                        $userId,
                        (int) $catalogItem->id,
                        $variant,
                        $now,
                    );
                }
            }
        });
    }

    private function prerequisitesExist(): bool
    {
        if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
            return false;
        }

        if (! DB::table('roles')->where('codigo', 'user')->exists()) {
            throw new RuntimeException('No se encontró el rol de usuario común.');
        }

        if (! DB::table('catalog_items')
            ->where('tenant_code', self::TENANT_CODE)
            ->where('slug', 'entrada')
            ->exists()) {
            throw new RuntimeException('No se encontró el catálogo de entradas del desfile.');
        }

        return true;
    }

    private function userId(DateTimeInterface $now): int
    {
        $user = DB::table('users')->where('email', self::USER_EMAIL)->first();

        if ($user !== null) {
            if ($user->tenant_codigo !== self::TENANT_CODE) {
                throw new RuntimeException('El email de invitados ya pertenece a otro tenant.');
            }

            DB::table('users')->where('id', $user->id)->update([
                'rol_codigo' => 'user',
                'nombre_apellido' => 'Invitados Pura Tendencia',
                'updated_at' => $now,
            ]);

            return (int) $user->id;
        }

        return DB::table('users')->insertGetId([
            'rol_codigo' => 'user',
            'tenant_codigo' => self::TENANT_CODE,
            'nombre_apellido' => 'Invitados Pura Tendencia',
            'email' => self::USER_EMAIL,
            'email_verified_at' => $now,
            'password' => Hash::make(Str::random(64)),
            'dni' => null,
            'telefono' => null,
            'remember_token' => null,
            'created_at' => $now,
            'updated_at' => $now,
        ]);
    }

    private function purchaseId(int $userId, DateTimeInterface $now): int
    {
        $purchaseId = DB::table('compras')
            ->where('tenant_codigo', self::TENANT_CODE)
            ->where('user_id', $userId)
            ->where('payment_method', self::PAYMENT_METHOD)
            ->value('id');

        if ($purchaseId !== null) {
            DB::table('compras')->where('id', $purchaseId)->update([
                'status' => 'paid',
                'total' => 0,
                'updated_at' => $now,
            ]);

            return (int) $purchaseId;
        }

        return DB::table('compras')->insertGetId([
            'tenant_codigo' => self::TENANT_CODE,
            'user_id' => $userId,
            'cart_id' => null,
            'status' => 'paid',
            'payment_method' => self::PAYMENT_METHOD,
            'total' => 0,
            'dni' => null,
            'transfer_payer_dni' => null,
            'telefono' => null,
            'nombre_apellido' => 'Invitados Pura Tendencia',
            'email' => self::USER_EMAIL,
            'created_at' => $now,
            'updated_at' => $now,
        ]);
    }

    private function variant(
        int $catalogItemId,
        string $sector,
        int $row,
        int $seat,
        string $type,
    ): object {
        $variant = $this->findVariant($catalogItemId, $sector, $row, $seat);

        if ($variant === null) {
            $variant = $this->createVariant($catalogItemId, $sector, $row, $seat, $type);
        }

        $this->ensureAttributeOptions($catalogItemId, [
            'tipo' => $type,
            'sector' => $sector,
            'fila' => (string) $row,
            'asiento' => (string) $seat,
        ]);

        $typeValueId = DB::table('variant_values as variant_value')
            ->join('item_attributes as item_attribute', 'item_attribute.id', '=', 'variant_value.item_attribute_id')
            ->join('attribute', 'attribute.id', '=', 'item_attribute.attribute_id')
            ->where('variant_value.variant_id', $variant->id)
            ->where('attribute.codigo', 'tipo')
            ->value('variant_value.id');

        if ($typeValueId === null) {
            throw new RuntimeException("La variante {$variant->id} no tiene definido el tipo de entrada.");
        }

        DB::table('variant_values')->where('id', $typeValueId)->update(['value' => $type]);
        DB::table('variantes')->where('id', $variant->id)->update([
            'descripcion' => "Sector {$sector} - Fila {$row} - Asiento {$seat} - {$type}",
        ]);

        return DB::table('variantes')->where('id', $variant->id)->first();
    }

    /** @param array<string, string> $values */
    private function ensureAttributeOptions(int $catalogItemId, array $values): void
    {
        $attributes = DB::table('item_attributes')
            ->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id')
            ->where('item_attributes.catalog_item_id', $catalogItemId)
            ->pluck('attribute.id', 'attribute.codigo');

        foreach ($values as $code => $value) {
            $attributeId = $attributes[$code] ?? null;

            if ($attributeId === null) {
                throw new RuntimeException("Falta el atributo {$code} en el catálogo de entradas.");
            }

            if (DB::table('attribute_options')
                ->where('attribute_id', $attributeId)
                ->where('value', $value)
                ->exists()) {
                continue;
            }

            $lastSortOrder = (int) DB::table('attribute_options')
                ->where('attribute_id', $attributeId)
                ->max('sort_order');

            DB::table('attribute_options')->insert([
                'attribute_id' => $attributeId,
                'validity_time_id' => null,
                'value' => $value,
                'label' => $value,
                'sort_order' => $lastSortOrder + 1,
                'metadata' => null,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }

    private function findVariant(int $catalogItemId, string $sector, int $row, int $seat): ?object
    {
        $query = DB::table('variantes')->where('catalog_item_id', $catalogItemId);

        foreach (['sector' => $sector, 'fila' => (string) $row, 'asiento' => (string) $seat] as $code => $value) {
            $query->whereExists(fn ($subquery) => $subquery
                ->selectRaw('1')
                ->from('variant_values as selected_value')
                ->join('item_attributes as selected_item_attribute', 'selected_item_attribute.id', '=', 'selected_value.item_attribute_id')
                ->join('attribute as selected_attribute', 'selected_attribute.id', '=', 'selected_item_attribute.attribute_id')
                ->whereColumn('selected_value.variant_id', 'variantes.id')
                ->where('selected_attribute.codigo', $code)
                ->where('selected_value.value', $value));
        }

        return $query->first();
    }

    private function createVariant(
        int $catalogItemId,
        string $sector,
        int $row,
        int $seat,
        string $type,
    ): object {
        $itemAttributes = DB::table('item_attributes')
            ->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id')
            ->where('item_attributes.catalog_item_id', $catalogItemId)
            ->pluck('item_attributes.id', 'attribute.codigo');

        foreach (['tipo', 'sector', 'fila', 'asiento'] as $requiredCode) {
            if (! isset($itemAttributes[$requiredCode])) {
                throw new RuntimeException("Falta el atributo {$requiredCode} en el catálogo de entradas.");
            }
        }

        $inventoryId = DB::table('inventories')->insertGetId([
            'sold_units' => 0,
            'reserved_stock' => 0,
            'real_stock' => 1,
        ]);
        $variantId = DB::table('variantes')->insertGetId([
            'catalog_item_id' => $catalogItemId,
            'event_date_id' => null,
            'inventory_id' => $inventoryId,
            'descripcion' => "Sector {$sector} - Fila {$row} - Asiento {$seat} - {$type}",
            'precio' => $this->catalogPrice($sector, $row),
        ]);

        foreach (['tipo' => $type, 'sector' => $sector, 'fila' => (string) $row, 'asiento' => (string) $seat] as $code => $value) {
            DB::table('variant_values')->insert([
                'variant_id' => $variantId,
                'item_attribute_id' => $itemAttributes[$code],
                'value' => $value,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }

        return DB::table('variantes')->where('id', $variantId)->first();
    }

    private function catalogPrice(string $sector, int $row): int
    {
        $prices = in_array($sector, ['A', 'C'], true)
            ? [1 => 250000, 2 => 200000, 3 => 100000, 4 => 75000, 5 => 50000]
            : [1 => 240000, 2 => 190000, 3 => 90000, 4 => 65000, 5 => 40000];

        return $prices[$row];
    }

    private function createPurchaseItem(
        int $purchaseId,
        object $catalogItem,
        object $variant,
        string $sector,
        int $row,
        int $seat,
        string $type,
        DateTimeInterface $now,
    ): void {
        if (DB::table('compra_items')
            ->where('compra_id', $purchaseId)
            ->where('source_variant_id', $variant->id)
            ->exists()) {
            return;
        }

        $attributes = [
            ['name' => 'Tipo', 'value' => $type],
            ['name' => 'Sector', 'value' => $sector],
            ['name' => 'Fila', 'value' => (string) $row],
            ['name' => 'Asiento', 'value' => (string) $seat],
        ];

        DB::table('compra_items')->insert([
            'compra_id' => $purchaseId,
            'source_catalog_item_id' => $catalogItem->id,
            'source_variant_id' => $variant->id,
            'image_attachment_id' => DB::table('catalog_items_attachments')
                ->where('catalog_item_id', $catalogItem->id)
                ->whereNull('variant_id')
                ->orderBy('orden')
                ->value('attachment_id'),
            'nombre' => $catalogItem->nombre,
            'descripcion' => $variant->descripcion,
            'slug' => $catalogItem->slug,
            'item_nombre' => "Entrada (Sector {$sector}, Fila {$row}, Asiento {$seat})",
            'variant_attributes' => json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
            'cantidad' => 1,
            'precio_unitario' => 0,
            'discount_total' => 0,
            'tax_total' => 0,
            'total' => 0,
            'created_at' => $now,
            'updated_at' => $now,
        ]);
    }

    private function createTicketAndCommitStock(
        int $purchaseId,
        int $userId,
        int $catalogItemId,
        object $variant,
        DateTimeInterface $now,
    ): void {
        if (DB::table('tickets')
            ->where('source_purchase_id', $purchaseId)
            ->where('source_variant_id', $variant->id)
            ->exists()) {
            return;
        }

        $inventory = DB::table('inventories')->where('id', $variant->inventory_id)->lockForUpdate()->first();

        if ($inventory === null || $inventory->real_stock < 1 || $inventory->reserved_stock > 0) {
            throw new RuntimeException("El asiento {$variant->descripcion} ya no está disponible.");
        }

        DB::table('inventories')->where('id', $inventory->id)->update([
            'real_stock' => $inventory->real_stock - 1,
            'sold_units' => $inventory->sold_units + 1,
        ]);

        $reservationId = DB::table('compras')->where('id', $purchaseId)->value('stock_reservation_id');
        if ($reservationId === null) {
            $reservationId = DB::table('stock_reservations')->insertGetId([
                'status' => 'committed',
                'committed_at' => $now,
                'released_at' => null,
                'expired_at' => null,
                'release_reason' => null,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('compras')->where('id', $purchaseId)->update([
                'stock_reservation_id' => $reservationId,
            ]);
        }

        DB::table('stock_reservation_lines')->insert([
            'stock_reservation_id' => $reservationId,
            'inventory_id' => $inventory->id,
            'quantity' => 1,
            'tracks_inventory' => true,
            'created_at' => $now,
            'updated_at' => $now,
        ]);

        DB::table('tickets')->insert([
            'tenant_code' => self::TENANT_CODE,
            'ticket' => (string) Str::uuid(),
            'name' => null,
            'description' => null,
            'source_purchase_id' => $purchaseId,
            'source_catalog_item_id' => $catalogItemId,
            'source_variant_id' => $variant->id,
            'used_at' => null,
            'scanner_user_id' => null,
            'user_id' => $userId,
        ]);
    }
}
