<?php
/**
 * CustomerReceipts — money-in against customer invoices.
 *
 * A receipt is created with a total amount + payment method + optional
 * invoice allocations. The JE is:
 *   Dr <bank/cash mapped from payment_method>  (total amount)
 *   Cr 1110 Trade Receivables                    (allocated amount)
 *   Cr <on-account credit>                        (unallocated remainder — future work)
 * For MVP we require full allocation of the receipt across selected invoices
 * so the credit line always matches AR. On-account credits land in Iter 4.2.
 *
 * After posting, we call CustomerInvoices::refreshPaidState() for every
 * touched invoice so the header status updates atomically.
 */
class CustomerReceipts extends Model
{
    /**
     * Rewired to the client's COA on 2026-08-27 (Turn 1.5 · slice 21).
     * Mirror of Expenses/FuelEntries payment-method map, minus fuel_card
     * (receipts don't come in via fuel cards). Bank transfers default to
     * CRDB; NMB path is a per-receipt override once the UI exposes it.
     */
    private const AR_ACCOUNT = '3007';   // Trade Receivables (was 1110)
    public const PAYMENT_METHOD_TO_DEBIT = [
        'cash'          => '3003',   // Cash
        'petty_cash'    => '3003',   // Cash (lumped)
        'mobile_money'  => '3006',   // Mobile Money Float
        'bank_transfer' => '3001',   // Bank - CRDB
        'cheque'        => '3001',   // Bank - CRDB
    ];

    private function s($v): string { return trim((string)$v); }
    private function m($v): float  { return round((float)preg_replace('/[^\d\.\-]/', '', (string)$v), 2); }

    /* ==================== reads =========================================== */

    public function listPaginated(int $page, int $perPage, array $filters = []): array
    {
        $where  = ['r.deleted = 0', 'r.tenant_id = :tenant'];
        $params = [':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1];

        if (!empty($filters['q'])) {
            $where[] = '(r.receipt_no LIKE :q OR r.reference LIKE :q OR TRIM(CONCAT(c.fname, " ", c.lname)) LIKE :q)';
            $params[':q'] = '%' . $filters['q'] . '%';
        }
        if (!empty($filters['status']))       { $where[] = 'r.status         = :st'; $params[':st'] = $this->s($filters['status']); }
        if (!empty($filters['customer_id']))  { $where[] = 'r.customer_id    = :cid';$params[':cid']= (int)$filters['customer_id']; }
        if (!empty($filters['from_date']))    { $where[] = 'r.receipt_date  >= :fd'; $params[':fd'] = $filters['from_date']; }
        if (!empty($filters['to_date']))      { $where[] = 'r.receipt_date  <= :td'; $params[':td'] = $filters['to_date']; }
        if (!empty($filters['method']))       { $where[] = 'r.payment_method = :pm'; $params[':pm'] = $this->s($filters['method']); }

        $whereSql = implode(' AND ', $where);
        $db = new DB();

        $countRs = $db->mySelect("SELECT COUNT(*) AS n FROM customer_receipts r LEFT JOIN customers c ON c.c0d = r.customer_id WHERE $whereSql", $params);
        $total = (int)($countRs[0]['n'] ?? 0);

        $page    = max(1, $page);
        $perPage = max(1, min(200, $perPage));
        $offset  = ($page - 1) * $perPage;

        $rows = $db->mySelect(
            "SELECT r.c0d, r.receipt_no, r.receipt_date, r.amount, r.amount_allocated, r.currency,
                    r.payment_method, r.reference, r.status,
                    r.customer_id, TRIM(CONCAT(c.fname, ' ', c.lname)) AS customer_name
               FROM customer_receipts r
          LEFT JOIN customers c ON c.c0d = r.customer_id
              WHERE $whereSql
           ORDER BY r.receipt_date DESC, r.c0d DESC
              LIMIT $perPage OFFSET $offset",
            $params
        ) ?: [];

        return ['rows' => $rows, 'total' => $total];
    }

    public function byId(int $id): array
    {
        $rs = (new DB())->mySelect(
            "SELECT r.*, TRIM(CONCAT(c.fname, ' ', c.lname)) AS customer_name
               FROM customer_receipts r
          LEFT JOIN customers c ON c.c0d = r.customer_id
              WHERE r.c0d = :id AND r.deleted = 0 LIMIT 1",
            [':id' => $id]
        );
        return $rs[0] ?? [];
    }

    public function allocationsFor(int $receiptId): array
    {
        return (new DB())->mySelect(
            "SELECT a.c0d, a.invoice_id, a.amount_applied, i.invoice_no, i.total_amount, i.amount_paid, i.status
               FROM customer_receipt_allocations a
               JOIN customer_invoices i ON i.c0d = a.invoice_id
              WHERE a.receipt_id = :id
           ORDER BY i.invoice_date ASC",
            [':id' => $receiptId]
        ) ?: [];
    }

    /* ==================== writes ========================================== */

    /**
     * Record a receipt and, in one shot, allocate it to N invoices.
     * $d = [ customer_id, amount, receipt_date?, payment_method, reference?, notes?, allocations: [{invoice_id, amount}] ]
     */
    public function record(array $d, int $uid, string $uname): array
    {
        $customerId = (int)($d['customer_id'] ?? 0);
        $amount     = $this->m($d['amount'] ?? 0);
        $method     = $this->s($d['payment_method'] ?? 'bank_transfer') ?: 'bank_transfer';

        if ($customerId <= 0)       return ['ok' => false, 'msg' => 'Customer is required.'];
        if ($amount <= 0)           return ['ok' => false, 'msg' => 'Amount must be greater than zero.'];
        if (!isset(self::PAYMENT_METHOD_TO_DEBIT[$method])) return ['ok' => false, 'msg' => 'Unknown payment method.'];

        $allocations = is_array($d['allocations'] ?? null) ? $d['allocations'] : [];
        // Validate + normalise allocations
        $sumApplied = 0.0;
        $normAlloc  = [];
        foreach ($allocations as $a) {
            $iid = (int)($a['invoice_id'] ?? 0);
            $amt = $this->m($a['amount']    ?? 0);
            if ($iid <= 0 || $amt <= 0) continue;
            $sumApplied += $amt;
            $normAlloc[] = ['invoice_id' => $iid, 'amount' => $amt];
        }
        if (empty($normAlloc)) return ['ok' => false, 'msg' => 'Allocate the receipt to at least one invoice.'];
        if ($sumApplied - 0.005 > $amount) return ['ok' => false, 'msg' => 'Allocations (' . number_format($sumApplied, 2) . ') exceed the receipt amount (' . number_format($amount, 2) . ').'];

        // Validate every invoice belongs to this customer, is not void, and has room
        $ids = array_map(fn($a) => $a['invoice_id'], $normAlloc);
        $placeholders = implode(',', array_fill(0, count($ids), '?'));
        $db = new DB();
        $invs = $db->mySelect(
            "SELECT c0d, invoice_no, customer_id, status, total_amount, amount_paid, currency
               FROM customer_invoices
              WHERE c0d IN ($placeholders) AND deleted = 0",
            $ids
        ) ?: [];
        $invMap = []; foreach ($invs as $inv) $invMap[(int)$inv['c0d']] = $inv;
        foreach ($normAlloc as $a) {
            $inv = $invMap[$a['invoice_id']] ?? null;
            if (!$inv) return ['ok' => false, 'msg' => "Invoice #{$a['invoice_id']} not found."];
            if ((int)$inv['customer_id'] !== $customerId) return ['ok' => false, 'msg' => "Invoice {$inv['invoice_no']} belongs to a different customer."];
            if ($inv['status'] === 'void' || $inv['status'] === 'draft') return ['ok' => false, 'msg' => "Invoice {$inv['invoice_no']} is {$inv['status']} — cannot allocate."];
            $room = round((float)$inv['total_amount'] - (float)$inv['amount_paid'], 2);
            if ($a['amount'] - 0.005 > $room) {
                return ['ok' => false, 'msg' => "Allocation " . number_format($a['amount'], 2) . " to {$inv['invoice_no']} exceeds outstanding " . number_format($room, 2) . '.'];
            }
        }

        // Insert receipt header
        $rcptDate = $this->s($d['receipt_date'] ?? '') ?: date('Y-m-d');
        $receiptNo = $this->nextReceiptNo();
        $currency  = $invs[0]['currency'] ?? 'TZS';
        $r = $db->insertUpdate(
            "INSERT INTO customer_receipts
                (tenant_id, receipt_no, customer_id, receipt_date, amount, amount_allocated, currency,
                 payment_method, reference, notes, status, created_by, created_on)
             VALUES
                (:tenant, :rno, :cid, :rdate, :amt, :alloc, :cur, :pm, :ref, :notes, 'recorded', :uid, NOW())",
            [
                ':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1,
                ':rno'    => $receiptNo,
                ':cid'    => $customerId,
                ':rdate'  => $rcptDate,
                ':amt'    => $amount,
                ':alloc'  => round($sumApplied, 2),
                ':cur'    => $currency,
                ':pm'     => $method,
                ':ref'    => $this->s($d['reference'] ?? ''),
                ':notes'  => $this->s($d['notes'] ?? ''),
                ':uid'    => $uid,
            ]
        );
        $receiptId = (int)$r['last_id'];
        if (!$receiptId) return ['ok' => false, 'msg' => 'Could not save receipt.'];

        foreach ($normAlloc as $a) {
            $db->insertUpdate(
                "INSERT INTO customer_receipt_allocations (receipt_id, invoice_id, amount_applied, created_on)
                 VALUES (:r, :i, :a, NOW())",
                [':r' => $receiptId, ':i' => $a['invoice_id'], ':a' => $a['amount']]
            );
        }

        // Post the JE — one debit to bank/cash, one credit to AR per invoice (tagged with customer_id + invoice via source_ref)
        $debitAcc = self::PAYMENT_METHOD_TO_DEBIT[$method];
        $jeLines  = [];
        $jeLines[] = [
            'account_code' => $debitAcc,
            'debit'        => round($sumApplied, 2),
            'narrative'    => "Receipt {$receiptNo} — {$method}",
            'customer_id'  => $customerId,
        ];
        foreach ($normAlloc as $a) {
            $inv = $invMap[$a['invoice_id']];
            $jeLines[] = [
                'account_code' => self::AR_ACCOUNT,
                'credit'       => round($a['amount'], 2),
                'narrative'    => "AR paid — Invoice {$inv['invoice_no']}",
                'customer_id'  => $customerId,
            ];
        }
        // Balance check: if the receipt has an unallocated remainder for MVP we simply
        // reduce the debit to the allocated total. The receipt row already stores the full amount.
        // (On-account credit posting arrives in a follow-up slice.)

        $jp = new JournalPosting();
        $res = $jp->post([
            'entry_date'     => $rcptDate,
            'source_type'    => 'RECEIPT_APPLY',
            'source_id'      => $receiptId,
            'source_ref'     => $receiptNo,
            'narrative'      => "Customer receipt {$receiptNo}",
            'currency'       => $currency,
            'posted_by'      => $uid,
            'posted_by_name' => $uname,
            'lines'          => $jeLines,
        ]);
        if (!$res['ok']) {
            // Roll back the receipt+allocations if the JE fails so we don't leave orphans
            $db->insertUpdate("DELETE FROM customer_receipt_allocations WHERE receipt_id = :r", [':r' => $receiptId]);
            $db->insertUpdate("DELETE FROM customer_receipts WHERE c0d = :r", [':r' => $receiptId]);
            return ['ok' => false, 'msg' => 'JE failed: ' . $res['msg']];
        }

        $db->insertUpdate(
            "UPDATE customer_receipts SET journal_entry_id = :je WHERE c0d = :id",
            [':je' => $res['id'], ':id' => $receiptId]
        );

        // Bump every touched invoice's amount_paid + status
        $invModel = new CustomerInvoices();
        foreach ($normAlloc as $a) $invModel->refreshPaidState($a['invoice_id']);

        return ['ok' => true, 'msg' => "Receipt {$receiptNo} recorded · JE {$res['je_no']}.", 'id' => $receiptId, 'receipt_no' => $receiptNo];
    }

    /** Void: reverses the JE and clears allocations from the affected invoices. */
    public function voidReceipt(int $id, string $reason, int $uid, string $uname): array
    {
        $r = $this->byId($id);
        if (!$r)                          return ['ok' => false, 'msg' => 'Receipt not found.'];
        if ($r['status'] === 'void')      return ['ok' => false, 'msg' => 'Receipt is already void.'];

        $db = new DB();
        $allocs = $this->allocationsFor($id);

        // Reverse JE
        if ($r['journal_entry_id']) {
            $debitAcc = self::PAYMENT_METHOD_TO_DEBIT[$r['payment_method']] ?? '1020';
            $lines = [];
            $lines[] = ['account_code' => $debitAcc, 'credit' => (float)$r['amount_allocated'], 'narrative' => "Reverse receipt {$r['receipt_no']}", 'customer_id' => (int)$r['customer_id']];
            foreach ($allocs as $a) {
                $lines[] = ['account_code' => self::AR_ACCOUNT, 'debit' => (float)$a['amount_applied'], 'narrative' => "Reverse AR — Void receipt {$r['receipt_no']}", 'customer_id' => (int)$r['customer_id']];
            }
            $jp = new JournalPosting();
            $res = $jp->post([
                'entry_date' => date('Y-m-d'), 'source_type' => 'RECEIPT_VOID',
                'source_id' => $id, 'source_ref' => $r['receipt_no'],
                'narrative' => "Void receipt {$r['receipt_no']}: {$reason}",
                'currency' => $r['currency'], 'posted_by' => $uid, 'posted_by_name' => $uname,
                'lines' => $lines,
            ]);
            if (!$res['ok']) return ['ok' => false, 'msg' => 'Reversal JE failed: ' . $res['msg']];
        }

        $db->insertUpdate(
            "UPDATE customer_receipts SET status='void', void_by=:uid, void_at=NOW(), void_reason=:r, edited_by=:uid, edited_on=NOW() WHERE c0d = :id",
            [':uid' => $uid, ':r' => $this->s($reason), ':id' => $id]
        );

        // Refresh every touched invoice (their paid state drops)
        $invModel = new CustomerInvoices();
        foreach ($allocs as $a) $invModel->refreshPaidState((int)$a['invoice_id']);

        return ['ok' => true, 'msg' => 'Receipt voided.'];
    }

    private function nextReceiptNo(): string
    {
        $year = (int)date('Y');
        $rs = (new DB())->mySelect(
            "SELECT receipt_no FROM customer_receipts WHERE receipt_no LIKE :p ORDER BY c0d DESC LIMIT 1",
            [':p' => 'RCPT-' . $year . '-%']
        );
        $seq = 1;
        if (!empty($rs) && preg_match('/-(\d+)$/', $rs[0]['receipt_no'], $m)) $seq = (int)$m[1] + 1;
        return sprintf('RCPT-%d-%04d', $year, $seq);
    }
}
