<?php

namespace App\Services;

use App\Models\ActionTicket;
use App\Models\Raffle;
use App\Models\Transaction;
use InvalidArgumentException;

/**
 * Lógica de la tienda de tarjetas coleccionables.
 *
 * Cada sorteo vende tarjetas con número único (0 .. total_tickets - 1).
 * Un número comprado no puede volver a comprarse. La rareza de cada tarjeta
 * se sortea al aprobar el pago según rarity_config y otorga llaves para
 * abrir cofres. Los descuentos por volumen se configuran por sorteo.
 */
class CardShopService
{
    /** Minutos que una transacción pendiente retiene los números elegidos. */
    public const HOLD_MINUTES = 15;

    /**
     * Cotiza el precio de una cantidad de tarjetas aplicando el tier
     * de descuento más alto alcanzado.
     */
    public function quote(Raffle $raffle, int $quantity): array
    {
        $unitPrice = (float) $raffle->ticket_price;
        $subtotal = $unitPrice * $quantity;

        $discountPercent = 0.0;
        foreach ($raffle->effectiveDiscountTiers() as $tier) {
            $minQty = (int) ($tier['min_quantity'] ?? 0);
            if ($minQty > 0 && $quantity >= $minQty) {
                $discountPercent = max($discountPercent, (float) ($tier['discount_percent'] ?? 0));
            }
        }

        $discountAmount = round($subtotal * ($discountPercent / 100), 2);

        return [
            'unit_price' => $unitPrice,
            'quantity' => $quantity,
            'subtotal' => round($subtotal, 2),
            'discount_percent' => $discountPercent,
            'discount_amount' => $discountAmount,
            'total' => round($subtotal - $discountAmount, 2),
        ];
    }

    /**
     * Cantidad de dígitos de los números de tarjeta del sorteo
     * (p.ej. 10000 tarjetas => 4 dígitos: 0000-9999).
     */
    public function digitsFor(Raffle $raffle): int
    {
        return strlen((string) max(1, $raffle->total_tickets - 1));
    }

    /**
     * Normaliza y valida los números elegidos. Lanza InvalidArgumentException
     * con un mensaje amigable si alguno es inválido o no está disponible.
     *
     * @return array<string> números normalizados con ceros a la izquierda
     */
    public function validateNumbers(Raffle $raffle, array $numbers, ?int $ignoreTransactionId = null): array
    {
        $digits = $this->digitsFor($raffle);
        $maxVal = max(1, $raffle->total_tickets - 1);
        $normalized = [];

        foreach ($numbers as $idx => $num) {
            $num = trim((string) $num);

            if (! preg_match('/^[0-9]+$/', $num)) {
                throw new InvalidArgumentException("El número " . ($idx + 1) . " ($num) debe ser numérico.");
            }

            $val = (int) $num;
            if ($val < 0 || $val > $maxVal) {
                throw new InvalidArgumentException("El número $num está fuera del rango permitido (0 a $maxVal).");
            }

            $padded = str_pad((string) $val, $digits, '0', STR_PAD_LEFT);

            if (in_array($padded, $normalized, true)) {
                throw new InvalidArgumentException("El número $padded está repetido en tu selección.");
            }

            $normalized[] = $padded;
        }

        $taken = $this->takenNumbers($raffle, $normalized, $ignoreTransactionId);
        if (count($taken) > 0) {
            throw new InvalidArgumentException(
                'Estos números ya no están disponibles: ' . implode(', ', $taken) . '. Por favor elige otros.'
            );
        }

        return $normalized;
    }

    /**
     * De la lista dada, devuelve los números ya vendidos o retenidos por
     * otra compra pendiente vigente.
     */
    public function takenNumbers(Raffle $raffle, array $numbers, ?int $ignoreTransactionId = null): array
    {
        if (count($numbers) === 0) {
            return [];
        }

        $sold = ActionTicket::where('raffle_id', $raffle->id)
            ->whereIn('ticket_number', $numbers)
            ->pluck('ticket_number')
            ->all();

        $held = [];
        Transaction::where('raffle_id', $raffle->id)
            ->where('type', 'purchase')
            ->where('status', 'pending')
            ->where('created_at', '>=', now()->subMinutes(self::HOLD_MINUTES))
            ->when($ignoreTransactionId, fn ($q) => $q->where('id', '!=', $ignoreTransactionId))
            ->whereNotNull('card_numbers')
            ->pluck('card_numbers')
            ->each(function ($cardNumbers) use (&$held, $numbers) {
                foreach ((array) $cardNumbers as $n) {
                    if (in_array($n, $numbers, true)) {
                        $held[] = $n;
                    }
                }
            });

        return array_values(array_unique(array_merge($sold, $held)));
    }

    /**
     * Genera hasta $quantity números disponibles al azar.
     */
    public function randomAvailableNumbers(Raffle $raffle, int $quantity): array
    {
        $digits = $this->digitsFor($raffle);
        $maxVal = max(1, $raffle->total_tickets - 1);
        $result = [];
        $attempts = 0;

        while (count($result) < $quantity && $attempts < $quantity * 200) {
            $attempts++;
            $num = str_pad((string) mt_rand(0, $maxVal), $digits, '0', STR_PAD_LEFT);
            if (in_array($num, $result, true)) {
                continue;
            }
            if (count($this->takenNumbers($raffle, [$num])) === 0) {
                $result[] = $num;
            }
        }

        // Respaldo secuencial si el azar no alcanza (sorteo casi agotado)
        if (count($result) < $quantity) {
            for ($i = 0; $i <= $maxVal && count($result) < $quantity; $i++) {
                $num = str_pad((string) $i, $digits, '0', STR_PAD_LEFT);
                if (! in_array($num, $result, true) && count($this->takenNumbers($raffle, [$num])) === 0) {
                    $result[] = $num;
                }
            }
        }

        return $result;
    }

    /**
     * Emite las tarjetas de una compra aprobada: sortea la rareza de cada
     * una según rarity_config, otorga las llaves correspondientes y
     * actualiza el contador de vendidas.
     *
     * Idempotente: si la transacción ya tiene tarjetas emitidas, las retorna.
     *
     * @return ActionTicket[] tarjetas emitidas
     */
    public function fulfill(Transaction $transaction): array
    {
        $existing = ActionTicket::where('transaction_id', $transaction->id)->get();
        if ($existing->count() > 0) {
            return $existing->all();
        }

        $raffle = $transaction->raffle;
        if (! $raffle) {
            return [];
        }

        $numbers = (array) ($transaction->card_numbers ?? []);
        $quantity = $transaction->quantity ?? count($numbers);

        // Completar con números aleatorios si faltan (compras legadas sin selección)
        if (count($numbers) < $quantity) {
            $numbers = array_merge(
                $numbers,
                $this->randomAvailableNumbers($raffle, $quantity - count($numbers))
            );
        }

        $config = $raffle->effectiveRarityConfig();
        $cards = [];
        $totalKeys = 0;

        foreach ($numbers as $number) {
            // Si el número fue tomado mientras el pago estaba en proceso,
            // se sustituye por uno disponible para no perder la compra.
            if (ActionTicket::where('raffle_id', $raffle->id)->where('ticket_number', $number)->exists()) {
                $replacement = $this->randomAvailableNumbers($raffle, 1);
                if (count($replacement) === 0) {
                    continue; // Sorteo agotado
                }
                $number = $replacement[0];
            }

            [$rarity, $keys] = $this->rollRarity($config);

            $cards[] = ActionTicket::create([
                'user_id' => $transaction->user_id,
                'raffle_id' => $raffle->id,
                'ticket_number' => $number,
                'transaction_id' => $transaction->id,
                'rarity' => $rarity,
                'keys_granted' => $keys,
            ]);

            $totalKeys += $keys;
        }

        if ($totalKeys > 0) {
            $userKey = \App\Models\UserKey::firstOrCreate(
                ['user_id' => $transaction->user_id, 'raffle_id' => $raffle->id],
                ['quantity' => 0]
            );
            $userKey->increment('quantity', $totalKeys);
        }

        $raffle->increment('sold_tickets', count($cards));

        return $cards;
    }

    /**
     * Sortea una rareza ponderada por probabilidad.
     *
     * @return array{0: string, 1: int} [rareza, llaves]
     */
    public function rollRarity(array $config): array
    {
        $totalWeight = 0.0;
        foreach ($config as $entry) {
            $totalWeight += max(0, (float) ($entry['probability'] ?? 0));
        }

        if ($totalWeight <= 0) {
            return ['common', 0];
        }

        $roll = mt_rand(1, (int) round($totalWeight * 100)) / 100;
        $accumulated = 0.0;

        foreach ($config as $entry) {
            $accumulated += max(0, (float) ($entry['probability'] ?? 0));
            if ($roll <= $accumulated) {
                return [
                    (string) ($entry['rarity'] ?? 'common'),
                    (int) ($entry['keys'] ?? 0),
                ];
            }
        }

        $last = end($config);

        return [(string) ($last['rarity'] ?? 'common'), (int) ($last['keys'] ?? 0)];
    }
}
