<?php

namespace App\Services;

use App\Models\Lookups\DiscountType;
use App\Models\Lookups\Status;
use App\Models\Sales\Invoice;
use App\Models\Vouchers\Voucher;
use App\Models\Vouchers\VoucherRedemption;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use RuntimeException;

/**
 * All voucher validation, discount calculation, and redemption bookkeeping
 * goes through this service. Never inline voucher logic in a controller.
 */
class VoucherService
{
    /**
     * Resolve a voucher code and validate it's usable right now. Throws with
     * a specific, operator-facing message for each rejection reason.
     */
    public function validate(string $code, ?int $customerId, ?int $branchId, float $cartTotal): Voucher
    {
        if (! app(PosSettings::class)->vouchersEnabled()) {
            throw new RuntimeException('Vouchers are disabled for this workspace.');
        }

        $voucher = Voucher::with('type', 'status')->where('code', $code)->first();

        if (! $voucher) {
            throw new RuntimeException("Voucher code '{$code}' was not found.");
        }
        if (! $voucher->isActive()) {
            // A manually deactivated voucher must not read as "Active" just
            // because its lifecycle status is the active one.
            if (! $voucher->is_active) {
                throw new RuntimeException("Voucher '{$code}' is inactive.");
            }

            throw new RuntimeException("Voucher '{$code}' is {$voucher->status?->name}.");
        }
        if ($voucher->valid_from && now()->lt($voucher->valid_from)) {
            throw new RuntimeException("Voucher '{$code}' is not valid yet.");
        }
        if ($voucher->valid_until && now()->gt($voucher->valid_until)) {
            throw new RuntimeException("Voucher '{$code}' has expired.");
        }
        if ($voucher->usage_limit !== null && $voucher->used_count >= $voucher->usage_limit) {
            throw new RuntimeException("Voucher '{$code}' has reached its usage limit.");
        }
        if ($voucher->isBalanceBased() && (float) $voucher->remaining_balance <= 0) {
            throw new RuntimeException("Voucher '{$code}' has no remaining balance.");
        }
        if ($voucher->customer_id !== null && $voucher->customer_id !== $customerId) {
            throw new RuntimeException("Voucher '{$code}' is not valid for this customer.");
        }
        if ($voucher->branch_id !== null && $voucher->branch_id !== $branchId) {
            throw new RuntimeException("Voucher '{$code}' is not valid at this branch.");
        }
        if ($cartTotal < (float) $voucher->minimum_purchase) {
            throw new RuntimeException("Voucher '{$code}' requires a minimum purchase of {$voucher->minimum_purchase}.");
        }

        return $voucher;
    }

    /** Amount this voucher would discount off the given eligible amount. */
    public function computeDiscount(Voucher $voucher, float $eligibleAmount): float
    {
        if ($voucher->isBalanceBased()) {
            return min((float) $voucher->remaining_balance, $eligibleAmount);
        }

        $amount = $voucher->discountType?->code === DiscountType::PERCENTAGE
            ? $eligibleAmount * ((float) $voucher->discount_value / 100)
            : (float) $voucher->discount_value;

        if ($voucher->maximum_discount !== null) {
            $amount = min($amount, (float) $voucher->maximum_discount);
        }

        return round(min($amount, $eligibleAmount), 2);
    }

    /**
     * Apply a voucher to an invoice: decrement balance/usage, write the
     * redemption record, auto-exhaust when spent/limit reached.
     */
    public function apply(Voucher $voucher, Invoice $invoice, float $amount): VoucherRedemption
    {
        return DB::transaction(function () use ($voucher, $invoice, $amount) {
            $balanceBefore = $voucher->remaining_balance;
            $balanceAfter = null;
            $exhausted = false;

            if ($voucher->isBalanceBased()) {
                $balanceAfter = round((float) $voucher->remaining_balance - $amount, 2);
                $exhausted = $balanceAfter <= 0.001;
                $voucher->update(['remaining_balance' => max(0, $balanceAfter), 'used_count' => $voucher->used_count + 1]);
            } else {
                $newUsedCount = $voucher->used_count + 1;
                $exhausted = $voucher->usage_limit !== null && $newUsedCount >= $voucher->usage_limit;
                $voucher->update(['used_count' => $newUsedCount]);
            }

            if ($exhausted) {
                $voucher->update(['status_id' => Status::idOf(Status::CTX_VOUCHER, Status::VOUCHER_EXHAUSTED)]);
            }

            $redemption = VoucherRedemption::create([
                'voucher_id' => $voucher->id,
                'invoice_id' => $invoice->id,
                'customer_id' => $invoice->customer_id,
                'redeemed_amount' => $amount,
                'balance_before' => $balanceBefore,
                'balance_after' => $balanceAfter,
                'redeemed_by' => Auth::id(),
                'redeemed_at' => now(),
            ]);

            AuditLog::record('voucher.redeemed', $voucher, [
                'code' => $voucher->code,
                'invoice' => $invoice->invoice_number,
                'amount' => (string) $amount,
            ]);

            return $redemption;
        });
    }

    /** Reverse a redemption: restore balance/count, un-exhaust if applicable. */
    public function reverse(VoucherRedemption $redemption, string $reason): void
    {
        if ($redemption->is_reversed) {
            return;
        }

        DB::transaction(function () use ($redemption, $reason) {
            $voucher = $redemption->voucher;

            if ($voucher->isBalanceBased()) {
                $voucher->update(['remaining_balance' => (float) $voucher->remaining_balance + (float) $redemption->redeemed_amount]);
            }
            $voucher->update(['used_count' => max(0, $voucher->used_count - 1)]);

            if ($voucher->status?->code === Status::VOUCHER_EXHAUSTED) {
                $voucher->update(['status_id' => Status::idOf(Status::CTX_VOUCHER, Status::VOUCHER_ACTIVE)]);
            }

            $redemption->update([
                'is_reversed' => true,
                'reversed_at' => now(),
                'reversed_by' => Auth::id(),
                'reverse_reason' => $reason,
            ]);

            AuditLog::record('voucher.redemption_reversed', $voucher, ['reason' => $reason]);
        });
    }

    /**
     * Generate a voucher with a random, collision-checked code — used for
     * refund/compensation vouchers issued from Returns/CS flows.
     *
     * @param  array<string,mixed>  $attributes
     */
    public function generate(array $attributes): Voucher
    {
        do {
            $code = 'V-'.Str::upper(Str::random(8));
        } while (Voucher::where('code', $code)->exists());

        $voucher = Voucher::create($attributes + [
            'code' => $code,
            'status_id' => Status::idOf(Status::CTX_VOUCHER, Status::VOUCHER_ACTIVE),
        ]);

        AuditLog::record('voucher.generated', $voucher, ['code' => $voucher->code]);

        return $voucher;
    }

    /** Bulk-flag past-valid_until vouchers as expired. Mirrors BatchService::flagExpiredBatches(). */
    public function expireDue(): int
    {
        $expiredId = Status::idOf(Status::CTX_VOUCHER, Status::VOUCHER_EXPIRED);
        $activeId = Status::idOf(Status::CTX_VOUCHER, Status::VOUCHER_ACTIVE);

        if (! $expiredId || ! $activeId) {
            return 0;
        }

        return Voucher::query()
            ->where('status_id', $activeId)
            ->whereNotNull('valid_until')
            ->where('valid_until', '<', now())
            ->update(['status_id' => $expiredId]);
    }
}
