<?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;
use Livewire\Attributes\Locked;

/**
 * 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.
 *
 * Everything except the three manual-discount inputs is #[Locked]. The picker
 * is driven entirely by wire:click on applyDiscount()/removeDiscount(), so
 * nothing needs to arrive from the browser; left unlocked, the candidate list
 * and the selection were both a price the browser could name.
 */
trait ManagesDiscounts
{
    /** @var array<int, array> Rendered candidates from PricingService::explain(). */
    #[Locked]
    public array $discountCandidates = [];

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

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

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

    #[Locked]
    public int $discountBaseAmount = 0;

    #[Locked]
    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.
     *
     * The academy's global discount ceiling applies here exactly as it does at
     * step 8 of the engine. Without it the setting only governed the discounts
     * nobody chose: a receptionist stacking three rules by hand walked straight
     * past a limit the same academy had set to fifty percent.
     */
    public function selectedDiscountTotal(): int
    {
        $total = 0;

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

        $total += $this->manualDiscountPiasters();

        return max(0, min(
            $total,
            $this->discountBaseAmount,
            app(PricingService::class)->maxDiscountFor($this->discountBaseAmount),
        ));
    }

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

        foreach ($this->honouredSelection() as $candidate) {
            $lines[] = [
                'rule_id'  => $candidate['rule_id'],
                'name'     => $candidate['name'],
                'discount' => (int) $candidate['discount'],
            ];
        }

        if ($this->manualDiscountPiasters() > 0) {
            $lines[] = [
                'rule_id'  => null,
                'name'     => ManualDiscountReason::from($this->manualDiscountReason)->label(),
                'discount' => $this->manualDiscountPiasters(),
                'note'     => $this->manualDiscountNote ?: null,
                'by'       => auth()->id(),
            ];
        }

        return $lines;
    }

    /**
     * The selected candidates the engine is willing to stand behind.
     *
     * applyDiscount() already refuses a blocked rule and turns one above the
     * operator's ceiling into an approval request. Those guards only hold while
     * the money is computed from the same verdict they were given, so it is
     * re-read here rather than trusted from the selection list.
     */
    protected function honouredSelection(): array
    {
        $honoured = [];

        foreach ($this->selectedDiscountIds as $id) {
            $candidate = $this->findCandidate($id);

            if ($candidate === null) {
                continue;
            }

            if (! in_array($candidate['state'], [
                DiscountCandidate::APPLIED,
                DiscountCandidate::AVAILABLE,
                DiscountCandidate::STACKS,
            ], true)) {
                continue;
            }

            $honoured[] = $candidate;
        }

        return $honoured;
    }

    /**
     * The manual discount, in piasters, or nothing when it is not one this
     * actor may give. validateManualDiscount() reports the refusal to the
     * screen; this keeps the refused amount out of the arithmetic either way.
     */
    protected function manualDiscountPiasters(): int
    {
        if ($this->manualDiscountAmount === '' || $this->manualDiscountReason === '') {
            return 0;
        }

        if (! ManualDiscountReason::tryFrom($this->manualDiscountReason)) {
            return 0;
        }

        $piasters = (int) round((float) $this->manualDiscountAmount * 100);

        $check = app(PricingService::class)->checkManualDiscount(
            $this->discountBaseAmount,
            $piasters,
            $this->actorRoleLevel(),
        );

        return $check['allowed'] ? $piasters : 0;
    }

    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);
    }
}
