<?php

namespace App\Services;

use App\Models\CashbackLedger;
use App\Models\CashbackWallet;
use App\Models\User;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class CashbackWalletService
{
    public function getWallet(User $user): CashbackWallet
    {
        return CashbackWallet::firstOrCreate(
            ['user_id' => $user->id],
            ['balance' => 0]
        );
    }

    public function balance(User $user): float
    {
        return round((float) $this->getWallet($user)->balance, 2);
    }

    public function debit(
        User $user,
        float|int|string $amount,
        string $reference,
        string $narration,
        array $meta = [],
        string $source = 'purchase'
    ): CashbackLedger {
        return $this->postEntry(
            user: $user,
            entryType: 'debit',
            amount: $amount,
            reference: $reference,
            narration: $narration,
            meta: $meta,
            source: $source
        );
    }

    public function credit(
        User $user,
        float|int|string $amount,
        string $reference,
        string $narration,
        array $meta = [],
        string $source = 'reward'
    ): CashbackLedger {
        return $this->postEntry(
            user: $user,
            entryType: 'credit',
            amount: $amount,
            reference: $reference,
            narration: $narration,
            meta: $meta,
            source: $source
        );
    }

    protected function postEntry(
        User $user,
        string $entryType,
        float|int|string $amount,
        string $reference,
        string $narration,
        array $meta,
        string $source
    ): CashbackLedger {
        $normalizedAmount = round((float) $amount, 2);
        if ($normalizedAmount <= 0) {
            throw new InvalidArgumentException('Cashback amount must be greater than zero.');
        }

        $existing = CashbackLedger::query()->where('reference', $reference)->first();
        if ($existing) {
            $this->assertDuplicateReferenceIntegrity($existing, $user->id, $entryType, $normalizedAmount);

            return $existing;
        }

        try {
            return DB::transaction(function () use (
                $user,
                $entryType,
                $normalizedAmount,
                $reference,
                $narration,
                $meta,
                $source
            ) {
                $existing = CashbackLedger::query()->where('reference', $reference)->lockForUpdate()->first();
                if ($existing) {
                    $this->assertDuplicateReferenceIntegrity($existing, $user->id, $entryType, $normalizedAmount);

                    return $existing;
                }

                $wallet = $this->getWallet($user);
                $wallet = CashbackWallet::query()->whereKey($wallet->id)->lockForUpdate()->firstOrFail();

                $before = round((float) $wallet->balance, 2);
                $after = $entryType === 'debit'
                    ? round($before - $normalizedAmount, 2)
                    : round($before + $normalizedAmount, 2);

                if ($entryType === 'debit' && $after < 0) {
                    throw new InvalidArgumentException('Insufficient cashback balance.');
                }

                $wallet->update([
                    'balance' => $after,
                    'last_activity_at' => now(),
                ]);

                return CashbackLedger::query()->create([
                    'user_id' => $user->id,
                    'entry_type' => $entryType,
                    'amount' => $normalizedAmount,
                    'reference' => $reference,
                    'source' => $source,
                    'narration' => $narration,
                    'meta_json' => $meta,
                    'balance_before' => $before,
                    'balance_after' => $after,
                ]);
            }, 3);
        } catch (QueryException $e) {
            if (! str_contains(strtolower($e->getMessage()), 'unique')) {
                throw $e;
            }

            $existing = CashbackLedger::query()->where('reference', $reference)->firstOrFail();
            $this->assertDuplicateReferenceIntegrity($existing, $user->id, $entryType, $normalizedAmount);

            return $existing;
        }
    }

    protected function assertDuplicateReferenceIntegrity(
        CashbackLedger $ledger,
        int $userId,
        string $entryType,
        float $amount
    ): void {
        if (
            (int) $ledger->user_id !== $userId
            || $ledger->entry_type !== $entryType
            || round((float) $ledger->amount, 2) !== round($amount, 2)
        ) {
            throw new InvalidArgumentException('Duplicate cashback reference used with a different payload.');
        }
    }
}
