<?php

namespace App\Services;

use App\Models\Customers\Customer;
use App\Models\Lookups\LoyaltyTransactionType;
use App\Models\Loyalty\CustomerLoyaltyBalance;
use App\Models\Loyalty\LoyaltyPointTransaction;
use App\Models\Loyalty\LoyaltyProgram;
use App\Models\Loyalty\LoyaltyTier;
use App\Models\Sales\Invoice;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * Core loyalty engine: earn, redeem, tier evaluation. All balance mutations
 * go through here — never write to customer_loyalty_balances directly.
 *
 * Deliberately does not include a campaign/rules engine or rewards catalog —
 * those are a future module. Every rate/threshold here is admin-configurable
 * on the LoyaltyProgram/LoyaltyTier records, not hardcoded.
 */
class LoyaltyService
{
    /**
     * The program currently in effect: switched on AND inside its
     * started_at/ended_at window (an open-ended bound means "no limit").
     * Every earn/redeem decision funnels through here, so a program that is
     * deactivated or outside its schedule stops applying immediately.
     */
    public function activeProgram(): ?LoyaltyProgram
    {
        return LoyaltyProgram::where('is_active', true)
            ->where(fn ($q) => $q->whereNull('started_at')->orWhere('started_at', '<=', now()))
            ->where(fn ($q) => $q->whereNull('ended_at')->orWhere('ended_at', '>=', now()))
            ->first();
    }

    /**
     * Earn points on a completed sale. Skipped silently if loyalty is switched
     * off for the workspace, there's no active program, or the sale is below
     * the program's minimum purchase to earn.
     */
    public function earn(Customer $customer, Invoice $invoice): ?LoyaltyPointTransaction
    {
        if (! app(PosSettings::class)->loyaltyEnabled()) {
            return null;
        }

        $program = $this->activeProgram();
        if (! $program || (float) $invoice->net_total < (float) $program->minimum_purchase_to_earn) {
            return null;
        }

        return DB::transaction(function () use ($customer, $invoice, $program) {
            $balance = $this->balanceFor($customer, $program);

            $multiplier = (float) ($balance->tier?->earn_multiplier ?? 1);
            $points = round((float) $invoice->net_total * (float) $program->base_earn_rate * $multiplier, 2);

            if ($points <= 0) {
                return null;
            }

            $newPointsBalance = (float) $balance->points_balance + $points;
            $newLifetime = (float) $balance->lifetime_points + $points;
            $balance->update(['points_balance' => $newPointsBalance, 'lifetime_points' => $newLifetime]);

            $transaction = LoyaltyPointTransaction::create([
                'customer_id' => $customer->id,
                'program_id' => $program->id,
                'invoice_id' => $invoice->id,
                'loyalty_transaction_type_id' => LoyaltyTransactionType::idOf(LoyaltyTransactionType::EARN),
                'points' => $points,
                'balance_after' => $newPointsBalance,
                'expires_at' => $program->points_validity_days ? now()->addDays($program->points_validity_days)->toDateString() : null,
                'description' => "Earned on invoice {$invoice->invoice_number}",
                'created_by' => Auth::id(),
            ]);

            $this->evaluateTier($balance->fresh());

            AuditLog::record('loyalty.earned', $transaction, ['points' => (string) $points, 'invoice' => $invoice->invoice_number]);

            return $transaction;
        });
    }

    /**
     * Redeem points for a monetary discount at checkout. Returns the
     * monetary value redeemed.
     */
    public function redeem(Customer $customer, float $points, ?Invoice $invoice = null): float
    {
        if (! app(PosSettings::class)->loyaltyEnabled()) {
            throw new RuntimeException('Loyalty programs are disabled for this workspace.');
        }

        $program = $this->activeProgram();
        if (! $program) {
            throw new RuntimeException('No active loyalty program.');
        }
        if ($points < (float) $program->minimum_points_to_redeem) {
            throw new RuntimeException("A minimum of {$program->minimum_points_to_redeem} points is required to redeem.");
        }

        return DB::transaction(function () use ($customer, $points, $invoice, $program) {
            $balance = $this->balanceFor($customer, $program);

            if ((float) $balance->points_balance < $points) {
                throw new RuntimeException('Insufficient loyalty points balance.');
            }

            $value = round($points / (float) $program->redemption_rate, 2);
            $newBalance = (float) $balance->points_balance - $points;
            $balance->update(['points_balance' => $newBalance]);

            LoyaltyPointTransaction::create([
                'customer_id' => $customer->id,
                'program_id' => $program->id,
                'invoice_id' => $invoice?->id,
                'loyalty_transaction_type_id' => LoyaltyTransactionType::idOf(LoyaltyTransactionType::REDEEM),
                'points' => -$points,
                'balance_after' => $newBalance,
                'description' => $invoice ? "Redeemed at checkout on invoice {$invoice->invoice_number}" : 'Manual redemption',
                'created_by' => Auth::id(),
            ]);

            AuditLog::record('loyalty.redeemed', $customer, ['points' => (string) $points, 'value' => (string) $value]);

            return $value;
        });
    }

    /** Reverses all earn/redeem transactions tied to a deleted invoice. */
    public function reverse(Invoice $invoice): void
    {
        $transactions = LoyaltyPointTransaction::where('invoice_id', $invoice->id)->where('is_reversed', false)->get();

        foreach ($transactions as $transaction) {
            DB::transaction(function () use ($transaction) {
                $balance = CustomerLoyaltyBalance::where('customer_id', $transaction->customer_id)
                    ->where('program_id', $transaction->program_id)
                    ->lockForUpdate()
                    ->first();

                if (! $balance) {
                    return;
                }

                $newPointsBalance = (float) $balance->points_balance - (float) $transaction->points;
                $newLifetime = (float) $balance->lifetime_points - max(0, (float) $transaction->points);
                $balance->update([
                    'points_balance' => $newPointsBalance,
                    'lifetime_points' => max(0, $newLifetime),
                ]);

                $transaction->update(['is_reversed' => true, 'reversed_at' => now()]);

                $this->evaluateTier($balance->fresh());
            });
        }
    }

    /** Pick the highest tier the balance's lifetime points qualify for; update if changed. */
    public function evaluateTier(CustomerLoyaltyBalance $balance): void
    {
        $tier = LoyaltyTier::where('program_id', $balance->program_id)
            ->where('min_lifetime_points', '<=', (float) $balance->lifetime_points)
            ->orderByDesc('min_lifetime_points')
            ->first();

        if ($tier && $tier->id !== $balance->tier_id) {
            $balance->update(['tier_id' => $tier->id, 'tier_evaluated_at' => now()]);
            AuditLog::record('loyalty.tier_changed', $balance, ['tier' => $tier->name]);
        }
    }

    /** Manual admin adjustment (audit-logged, positive or negative). */
    public function adjust(Customer $customer, float $points, string $description): void
    {
        if (! app(PosSettings::class)->loyaltyEnabled()) {
            throw new RuntimeException('Loyalty programs are disabled for this workspace.');
        }

        $program = $this->activeProgram();
        if (! $program) {
            throw new RuntimeException('No active loyalty program.');
        }

        DB::transaction(function () use ($customer, $points, $description, $program) {
            $balance = $this->balanceFor($customer, $program);
            $newBalance = max(0, (float) $balance->points_balance + $points);
            $newLifetime = $points > 0 ? (float) $balance->lifetime_points + $points : (float) $balance->lifetime_points;
            $balance->update(['points_balance' => $newBalance, 'lifetime_points' => $newLifetime]);

            LoyaltyPointTransaction::create([
                'customer_id' => $customer->id,
                'program_id' => $program->id,
                'loyalty_transaction_type_id' => LoyaltyTransactionType::idOf(LoyaltyTransactionType::ADJUST),
                'points' => $points,
                'balance_after' => $newBalance,
                'description' => $description,
                'created_by' => Auth::id(),
            ]);

            $this->evaluateTier($balance->fresh());

            AuditLog::record('loyalty.adjusted', $customer, ['points' => (string) $points, 'reason' => $description]);
        });
    }

    /** Expire ledger rows past their expires_at date for programs with points_validity_days set. */
    public function expireDue(): int
    {
        $expireTypeId = LoyaltyTransactionType::idOf(LoyaltyTransactionType::EXPIRE);
        $due = LoyaltyPointTransaction::query()
            ->whereNotNull('expires_at')
            ->where('expires_at', '<', now()->toDateString())
            ->where('is_reversed', false)
            ->whereIn('loyalty_transaction_type_id', [LoyaltyTransactionType::idOf(LoyaltyTransactionType::EARN)])
            ->get();

        $count = 0;
        foreach ($due as $earnTransaction) {
            DB::transaction(function () use ($earnTransaction, $expireTypeId) {
                $balance = CustomerLoyaltyBalance::where('customer_id', $earnTransaction->customer_id)
                    ->where('program_id', $earnTransaction->program_id)
                    ->lockForUpdate()
                    ->first();

                if (! $balance) {
                    return;
                }

                $expiring = min((float) $earnTransaction->points, (float) $balance->points_balance);
                $newBalance = (float) $balance->points_balance - $expiring;
                $balance->update(['points_balance' => $newBalance]);

                LoyaltyPointTransaction::create([
                    'customer_id' => $earnTransaction->customer_id,
                    'program_id' => $earnTransaction->program_id,
                    'loyalty_transaction_type_id' => $expireTypeId,
                    'points' => -$expiring,
                    'balance_after' => $newBalance,
                    'description' => 'Points expired',
                ]);

                $earnTransaction->update(['is_reversed' => true, 'reversed_at' => now()]);
            });
            $count++;
        }

        return $count;
    }

    private function balanceFor(Customer $customer, LoyaltyProgram $program): CustomerLoyaltyBalance
    {
        return CustomerLoyaltyBalance::firstOrCreate(
            ['customer_id' => $customer->id, 'program_id' => $program->id],
            ['points_balance' => 0, 'lifetime_points' => 0],
        );
    }
}
