<?php

namespace App\Domain\Financial\Services;

use App\Domain\Participant\Models\Participant;
use Illuminate\Support\Facades\DB;

/**
 * How much a participant has actually paid toward one part of their bill.
 *
 * Invoices here are routinely bundled — a subscription and a registration card
 * on the same invoice, sometimes several products — and a payment lands on the
 * invoice, never on a line. So "how much did they pay for the card" can only be
 * answered by allocating each payment across the lines in proportion to their
 * share of the invoice.
 *
 * The denominator is subtotal_amount (the sum of the line totals), not
 * total_amount: Invoice::recalculateTotals() defines
 * total = subtotal - discount + tax + service_fee, so dividing by total makes
 * the shares exceed 1 whenever a header discount exists.
 *
 * All amounts are piastres. Integer arithmetic only.
 */
class ParticipantBillingService
{
    /**
     * Piastres paid toward programme subscription lines over ALL TIME, keyed by
     * participant id.
     *
     * This is a lifetime total and answers "how much has this person ever paid
     * us for subscriptions" — it is NOT the current month. A roster showing it
     * beside a monthly bill reads as though someone paid 2,500 for one month.
     * For anything cycle-shaped, use subscriptionForPeriod().
     *
     * A subscription line is one with no itemable — products and kits carry a
     * morph. (POS sales historically left that null, which is what made product
     * sales read as subscription revenue; see the 2026_09_01 backfill.)
     */
    public function subscriptionPaid(array $participantIds): array
    {
        if (empty($participantIds)) {
            return [];
        }

        return $this->allocate($participantIds, function ($q) {
            $q->whereNull('invoice_items.itemable_type');
        });
    }

    /**
     * What each participant was billed and has paid for programme subscription
     * within ONE billing cycle, plus why the figure is what it is.
     *
     * `billed` is what the subscription lines came to; `due` is that share of
     * the invoice total, so a header discount lowers what they actually owe
     * while `billed` still shows the undiscounted price. Comparing the two is
     * what lets the roster say "650 of 900, because an admin discounted it"
     * rather than just "650".
     *
     * $periodStart is inclusive, $periodEnd exclusive, both Y-m-d.
     *
     * @return array<int, array{billed:int, due:int, paid:int, prorated:bool,
     *     proration_label:?string, admin_override:?array, price_override:?array,
     *     has_plan:bool, plan:?array, invoice_numbers:array<int,string>}>
     */
    public function subscriptionForPeriod(array $participantIds, string $periodStart, string $periodEnd): array
    {
        if (empty($participantIds)) {
            return [];
        }

        $invoices = DB::table('invoices')
            ->where('billable_type', Participant::class)
            ->whereIn('billable_id', $participantIds)
            ->whereNull('deleted_at')
            ->where('status', '!=', 'cancelled')
            ->where('subtotal_amount', '>', 0)
            ->where('issue_date', '>=', $periodStart)
            ->where('issue_date', '<', $periodEnd)
            ->get(['id', 'number', 'billable_id', 'subtotal_amount', 'total_amount', 'metadata']);

        if ($invoices->isEmpty()) {
            return [];
        }

        $invoiceIds = $invoices->pluck('id')->all();

        // Subscription lines only — anything carrying a morph is a product or a
        // kit and belongs in its own column, not in the subscription figure.
        $items = DB::table('invoice_items')
            ->whereIn('invoice_id', $invoiceIds)
            ->whereNull('itemable_type')
            ->get(['invoice_id', 'description', 'total_amount', 'metadata'])
            ->groupBy('invoice_id');

        $payments = DB::table('payments')
            ->whereIn('invoice_id', $invoiceIds)
            ->where('status', 'confirmed')
            ->where('direction', 'inbound')
            ->whereNull('deleted_at')
            ->groupBy('invoice_id')
            ->select('invoice_id', DB::raw('SUM(amount) as paid'))
            ->pluck('paid', 'invoice_id');

        $plans = DB::table('payment_plans')
            ->whereIn('invoice_id', $invoiceIds)
            ->whereIn('status', ['active', 'partial'])
            ->get(['invoice_id', 'total_installments', 'paid_installments', 'installment_amount'])
            ->keyBy('invoice_id');

        $out = [];

        foreach ($invoices as $invoice) {
            $lines = $items[$invoice->id] ?? collect();

            $billed = (int) $lines->sum('total_amount');
            if ($billed <= 0) {
                continue;
            }

            $subtotal = (int) $invoice->subtotal_amount;
            $total = (int) $invoice->total_amount;
            $paidOnInvoice = (int) ($payments[$invoice->id] ?? 0);

            // Round down throughout; an unallocated remainder is honest, money
            // conjured by rounding up is not.
            $due = min(intdiv($total * $billed, $subtotal), $billed);
            $paid = min(intdiv($paidOnInvoice * $billed, $subtotal), $billed);

            $pid = (int) $invoice->billable_id;
            $row = $out[$pid] ?? [
                'billed' => 0,
                'due' => 0,
                'paid' => 0,
                'prorated' => false,
                'proration_label' => null,
                'admin_override' => null,
                'price_override' => null,
                'has_plan' => false,
                'plan' => null,
                'invoice_numbers' => [],
            ];

            $row['billed'] += $billed;
            $row['due'] += $due;
            $row['paid'] += $paid;
            $row['invoice_numbers'][] = $invoice->number;

            foreach ($lines as $line) {
                // Proration is recorded only in the line text the pricing engine
                // wrote ("متناسب: 13 من 30 يوم"), so that is where it is read from.
                if ($line->description && str_contains($line->description, 'متناسب')) {
                    $row['prorated'] = true;
                    $row['proration_label'] ??= $line->description;
                }

                $meta = $this->decodeMetadata($line->metadata);
                if (! empty($meta['overridden_by'])) {
                    $row['price_override'] ??= [
                        'by' => $meta['overridden_by'],
                        'reason' => $meta['override_reason'] ?? null,
                        'original' => (int) ($meta['original_price'] ?? 0),
                        'final' => (int) ($meta['overridden_price'] ?? 0),
                    ];
                }
            }

            $invoiceMeta = $this->decodeMetadata($invoice->metadata);
            if (! empty($invoiceMeta['admin_override'])) {
                $o = $invoiceMeta['admin_override'];
                $row['admin_override'] ??= [
                    'by' => $o['applied_by_name'] ?? null,
                    'reason' => $o['reason'] ?? null,
                    'discount' => (int) ($o['discount_piasters'] ?? 0),
                    'at' => $o['applied_at'] ?? null,
                ];
            }

            if ($plan = $plans[$invoice->id] ?? null) {
                $row['has_plan'] = true;
                $row['plan'] ??= [
                    'paid' => (int) $plan->paid_installments,
                    'total' => (int) $plan->total_installments,
                    'amount' => (int) $plan->installment_amount,
                ];
            }

            $out[$pid] = $row;
        }

        return $out;
    }

    private function decodeMetadata(mixed $raw): array
    {
        if (is_array($raw)) {
            return $raw;
        }

        if (! is_string($raw) || $raw === '') {
            return [];
        }

        $decoded = json_decode($raw, true);

        return is_array($decoded) ? $decoded : [];
    }

    /**
     * Piastres paid toward one product, keyed by participant id.
     */
    public function productPaid(array $participantIds, int $productId): array
    {
        if (empty($participantIds)) {
            return [];
        }

        return $this->allocate($participantIds, function ($q) use ($productId) {
            $q->where('invoice_items.itemable_type', \App\Domain\Inventory\Models\Product::class)
                ->where('invoice_items.itemable_id', $productId);
        });
    }

    /**
     * What each participant was billed for one product, keyed by participant id.
     *
     * Billed is not the same as paid — someone paying in instalments is billed
     * the full amount on day one. Both numbers are needed to show progress.
     *
     * @return array<int, array{billed:int, quantity:int}>
     */
    public function productBilled(array $participantIds, int $productId): array
    {
        if (empty($participantIds)) {
            return [];
        }

        $rows = DB::table('invoice_items')
            ->join('invoices', 'invoices.id', '=', 'invoice_items.invoice_id')
            ->where('invoice_items.itemable_type', \App\Domain\Inventory\Models\Product::class)
            ->where('invoice_items.itemable_id', $productId)
            ->where('invoices.billable_type', Participant::class)
            ->whereIn('invoices.billable_id', $participantIds)
            ->whereNull('invoices.deleted_at')
            ->where('invoices.status', '!=', 'cancelled')
            ->groupBy('invoices.billable_id')
            ->select(
                'invoices.billable_id',
                DB::raw('SUM(invoice_items.total_amount) as billed'),
                DB::raw('SUM(invoice_items.quantity) as quantity')
            )
            ->get();

        $out = [];
        foreach ($rows as $r) {
            $out[(int) $r->billable_id] = [
                'billed' => (int) $r->billed,
                'quantity' => (int) $r->quantity,
            ];
        }

        return $out;
    }

    /**
     * Allocate confirmed inbound payments across the lines matched by $filter,
     * in proportion to those lines' share of each invoice.
     *
     * @return array<int, int> participant id => piastres
     */
    private function allocate(array $participantIds, callable $filter): array
    {
        // Share of each invoice represented by the lines we care about.
        $shares = DB::table('invoice_items')
            ->join('invoices', 'invoices.id', '=', 'invoice_items.invoice_id')
            ->where('invoices.billable_type', Participant::class)
            ->whereIn('invoices.billable_id', $participantIds)
            ->whereNull('invoices.deleted_at')
            ->where('invoices.status', '!=', 'cancelled')
            ->where('invoices.subtotal_amount', '>', 0)
            ->tap($filter)
            ->groupBy('invoice_items.invoice_id')
            ->select('invoice_items.invoice_id', DB::raw('SUM(invoice_items.total_amount) as part_total'))
            ->get()
            ->keyBy('invoice_id');

        if ($shares->isEmpty()) {
            return [];
        }

        $rows = DB::table('payments')
            ->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
            ->whereIn('payments.invoice_id', $shares->keys()->all())
            ->where('payments.status', 'confirmed')
            ->where('payments.direction', 'inbound')
            ->whereNull('payments.deleted_at')
            ->groupBy('invoices.billable_id', 'payments.invoice_id', 'invoices.subtotal_amount')
            ->select(
                'invoices.billable_id',
                'payments.invoice_id',
                'invoices.subtotal_amount',
                DB::raw('SUM(payments.amount) as paid')
            )
            ->get();

        $out = [];
        foreach ($rows as $r) {
            $part = (int) ($shares[$r->invoice_id]->part_total ?? 0);
            $subtotal = (int) $r->subtotal_amount;

            if ($part <= 0 || $subtotal <= 0) {
                continue;
            }

            // Round down; the remainder stays unallocated rather than inventing
            // money that was never paid.
            $allocated = intdiv((int) $r->paid * $part, $subtotal);

            // The share is of subtotal_amount, but a payment settles
            // total_amount — which also carries tax and service fees. Paying an
            // invoice in full therefore allocates slightly more than the line
            // was billed, and a progress bar would read over 100%. Nobody can
            // have paid more toward a line than it cost.
            $allocated = min($allocated, $part);

            $pid = (int) $r->billable_id;
            $out[$pid] = ($out[$pid] ?? 0) + $allocated;
        }

        return $out;
    }
}
