<?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, keyed by participant id.
     *
     * 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');
        });
    }

    /**
     * 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;
    }
}
