<?php

namespace App\Domain\Financial\Enums;

/**
 * Why a participant's subscription figure for the current billing cycle is the
 * number it is.
 *
 * The group roster used to show a bare amount, which answered "how much" but
 * never "why" — so a coach could not tell 650 collected at the member rate from
 * 650 collected after a half-price admin discount on a 1,300 bill. Each case
 * below is the single most important reason the figure differs from the
 * programme's standard price, and carries the colour the amount is painted in.
 *
 * Computed at read time from invoices, payments and their metadata. It is never
 * persisted, so unlike every other enum in this codebase it has no CHECK
 * constraint to match (see docs/agent-rules/16-enums-and-checks.md).
 */
enum SubscriptionPaymentCase: string
{
    /** Participant flagged is_free — never billed at all. */
    case Free = 'free';

    /** No subscription invoice exists for this cycle yet. */
    case NotBilled = 'not_billed';

    /** Billed for this cycle, nothing collected. */
    case Unpaid = 'unpaid';

    /** Settled so far against an agreed instalment plan. */
    case Installment = 'installment';

    /** Some money in, still short of what is due. */
    case Partial = 'partial';

    /** Settled, but an admin discounted the invoice total. */
    case AdminDiscount = 'admin_discount';

    /** Settled, but the line price itself was overridden at sale time. */
    case PriceOverride = 'price_override';

    /** Settled at a mid-month pro-rated share of the full price. */
    case Prorated = 'prorated';

    /** Settled in full at the programme's standard price. */
    case Full = 'full';

    /**
     * Highest-priority case first. An unsettled bill outranks the reason the
     * bill was unusual: a coach needs to see missing money before they need to
     * see why the amount was discounted.
     */
    public const PRECEDENCE = [
        self::Free,
        self::NotBilled,
        self::Unpaid,
        self::Installment,
        self::Partial,
        self::AdminDiscount,
        self::PriceOverride,
        self::Prorated,
        self::Full,
    ];

    /**
     * Decide the single case that best explains one cycle's figure.
     *
     * $facts is one row of ParticipantBillingService::subscriptionForPeriod(),
     * or null when that cycle produced no subscription invoice at all.
     */
    public static function classify(?array $facts, bool $isFree): self
    {
        if ($isFree) {
            return self::Free;
        }

        if ($facts === null || ($facts['billed'] ?? 0) <= 0) {
            return self::NotBilled;
        }

        $due = (int) $facts['due'];
        $paid = (int) $facts['paid'];

        if ($paid <= 0) {
            return self::Unpaid;
        }

        // An instalment plan explains a short balance; without this an agreed
        // plan reads as a shortfall and the coach chases money nobody owes yet.
        if (! empty($facts['has_plan']) && $paid < $due) {
            return self::Installment;
        }

        if ($paid < $due) {
            return self::Partial;
        }

        // Settled. What remains is explaining why the sum is not the standard
        // programme price — most specific reason first.
        if (! empty($facts['admin_override'])) {
            return self::AdminDiscount;
        }

        if (! empty($facts['price_override'])) {
            return self::PriceOverride;
        }

        if (! empty($facts['prorated'])) {
            return self::Prorated;
        }

        return self::Full;
    }

    /**
     * The sentence shown to the admin explaining this participant's figure.
     */
    public function explain(?array $facts): string
    {
        $money = fn (int $p) => number_format($p / 100, 0) . ' ' . __('ج.م');

        return match ($this) {
            self::Free => __('لاعب مجاني — لا تُصدر له فاتورة اشتراك'),

            self::NotBilled => __('لم تُصدر فاتورة اشتراك لهذه الدورة بعد'),

            self::Unpaid => __('مستحق :due ولم يُحصَّل منه شيء', [
                'due' => $money((int) ($facts['due'] ?? 0)),
            ]),

            self::Installment => __('تقسيط: سدد :paid من :due (قسط :n من :total)', [
                'paid' => $money((int) ($facts['paid'] ?? 0)),
                'due' => $money((int) ($facts['due'] ?? 0)),
                'n' => $facts['plan']['paid'] ?? 0,
                'total' => $facts['plan']['total'] ?? 0,
            ]),

            self::Partial => __('سدد :paid من :due — متبقٍ :rest', [
                'paid' => $money((int) ($facts['paid'] ?? 0)),
                'due' => $money((int) ($facts['due'] ?? 0)),
                'rest' => $money(max(0, (int) ($facts['due'] ?? 0) - (int) ($facts['paid'] ?? 0))),
            ]),

            self::AdminDiscount => __('خصم إداري :amount من أصل :billed — :reason', [
                'amount' => $money((int) ($facts['admin_override']['discount'] ?? 0)),
                'billed' => $money((int) ($facts['billed'] ?? 0)),
                'reason' => $facts['admin_override']['reason'] ?: __('بدون سبب مسجل'),
            ]) . ($facts['admin_override']['by'] ?? null ? ' (' . $facts['admin_override']['by'] . ')' : ''),

            self::PriceOverride => __('عُدِّل السعر من :from إلى :to — :reason', [
                'from' => $money((int) ($facts['price_override']['original'] ?? 0)),
                'to' => $money((int) ($facts['price_override']['final'] ?? 0)),
                'reason' => $facts['price_override']['reason'] ?: __('بدون سبب مسجل'),
            ]) . ($facts['price_override']['by'] ?? null ? ' (' . $facts['price_override']['by'] . ')' : ''),

            self::Prorated => __('اشترك بعد بداية الشهر — :label', [
                'label' => $facts['proration_label'] ?? __('حُسب بالأيام المتبقية'),
            ]),

            self::Full => __('دفع سعر البرنامج كاملاً (:due)', [
                'due' => $money((int) ($facts['due'] ?? 0)),
            ]),
        };
    }

    public function label(): string
    {
        return match ($this) {
            self::Free => __('لاعب مجاني'),
            self::NotBilled => __('لم تُصدر فاتورة'),
            self::Unpaid => __('لم يدفع'),
            self::Installment => __('أقساط'),
            self::Partial => __('دفع جزئي'),
            self::AdminDiscount => __('خصم إداري'),
            self::PriceOverride => __('سعر معدّل'),
            self::Prorated => __('اشتراك متناسب'),
            self::Full => __('السعر القياسي'),
        };
    }

    /** One line telling the admin what the colour means, for the legend. */
    public function legend(): string
    {
        return match ($this) {
            self::Free => __('معفى من الاشتراك'),
            self::NotBilled => __('لم يُفوتر لهذه الدورة بعد'),
            self::Unpaid => __('فاتورة صادرة ولم يُحصَّل منها شيء'),
            self::Installment => __('يسدد على أقساط متفق عليها'),
            self::Partial => __('دفع جزءاً من المستحق'),
            self::AdminDiscount => __('خصم على إجمالي الفاتورة بقرار إداري'),
            self::PriceOverride => __('تم تعديل سعر البند وقت البيع'),
            self::Prorated => __('اشترك بعد بداية الشهر فحُسب بالأيام المتبقية'),
            self::Full => __('دفع سعر البرنامج كاملاً'),
        };
    }

    /**
     * Tailwind text colour for the amount itself.
     *
     * All -700 (gray -600): these sit at 10-14px, where WCAG AA wants 4.5:1
     * against white. The -500/-600 shades of amber and sky look right in a mock
     * and fail the check — amber-600 lands near 3:1.
     */
    public function textClass(): string
    {
        return match ($this) {
            self::Free => 'text-violet-700',
            self::NotBilled => 'text-gray-600',
            self::Unpaid => 'text-red-700',
            self::Installment => 'text-indigo-700',
            self::Partial => 'text-amber-700',
            self::AdminDiscount => 'text-purple-700',
            self::PriceOverride => 'text-pink-700',
            self::Prorated => 'text-sky-700',
            self::Full => 'text-green-700',
        };
    }

    /**
     * Tailwind background for the legend swatch.
     *
     * Same hue as the amount so the legend and the cell read as one system, one
     * step lighter so the swatch reads as a marker rather than a second word.
     * Kept at -600 because the swatch carries meaning, and WCAG 1.4.11 asks
     * non-text indicators for 3:1.
     */
    public function swatchClass(): string
    {
        return match ($this) {
            self::Free => 'bg-violet-600',
            self::NotBilled => 'bg-gray-500',
            self::Unpaid => 'bg-red-600',
            self::Installment => 'bg-indigo-600',
            self::Partial => 'bg-amber-600',
            self::AdminDiscount => 'bg-purple-600',
            self::PriceOverride => 'bg-pink-600',
            self::Prorated => 'bg-sky-600',
            self::Full => 'bg-green-600',
        };
    }

    /**
     * Colour alone cannot carry meaning — a colour-blind admin, or one reading a
     * printed roster, still needs the case. Shown beside the amount.
     */
    public function glyph(): string
    {
        return match ($this) {
            self::Free => '★',
            self::NotBilled => '—',
            self::Unpaid => '✕',
            self::Installment => '≡',
            self::Partial => '◐',
            self::AdminDiscount => '%',
            self::PriceOverride => '✎',
            self::Prorated => '◔',
            self::Full => '✓',
        };
    }

    /** True when the row should be flagged as owing money. */
    public function isOwing(): bool
    {
        return in_array($this, [self::Unpaid, self::Partial, self::NotBilled], true);
    }
}
