<?php

declare(strict_types=1);

namespace App\Services\Fees;

use App\Enums\FeeType;
use App\Models\Fee;
use App\Support\Money\Money;

/**
 * A fee that has been resolved and computed for a specific amount.
 *
 * Carries the full derivation, not just the total: the base rule that set the
 * rate and every adjustment that moved it. Orders persist this snapshot, so a
 * later change to the fee table can never alter what a historical order says it
 * charged -- and, just as importantly, a seller asking why they were charged 7%
 * instead of 10% can be given the actual answer.
 */
final readonly class ResolvedFee
{
    /**
     * @param  list<AppliedFeeRule>  $appliedRules
     */
    public function __construct(
        public FeeType $type,
        public string $code,
        public string $label,
        public Money $amount,
        public ?int $ruleId = null,
        public ?int $rateBps = null,
        public ?int $fixedMinor = null,
        public bool $isFallback = false,
        public array $appliedRules = [],
    ) {}

    /**
     * @param  list<AppliedFeeRule>  $applied
     */
    public static function fromRules(FeeType $type, Fee $base, Money $amount, array $applied): self
    {
        return new self(
            type: $type,
            code: (string) $base->code,
            label: (string) $base->name,
            amount: $amount,
            ruleId: (int) $base->getKey(),
            rateBps: $base->rate_bps === null ? null : (int) $base->rate_bps,
            fixedMinor: $base->fixed_amount_minor === null ? null : (int) $base->fixed_amount_minor,
            appliedRules: $applied,
        );
    }

    /** How many adjustment rules moved the base figure. */
    public function adjustmentCount(): int
    {
        return count(array_filter(
            $this->appliedRules,
            fn (AppliedFeeRule $rule): bool => $rule->role->allowsNegativeRate()
        ));
    }

    /**
     * The rate actually charged, in basis points, after adjustments.
     *
     * Derived from the computed amount rather than by summing rates, so a fixed
     * component or a clamp is reflected honestly. Null when the subject amount
     * is zero and no rate can be inferred.
     */
    public function effectiveRateBps(Money $subject): ?int
    {
        if ($subject->isZero()) {
            return null;
        }

        return (int) round($this->amount->minor * 10_000 / $subject->minor);
    }

    /** @return array<string, mixed> */
    public function toSnapshot(): array
    {
        return [
            'type' => $this->type->value,
            'code' => $this->code,
            'label' => $this->label,
            'amount_minor' => $this->amount->minor,
            'currency' => $this->amount->currency,
            'rule_id' => $this->ruleId,
            'rate_bps' => $this->rateBps,
            'fixed_minor' => $this->fixedMinor,
            'is_fallback' => $this->isFallback,
            // The full derivation, so the charge stays explainable years later.
            'applied_rules' => array_map(
                fn (AppliedFeeRule $rule): array => $rule->toArray(),
                $this->appliedRules,
            ),
        ];
    }
}
