<?php

namespace App\Domain\Pricing\Concerns;

use App\Domain\Participant\Models\Participant;
use App\Domain\Pricing\Enums\ManualDiscountReason;
use App\Domain\Pricing\Models\PricingRule;
use App\Domain\Pricing\Services\DiscountCandidate;
use App\Domain\Pricing\Services\PricingService;
use Illuminate\Database\Eloquent\Model;

/**
 * Drop-in discount picker state for any checkout Livewire component.
 *
 * The engine decides; this trait only carries the receptionist's choices.
 * Nothing here re-derives eligibility — that would let the till and the
 * invoice disagree about the price.
 */
trait ManagesDiscounts
{
    /** @var array<int, array> Rendered candidates from PricingService::explain(). */
    public array $discountCandidates = [];

    /** @var array<int> Rule ids the operator explicitly turned on. */
    public array $selectedDiscountIds = [];

    /** @var array<int> Rule ids the engine applied that the operator turned off. */
    public array $declinedDiscountIds = [];

    public string $manualDiscountAmount = '';
    public string $manualDiscountReason = '';
    public string $manualDiscountNote = '';
    public ?string $manualDiscountError = null;

    public int $discountBaseAmount = 0;
    public int $discountFinalAmount = 0;

    /**
     * Ask the engine what this participant may have, at this branch, today.
     */
    public function loadDiscounts(
        Model $priceable,
        ?Participant $participant,
        ?int $branchId,
        array $extraContext = []
    ): void {
        $explained = app(PricingService::class)->explain(
            priceable: $priceable,
            participant: $participant,
            branchId: $branchId,
            extraContext: $extraContext,
            actorRoleLevel: $this->actorRoleLevel(),
        );

        $this->discountCandidates = array_map(
            fn (DiscountCandidate $c) => $c->toArray(),
            $explained['candidates']
        );

        $this->discountBaseAmount = $explained['result']->baseAmount;
        $this->discountFinalAmount = $explained['result']->finalAmount;

        // Auto-applied rules start selected unless the operator already declined them.
        foreach ($this->discountCandidates as $candidate) {
            if ($candidate['state'] === DiscountCandidate::APPLIED
                && ! in_array($candidate['rule_id'], $this->declinedDiscountIds, true)
                && ! in_array($candidate['rule_id'], $this->selectedDiscountIds, true)) {
                $this->selectedDiscountIds[] = $candidate['rule_id'];
            }
        }
    }

    public function applyDiscount(int $ruleId): void
    {
        $candidate = $this->findCandidate($ruleId);
        if (! $candidate) {
            return;
        }

        if ($candidate['state'] === DiscountCandidate::BLOCKED) {
            return;
        }

        if ($candidate['state'] === DiscountCandidate::NEEDS_APPROVAL) {
            $this->dispatch('discount-needs-approval', ruleId: $ruleId, name: $candidate['name']);

            return;
        }

        // Non-stackable discounts replace whatever non-stackable one is on.
        if (! $candidate['is_stackable']) {
            $this->selectedDiscountIds = array_values(array_filter(
                $this->selectedDiscountIds,
                fn ($id) => ($this->findCandidate($id)['is_stackable'] ?? true) === true
            ));
        }

        if (! in_array($ruleId, $this->selectedDiscountIds, true)) {
            $this->selectedDiscountIds[] = $ruleId;
        }

        $this->declinedDiscountIds = array_values(array_diff($this->declinedDiscountIds, [$ruleId]));
        $this->dispatch('discounts-changed');
    }

    public function removeDiscount(int $ruleId): void
    {
        // Declining a discount the participant is entitled to is a separate act
        // from applying one, and is gated separately.
        if (! auth()->user()?->can('pricing.discount_remove')) {
            $this->dispatch('discount-remove-denied');

            return;
        }

        $this->selectedDiscountIds = array_values(array_diff($this->selectedDiscountIds, [$ruleId]));

        if (! in_array($ruleId, $this->declinedDiscountIds, true)) {
            $this->declinedDiscountIds[] = $ruleId;
        }

        $this->dispatch('discounts-changed');
    }

    /**
     * Validate a manual discount against the actor's ceiling before it is used.
     */
    public function validateManualDiscount(): bool
    {
        $this->manualDiscountError = null;

        if (! ManualDiscountReason::tryFrom($this->manualDiscountReason)) {
            $this->manualDiscountError = __('اختر سبب الخصم');

            return false;
        }

        $reason = ManualDiscountReason::from($this->manualDiscountReason);
        if ($reason->requiresNote() && trim($this->manualDiscountNote) === '') {
            $this->manualDiscountError = __('هذا السبب يتطلب كتابة ملاحظة');

            return false;
        }

        $piasters = (int) round((float) $this->manualDiscountAmount * 100);
        $check = app(PricingService::class)->checkManualDiscount(
            $this->discountBaseAmount,
            $piasters,
            $this->actorRoleLevel()
        );

        if (! $check['allowed']) {
            $this->manualDiscountError = $check['message'];

            return false;
        }

        return true;
    }

    public function clearManualDiscount(): void
    {
        $this->manualDiscountAmount = '';
        $this->manualDiscountReason = '';
        $this->manualDiscountNote = '';
        $this->manualDiscountError = null;
    }

    /** Total discount in piasters from the operator's current selection. */
    public function selectedDiscountTotal(): int
    {
        $total = 0;

        foreach ($this->selectedDiscountIds as $id) {
            $total += (int) ($this->findCandidate($id)['discount'] ?? 0);
        }

        if ($this->manualDiscountAmount !== '') {
            $total += (int) round((float) $this->manualDiscountAmount * 100);
        }

        return min($total, $this->discountBaseAmount);
    }

    /** Snapshot for the invoice — frozen names, not ids, so receipts stay readable. */
    public function discountSnapshot(): array
    {
        $lines = [];

        foreach ($this->selectedDiscountIds as $id) {
            if ($c = $this->findCandidate($id)) {
                $lines[] = [
                    'rule_id'  => $c['rule_id'],
                    'name'     => $c['name'],
                    'discount' => (int) $c['discount'],
                ];
            }
        }

        if ($this->manualDiscountAmount !== '' && $this->manualDiscountReason !== '') {
            $lines[] = [
                'rule_id'  => null,
                'name'     => ManualDiscountReason::from($this->manualDiscountReason)->label(),
                'discount' => (int) round((float) $this->manualDiscountAmount * 100),
                'note'     => $this->manualDiscountNote ?: null,
                'by'       => auth()->id(),
            ];
        }

        return $lines;
    }

    public function getManualReasonOptionsProperty(): array
    {
        return ManualDiscountReason::options();
    }

    public function getManualDiscountCapProperty(): int
    {
        return app(PricingService::class)->manualDiscountCap($this->actorRoleLevel());
    }

    protected function findCandidate(int $ruleId): ?array
    {
        foreach ($this->discountCandidates as $candidate) {
            if ($candidate['rule_id'] === $ruleId) {
                return $candidate;
            }
        }

        return null;
    }

    protected function actorRoleLevel(): int
    {
        $user = auth()->user();

        if (! $user) {
            return 0;
        }

        if ($user->is_super_admin) {
            return 100;
        }

        return (int) ($user->roles()->max('level') ?? 0);
    }
}
