<?php
declare(strict_types=1);

namespace App\Modules\Accounting\Services;

use App\Core\App;
use App\Core\Logger;

/**
 * Auto-posting journal entries from other modules.
 * Each method is called from bootstrap.php event listeners.
 *
 * Account codes used (Egyptian standard):
 *   1101 - Cash (نقدية بالصندوق)
 *   1102 - Bank (نقدية بالبنك)
 *   1103 - Accounts Receivable (حسابات مدينة)
 *   1104 - Inventory (مخزون)
 *   2101 - Accounts Payable (حسابات دائنة)
 *   2103 - Tax Payable (ضرائب مستحقة)
 *   2104 - Insurance Payable (تأمينات مستحقة)
 *   2105 - Deferred Revenue (إيرادات مؤجلة)
 *   4101 - Membership Revenue (إيرادات عضوية)
 *   4102 - Subscription Revenue (إيرادات اشتراكات)
 *   4103 - Sales Revenue (إيرادات مبيعات)
 *   4104 - Fine Revenue (إيرادات غرامات)
 *   4105 - Service Revenue (إيرادات خدمات)
 *   5101 - Salary Expense (مصروفات رواتب)
 *   5102 - Insurance Expense (مصروفات تأمينات — حصة صاحب العمل)
 *   5106 - Cost of Goods Sold (تكلفة البضاعة المباعة)
 */
final class AccountingIntegrationService
{
    // ────────────────────────────────────────────────────────────
    // PAYMENTS MODULE
    // ────────────────────────────────────────────────────────────

    /**
     * Auto-post journal entry when a payment is completed.
     * Dr. Cash/Bank  |  Cr. Revenue (type-dependent)
     *
     * @param array $data From 'payment.completed' event
     */
    public static function onPaymentCompleted(array $data): void
    {
        $db = App::getInstance()->db();

        $paymentId = (int) ($data['payment_id'] ?? 0);
        $amount = (string) ($data['amount'] ?? '0.00');
        $method = $data['method'] ?? 'cash';
        $type = $data['type'] ?? '';
        $memberId = (int) ($data['member_id'] ?? 0);

        if (bccomp($amount, '0.00', 2) <= 0) {
            return;
        }

        // Determine debit account (where money goes)
        $debitAccountCode = match ($method) {
            'cash'          => '1101',
            'check'         => '1102',
            'visa'          => '1102',
            'bank_transfer' => '1102',
            default         => '1101',
        };

        // Determine credit account (revenue type)
        // Uses granular sub-accounts (4110+) with fallback to legacy accounts (4101+)
        $creditAccountCode = match ($type) {
            'form_fee'            => '4110', // Form Fee Revenue
            'membership_fee'      => '4111', // Membership Value Revenue
            'addition_fee'        => '4112', // Dependent Addition Revenue
            'separation_fee'      => '4120', // Separation Fee Revenue
            'divorce_fee'         => '4121', // Divorce Fee Revenue
            'death_fee'           => '4122', // Death Transfer Fee Revenue
            'waiver_fee'          => '4123', // Waiver Fee Revenue
            'sports_conversion'   => '4124', // Sports Conversion Revenue
            'seasonal_fee'        => '4130', // Seasonal Membership Revenue
            'carnet_replacement'  => '4131', // Carnet Replacement Revenue
            'annual_subscription' => '4140', // Annual Subscription Revenue
            'development_fee'     => '4141', // Development Fee Revenue
            'activity_subscription' => '4150', // Activity Subscription Revenue
            'down_payment'        => '1103', // Accounts Receivable (reducing AR)
            'installment'         => '1103', // Accounts Receivable (reducing AR)
            'fine'                => '4104', // Fine Revenue
            'inventory_sale'      => '4103', // Sales Revenue
            default               => '4105', // Service Revenue (catch-all)
        };

        // Fallback: if the granular account doesn't exist yet, use legacy grouping
        $creditAccount = self::getAccountByCode($creditAccountCode);
        if (!$creditAccount) {
            $fallbackCode = match ($type) {
                'form_fee', 'membership_fee', 'addition_fee', 'separation_fee',
                'divorce_fee', 'death_fee', 'waiver_fee', 'sports_conversion',
                'carnet_replacement', 'seasonal_fee'
                    => '4101', // Membership Revenue (legacy)
                'annual_subscription', 'development_fee'
                    => '4102', // Subscription Revenue (legacy)
                'activity_subscription'
                    => '4105', // Service Revenue (legacy)
                default => null,
            };
            if ($fallbackCode) {
                $creditAccountCode = $fallbackCode;
                $creditAccount = self::getAccountByCode($creditAccountCode);
            }
        }

        $debitAccount = self::getAccountByCode($debitAccountCode);

        if (!$debitAccount || !$creditAccount) {
            Logger::error("Accounting auto-post failed: account not found", [
                'debit_code' => $debitAccountCode,
                'credit_code' => $creditAccountCode,
                'payment_id' => $paymentId,
            ]);
            return;
        }

        $payment = $db->selectOne("SELECT * FROM payments WHERE id = ?", [$paymentId]);
        $receiptNumber = '';
        if ($payment && $payment['receipt_id']) {
            $receipt = $db->selectOne("SELECT receipt_number FROM receipts WHERE id = ?", [(int) $payment['receipt_id']]);
            $receiptNumber = $receipt['receipt_number'] ?? '';
        }

        $typeLabel = self::getPaymentTypeLabel($type);
        $description = 'تحصيل ' . $typeLabel;
        if ($receiptNumber) {
            $description .= ' — إيصال ' . $receiptNumber;
        }

        // For installment/down_payment, special handling
        $lines = [
            [
                'account_id'     => (int) $debitAccount['id'],
                'debit'          => $amount,
                'credit'         => '0.00',
                'description_ar' => $description,
                'member_id'      => $memberId > 0 ? $memberId : null,
            ],
            [
                'account_id'     => (int) $creditAccount['id'],
                'debit'          => '0.00',
                'credit'         => $amount,
                'description_ar' => $description,
                'member_id'      => $memberId > 0 ? $memberId : null,
            ],
        ];

        $result = JournalService::createEntry([
            'entry_date'        => $payment['payment_date'] ?? date('Y-m-d'),
            'description_ar'    => $description,
            'description_en'    => 'Payment collection — ' . $type,
            'reference_type'    => 'payment',
            'reference_id'      => $paymentId,
            'reference_number'  => $receiptNumber,
            'source_module'     => 'payments',
            'is_auto_generated' => 1,
        ], $lines, true);

        if (!$result['success']) {
            Logger::error("Accounting auto-post failed for payment", [
                'payment_id' => $paymentId,
                'error'      => $result['error'] ?? 'unknown',
            ]);
        }
    }

    /**
     * Auto-reverse journal entry when a payment is voided.
     */
    public static function onPaymentVoided(array $data): void
    {
        $paymentId = (int) ($data['payment_id'] ?? 0);
        $reason = $data['reason'] ?? 'إلغاء دفعة';

        $entry = \App\Modules\Accounting\Models\JournalEntry::findByReference('payment', $paymentId);
        if ($entry && $entry->isPosted()) {
            JournalService::reverseEntry((int) $entry->id, $reason);
        }
    }

    // ────────────────────────────────────────────────────────────
    // SALES MODULE
    // ────────────────────────────────────────────────────────────

    /**
     * Auto-post sales entry: Dr. Cash/Bank, Cr. Sales Revenue + COGS entry.
     */
    public static function onSaleCompleted(array $data): void
    {
        $db = App::getInstance()->db();

        $saleId = (int) ($data['sale_id'] ?? 0);
        $totalAmount = (string) ($data['total_amount'] ?? '0.00');
        $invoiceNumber = $data['invoice_number'] ?? '';

        if (bccomp($totalAmount, '0.00', 2) <= 0) {
            return;
        }

        $sale = $db->selectOne("SELECT * FROM sales WHERE id = ?", [$saleId]);
        if (!$sale) {
            return;
        }

        // Calculate total cost from sale items
        $costRow = $db->selectOne(
            "SELECT COALESCE(SUM(total_cost), 0) as total_cost FROM sale_items WHERE sale_id = ?",
            [$saleId]
        );
        $totalCost = (string) ($costRow['total_cost'] ?? '0.00');

        $salesRevenue = self::getAccountByCode('4103');
        $cashAccount = self::getAccountByCode('1101');
        $cogsAccount = self::getAccountByCode('5106');
        $inventoryAccount = self::getAccountByCode('1104');

        if (!$salesRevenue || !$cashAccount) {
            return;
        }

        $description = 'مبيعات فاتورة رقم ' . $invoiceNumber;

        // Revenue entry is handled by payment.completed, but COGS needs its own entry
        if ($cogsAccount && $inventoryAccount && bccomp($totalCost, '0.00', 2) > 0) {
            $cogsResult = JournalService::createEntry([
                'entry_date'        => $sale['sale_date'] ?? date('Y-m-d'),
                'description_ar'    => 'تكلفة بضاعة مباعة — فاتورة ' . $invoiceNumber,
                'description_en'    => 'COGS — Invoice ' . $invoiceNumber,
                'reference_type'    => 'sale_cogs',
                'reference_id'      => $saleId,
                'reference_number'  => $invoiceNumber,
                'source_module'     => 'sales',
                'is_auto_generated' => 1,
            ], [
                [
                    'account_id'     => (int) $cogsAccount['id'],
                    'debit'          => $totalCost,
                    'credit'         => '0.00',
                    'description_ar' => 'تكلفة بضاعة مباعة',
                ],
                [
                    'account_id'     => (int) $inventoryAccount['id'],
                    'debit'          => '0.00',
                    'credit'         => $totalCost,
                    'description_ar' => 'خصم من المخزون',
                ],
            ], true);

            if (!$cogsResult['success']) {
                Logger::error("COGS auto-post failed", ['sale_id' => $saleId, 'error' => $cogsResult['error'] ?? '']);
            }
        }
    }

    /**
     * Reverse sale journal entries when a sale is voided.
     */
    public static function onSaleVoided(array $data): void
    {
        $saleId = (int) ($data['sale_id'] ?? 0);
        $reason = $data['reason'] ?? 'إلغاء عملية بيع';

        // Reverse COGS entry
        $cogsEntry = \App\Modules\Accounting\Models\JournalEntry::findByReference('sale_cogs', $saleId);
        if ($cogsEntry && $cogsEntry->isPosted()) {
            JournalService::reverseEntry((int) $cogsEntry->id, $reason);
        }
    }

    // ────────────────────────────────────────────────────────────
    // HR PAYROLL MODULE
    // ────────────────────────────────────────────────────────────

    /**
     * Auto-post payroll journal when payroll run is paid.
     * Dr. Salary Expense          (gross salaries)
     * Dr. Insurance Expense       (employer's insurance share)
     * Cr. Bank                    (net salaries paid)
     * Cr. Insurance Payable       (employee + employer insurance)
     * Cr. Tax Payable             (income tax withheld)
     */
    public static function onPayrollPaid(array $data): void
    {
        $db = App::getInstance()->db();

        $payrollRunId = (int) ($data['payroll_run_id'] ?? 0);
        if ($payrollRunId <= 0) {
            return;
        }

        $run = $db->selectOne("SELECT * FROM hr_payroll_runs WHERE id = ?", [$payrollRunId]);
        if (!$run) {
            return;
        }

        // Get payroll component totals
        $components = $db->select(
            "SELECT component_type, SUM(amount) as total
             FROM hr_payroll_components_log
             WHERE payroll_run_id = ?
             GROUP BY component_type",
            [$payrollRunId]
        );

        $componentMap = [];
        foreach ($components as $c) {
            $componentMap[$c['component_type']] = (string) $c['total'];
        }

        $grossSalary = (string) ($run['total_gross'] ?? '0.00');
        $netSalary = (string) ($run['total_net'] ?? '0.00');
        $totalTax = $componentMap['income_tax'] ?? '0.00';
        $empInsurance = $componentMap['social_insurance_employee'] ?? '0.00';
        $erInsurance = $componentMap['social_insurance_employer'] ?? '0.00';
        $totalInsurance = bcadd($empInsurance, $erInsurance, 2);

        $salaryExpense = self::getAccountByCode('5101');
        $insuranceExpense = self::getAccountByCode('5102');
        $bankAccount = self::getAccountByCode('1102');
        $insurancePayable = self::getAccountByCode('2104');
        $taxPayable = self::getAccountByCode('2103');

        if (!$salaryExpense || !$bankAccount) {
            Logger::error("Payroll auto-post failed: core accounts not found");
            return;
        }

        $period = $db->selectOne("SELECT * FROM hr_payroll_periods WHERE id = ?", [(int) $run['period_id']]);
        $periodName = $period ? $period['period_name'] ?? $period['month'] . '/' . $period['year'] : '';

        $lines = [];

        // Dr. Salary Expense (gross)
        $lines[] = [
            'account_id'     => (int) $salaryExpense['id'],
            'debit'          => $grossSalary,
            'credit'         => '0.00',
            'description_ar' => 'مصروفات رواتب — ' . $periodName,
        ];

        // Dr. Insurance Expense (employer share)
        if ($insuranceExpense && bccomp($erInsurance, '0.00', 2) > 0) {
            $lines[] = [
                'account_id'     => (int) $insuranceExpense['id'],
                'debit'          => $erInsurance,
                'credit'         => '0.00',
                'description_ar' => 'حصة صاحب العمل في التأمينات — ' . $periodName,
            ];
        }

        // Cr. Bank (net salary)
        $lines[] = [
            'account_id'     => (int) $bankAccount['id'],
            'debit'          => '0.00',
            'credit'         => $netSalary,
            'description_ar' => 'صرف رواتب — ' . $periodName,
        ];

        // Cr. Insurance Payable
        if ($insurancePayable && bccomp($totalInsurance, '0.00', 2) > 0) {
            $lines[] = [
                'account_id'     => (int) $insurancePayable['id'],
                'debit'          => '0.00',
                'credit'         => $totalInsurance,
                'description_ar' => 'تأمينات مستحقة — ' . $periodName,
            ];
        }

        // Cr. Tax Payable
        if ($taxPayable && bccomp($totalTax, '0.00', 2) > 0) {
            $lines[] = [
                'account_id'     => (int) $taxPayable['id'],
                'debit'          => '0.00',
                'credit'         => $totalTax,
                'description_ar' => 'ضرائب مستحقة — ' . $periodName,
            ];
        }

        // Verify double-entry balance before posting
        $totalDebit = '0.00';
        $totalCredit = '0.00';
        foreach ($lines as $l) {
            $totalDebit = bcadd($totalDebit, (string) $l['debit'], 2);
            $totalCredit = bcadd($totalCredit, (string) $l['credit'], 2);
        }

        // If imbalanced due to rounding or missing components, adjust
        $diff = bcsub($totalDebit, $totalCredit, 2);
        if (bccomp($diff, '0.00', 2) !== 0) {
            // Adjust bank line to balance
            foreach ($lines as &$l) {
                if ((int) $l['account_id'] === (int) $bankAccount['id']) {
                    $l['credit'] = bcadd((string) $l['credit'], $diff, 2);
                    break;
                }
            }
            unset($l);
        }

        $result = JournalService::createEntry([
            'entry_date'        => $run['payment_date'] ?? date('Y-m-d'),
            'description_ar'    => 'قيد رواتب — ' . $periodName,
            'description_en'    => 'Payroll entry — ' . $periodName,
            'reference_type'    => 'payroll',
            'reference_id'      => $payrollRunId,
            'source_module'     => 'hr',
            'is_auto_generated' => 1,
        ], $lines, true);

        if (!$result['success']) {
            Logger::error("Payroll auto-post failed", [
                'payroll_run_id' => $payrollRunId,
                'error'          => $result['error'] ?? '',
            ]);
        }
    }

    // ────────────────────────────────────────────────────────────
    // SUBSCRIPTIONS MODULE
    // ────────────────────────────────────────────────────────────

    /**
     * Auto-post subscription revenue when subscription is paid.
     * Payment.completed handles the cash/bank side.
     * This handles deferred revenue for annual subscriptions.
     */
    public static function onSubscriptionPaid(array $data): void
    {
        // Revenue already posted through payment.completed event
        // This is for additional tracking / deferred revenue if needed
        $subscriptionId = (int) ($data['subscription_id'] ?? 0);
        $memberId = (int) ($data['member_id'] ?? 0);
        $amount = (string) ($data['amount'] ?? '0.00');

        if ($subscriptionId <= 0 || bccomp($amount, '0.00', 2) <= 0) {
            return;
        }

        // Update accounts receivable if exists
        $ar = \App\Modules\Accounting\Models\AccountReceivable::findByDocument('subscription', $subscriptionId);
        if ($ar) {
            $newPaid = bcadd((string) $ar->paid_amount, $amount, 2);
            $newBalance = bcsub((string) $ar->total_amount, $newPaid, 2);
            $status = bccomp($newBalance, '0.00', 2) <= 0 ? 'paid' : 'partial';

            $ar->update([
                'paid_amount' => $newPaid,
                'balance'     => max('0.00', $newBalance),
                'status'      => $status,
            ]);
        }
    }

    // ────────────────────────────────────────────────────────────
    // FINES MODULE
    // ────────────────────────────────────────────────────────────

    /**
     * Auto-post fine revenue when a fine is collected.
     */
    public static function onFineCollected(array $data): void
    {
        $fineId = (int) ($data['fine_id'] ?? 0);
        $memberId = (int) ($data['member_id'] ?? 0);
        $amount = (string) ($data['amount'] ?? '0.00');

        if ($fineId <= 0 || bccomp($amount, '0.00', 2) <= 0) {
            return;
        }

        // Update accounts receivable for the fine
        $ar = \App\Modules\Accounting\Models\AccountReceivable::findByDocument('fine', $fineId);
        if ($ar) {
            $newPaid = bcadd((string) $ar->paid_amount, $amount, 2);
            $newBalance = bcsub((string) $ar->total_amount, $newPaid, 2);
            $status = bccomp($newBalance, '0.00', 2) <= 0 ? 'paid' : 'partial';

            $ar->update([
                'paid_amount' => $newPaid,
                'balance'     => max('0.00', $newBalance),
                'status'      => $status,
            ]);
        }
    }

    /**
     * Create accounts receivable when a fine is imposed.
     */
    public static function onFineImposed(array $data): void
    {
        $db = App::getInstance()->db();

        $fineId = (int) ($data['fine_id'] ?? 0);
        $memberId = (int) ($data['member_id'] ?? 0);
        $amount = (string) ($data['amount'] ?? '0.00');

        if ($fineId <= 0 || $memberId <= 0 || bccomp($amount, '0.00', 2) <= 0) {
            return;
        }

        $fine = $db->selectOne("SELECT * FROM fines WHERE id = ?", [$fineId]);
        if (!$fine) {
            return;
        }

        // Create AR entry
        $arAccount = self::getAccountByCode('1103');
        $fineRevenue = self::getAccountByCode('4104');

        if ($arAccount && $fineRevenue) {
            // Create receivable journal entry
            $result = JournalService::createEntry([
                'entry_date'        => $fine['imposed_date'] ?? date('Y-m-d'),
                'description_ar'    => 'استحقاق غرامة — عضو رقم ' . $memberId,
                'reference_type'    => 'fine',
                'reference_id'      => $fineId,
                'source_module'     => 'fines',
                'is_auto_generated' => 1,
            ], [
                [
                    'account_id'     => (int) $arAccount['id'],
                    'debit'          => $amount,
                    'credit'         => '0.00',
                    'description_ar' => 'مدينون — غرامة',
                    'member_id'      => $memberId,
                ],
                [
                    'account_id'     => (int) $fineRevenue['id'],
                    'debit'          => '0.00',
                    'credit'         => $amount,
                    'description_ar' => 'إيرادات غرامات',
                ],
            ], true);

            // Create accounts receivable record
            if ($result['success']) {
                $db->insert('accounts_receivable', [
                    'member_id'        => $memberId,
                    'document_type'    => 'fine',
                    'document_id'      => $fineId,
                    'document_date'    => $fine['imposed_date'] ?? date('Y-m-d'),
                    'due_date'         => date('Y-m-d', strtotime('+30 days')),
                    'description_ar'   => 'غرامة مخالفة',
                    'total_amount'     => $amount,
                    'paid_amount'      => '0.00',
                    'balance'          => $amount,
                    'status'           => 'pending',
                    'journal_entry_id' => $result['journal_entry_id'],
                    'created_at'       => date('Y-m-d H:i:s'),
                    'updated_at'       => date('Y-m-d H:i:s'),
                ]);
            }
        }
    }

    // ────────────────────────────────────────────────────────────
    // INSTALLMENTS MODULE
    // ────────────────────────────────────────────────────────────

    /**
     * Create accounts receivable when an installment plan is created.
     */
    public static function onInstallmentPlanCreated(array $data): void
    {
        $db = App::getInstance()->db();

        $planId = (int) ($data['plan_id'] ?? 0);
        $memberId = (int) ($data['member_id'] ?? 0);
        $totalAmount = (string) ($data['total_amount'] ?? '0.00');

        if ($planId <= 0 || bccomp($totalAmount, '0.00', 2) <= 0) {
            return;
        }

        $plan = $db->selectOne("SELECT * FROM installment_plans WHERE id = ?", [$planId]);
        if (!$plan) {
            return;
        }

        $arAccount = self::getAccountByCode('1103');
        $membershipRevenue = self::getAccountByCode('4101');

        if ($arAccount && $membershipRevenue) {
            // Journal: Dr. AR, Cr. Membership Revenue
            $result = JournalService::createEntry([
                'entry_date'        => $plan['created_at'] ? substr($plan['created_at'], 0, 10) : date('Y-m-d'),
                'description_ar'    => 'استحقاق أقساط عضوية — عضو رقم ' . $memberId,
                'reference_type'    => 'installment_plan',
                'reference_id'      => $planId,
                'source_module'     => 'installments',
                'is_auto_generated' => 1,
            ], [
                [
                    'account_id'     => (int) $arAccount['id'],
                    'debit'          => $totalAmount,
                    'credit'         => '0.00',
                    'description_ar' => 'مدينون — أقساط عضوية',
                    'member_id'      => $memberId,
                ],
                [
                    'account_id'     => (int) $membershipRevenue['id'],
                    'debit'          => '0.00',
                    'credit'         => $totalAmount,
                    'description_ar' => 'إيرادات عضوية — أقساط',
                ],
            ], true);

            if ($result['success']) {
                $db->insert('accounts_receivable', [
                    'member_id'        => $memberId,
                    'document_type'    => 'installment',
                    'document_id'      => $planId,
                    'document_date'    => date('Y-m-d'),
                    'due_date'         => $plan['end_date'] ?? date('Y-m-d', strtotime('+12 months')),
                    'description_ar'   => 'أقساط عضوية',
                    'total_amount'     => $totalAmount,
                    'paid_amount'      => '0.00',
                    'balance'          => $totalAmount,
                    'status'           => 'pending',
                    'journal_entry_id' => $result['journal_entry_id'],
                    'created_at'       => date('Y-m-d H:i:s'),
                    'updated_at'       => date('Y-m-d H:i:s'),
                ]);
            }
        }
    }

    // ────────────────────────────────────────────────────────────
    // INSTALLMENT PAYMENTS
    // ────────────────────────────────────────────────────────────

    /**
     * Update AR when an installment is paid.
     * The cash/revenue side is already handled by payment.completed.
     * This reduces the AR balance for the installment plan.
     */
    public static function onInstallmentPaid(array $data): void
    {
        $db = App::getInstance()->db();

        $planId = (int) ($data['plan_id'] ?? 0);
        $scheduleId = (int) ($data['schedule_id'] ?? 0);
        $memberId = (int) ($data['member_id'] ?? 0);

        if ($planId <= 0 || $scheduleId <= 0) {
            return;
        }

        $scheduleItem = $db->selectOne("SELECT amount FROM installment_schedule WHERE id = ?", [$scheduleId]);
        $amount = (string) ($scheduleItem['amount'] ?? '0.00');

        if (bccomp($amount, '0.00', 2) <= 0) {
            return;
        }

        $ar = \App\Modules\Accounting\Models\AccountReceivable::findByDocument('installment', $planId);
        if ($ar) {
            $newPaid = bcadd((string) $ar->paid_amount, $amount, 2);
            $newBalance = bcsub((string) $ar->total_amount, $newPaid, 2);
            if (bccomp($newBalance, '0.00', 2) < 0) $newBalance = '0.00';
            $status = bccomp($newBalance, '0.00', 2) <= 0 ? 'paid' : 'partial';

            $ar->update([
                'paid_amount' => $newPaid,
                'balance'     => $newBalance,
                'status'      => $status,
            ]);
        }
    }

    // ────────────────────────────────────────────────────────────
    // REFUNDS
    // ────────────────────────────────────────────────────────────

    /**
     * Post GL entry when a sale is refunded.
     * Dr. Sales Revenue (reduce revenue)  |  Cr. Cash/Bank (money out)
     */
    public static function onSaleRefunded(array $data): void
    {
        $db = App::getInstance()->db();

        $saleId = (int) ($data['sale_id'] ?? 0);
        $refundId = (int) ($data['refund_id'] ?? 0);
        $refundNumber = $data['refund_number'] ?? '';
        $amount = (string) ($data['amount'] ?? '0.00');

        if ($saleId <= 0 || bccomp($amount, '0.00', 2) <= 0) {
            return;
        }

        $sale = $db->selectOne("SELECT * FROM sales WHERE id = ?", [$saleId]);
        if (!$sale) {
            return;
        }

        $salesRevenue = self::getAccountByCode('4103');
        $cashAccount = self::getAccountByCode('1101');

        if (!$salesRevenue || !$cashAccount) {
            Logger::error("Refund auto-post failed: accounts not found", ['sale_id' => $saleId]);
            return;
        }

        $description = 'مرتجع مبيعات — فاتورة ' . ($sale['invoice_number'] ?? '') . ' — إشعار ' . $refundNumber;

        $result = JournalService::createEntry([
            'entry_date'        => date('Y-m-d'),
            'description_ar'    => $description,
            'description_en'    => 'Sales refund — ' . $refundNumber,
            'reference_type'    => 'sale_refund',
            'reference_id'      => $refundId,
            'reference_number'  => $refundNumber,
            'source_module'     => 'sales',
            'is_auto_generated' => 1,
        ], [
            [
                'account_id'     => (int) $salesRevenue['id'],
                'debit'          => $amount,
                'credit'         => '0.00',
                'description_ar' => 'مرتجع مبيعات — تخفيض إيراد',
            ],
            [
                'account_id'     => (int) $cashAccount['id'],
                'debit'          => '0.00',
                'credit'         => $amount,
                'description_ar' => 'مرتجع مبيعات — صرف نقدي',
            ],
        ], true);

        if (!$result['success']) {
            Logger::error("Refund auto-post failed", ['sale_id' => $saleId, 'error' => $result['error'] ?? '']);
        }
    }

    // ────────────────────────────────────────────────────────────
    // RENTAL DEPOSITS
    // ────────────────────────────────────────────────────────────

    /**
     * Record deposit liability when collected.
     * The cash side is handled by payment.completed.
     * This creates a Dr. Cash, Cr. Deposit Liability (2105 Deferred Revenue).
     * Since payment.completed already handles Dr. Cash → Cr. Revenue,
     * we only need to reclassify from revenue to deposit liability.
     */
    public static function onRentalDepositCollected(array $data): void
    {
        $db = App::getInstance()->db();

        $contractId = (int) ($data['contract_id'] ?? 0);
        $paymentId = (int) ($data['payment_id'] ?? 0);

        if ($contractId <= 0 || $paymentId <= 0) {
            return;
        }

        $payment = $db->selectOne("SELECT * FROM payments WHERE id = ?", [$paymentId]);
        if (!$payment) {
            return;
        }

        $amount = (string) ($payment['amount'] ?? '0.00');
        if (bccomp($amount, '0.00', 2) <= 0) {
            return;
        }

        // Reclassify: Dr. Service Revenue (4105), Cr. Deferred Revenue / Deposits (2105)
        $serviceRevenue = self::getAccountByCode('4105');
        $deferredRevenue = self::getAccountByCode('2105');

        if (!$serviceRevenue || !$deferredRevenue) {
            return;
        }

        $contract = $db->selectOne("SELECT * FROM rental_contracts WHERE id = ?", [$contractId]);
        $contractNum = $contract['contract_number'] ?? $contractId;

        $result = JournalService::createEntry([
            'entry_date'        => date('Y-m-d'),
            'description_ar'    => 'إعادة تصنيف تأمين إيجار — عقد ' . $contractNum,
            'description_en'    => 'Rental deposit reclassification — contract ' . $contractNum,
            'reference_type'    => 'rental_deposit',
            'reference_id'      => $contractId,
            'source_module'     => 'rentals',
            'is_auto_generated' => 1,
        ], [
            [
                'account_id'     => (int) $serviceRevenue['id'],
                'debit'          => $amount,
                'credit'         => '0.00',
                'description_ar' => 'إعادة تصنيف من إيراد خدمات إلى تأمينات',
            ],
            [
                'account_id'     => (int) $deferredRevenue['id'],
                'debit'          => '0.00',
                'credit'         => $amount,
                'description_ar' => 'تأمين إيجار مؤجل — عقد ' . $contractNum,
            ],
        ], true);

        if (!$result['success']) {
            Logger::error("Rental deposit reclassification failed", ['contract_id' => $contractId, 'error' => $result['error'] ?? '']);
        }
    }

    /**
     * Reverse deposit liability when refunded.
     * Dr. Deferred Revenue (2105), Cr. Cash (1101)
     */
    public static function onRentalDepositRefunded(array $data): void
    {
        $db = App::getInstance()->db();

        $contractId = (int) ($data['contract_id'] ?? 0);
        $paymentId = (int) ($data['payment_id'] ?? 0);

        if ($contractId <= 0) {
            return;
        }

        // Find the original deposit reclassification entry and reverse it
        $entry = \App\Modules\Accounting\Models\JournalEntry::findByReference('rental_deposit', $contractId);
        if ($entry && $entry->isPosted()) {
            JournalService::reverseEntry((int) $entry->id, 'رد تأمين إيجار — عقد ' . $contractId);
        }
    }

    // ────────────────────────────────────────────────────────────
    // PROCUREMENT MODULE
    // ────────────────────────────────────────────────────────────

    /**
     * Auto-post AP accrual when a vendor invoice is approved.
     * Dr. 1104 Inventory (subtotal)
     * Dr. 2103 Tax Receivable / Input VAT (tax_amount)
     * Cr. 2101 Accounts Payable (total_amount)
     *
     * Also creates accounts_payable record.
     */
    public static function onVendorInvoiceApproved(array $data): void
    {
        $db = App::getInstance()->db();

        $invoiceId     = (int) ($data['invoice_id'] ?? 0);
        $supplierId    = (int) ($data['supplier_id'] ?? 0);
        $subtotal      = (string) ($data['subtotal'] ?? '0.00');
        $taxAmount     = (string) ($data['tax_amount'] ?? '0.00');
        $totalAmount   = (string) ($data['total_amount'] ?? '0.00');
        $invoiceNumber = $data['internal_number'] ?? '';

        if ($invoiceId <= 0 || bccomp($totalAmount, '0.00', 2) <= 0) {
            return;
        }

        $invoice = $db->selectOne("SELECT * FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
        if (!$invoice) {
            return;
        }

        $inventoryAccount = self::getAccountByCode('1104');
        $taxAccount       = self::getAccountByCode('2103');
        $apAccount        = self::getAccountByCode('2101');

        if (!$inventoryAccount || !$apAccount) {
            Logger::error("Vendor invoice auto-post failed: core accounts not found", [
                'invoice_id' => $invoiceId,
            ]);
            return;
        }

        $description = 'فاتورة مورد — ' . $invoiceNumber;

        $lines = [];

        // Dr. Inventory (subtotal)
        if (bccomp($subtotal, '0.00', 2) > 0) {
            $lines[] = [
                'account_id'     => (int) $inventoryAccount['id'],
                'debit'          => $subtotal,
                'credit'         => '0.00',
                'description_ar' => 'مخزون — فاتورة مورد ' . $invoiceNumber,
            ];
        }

        // Dr. Tax Receivable (Input VAT)
        if ($taxAccount && bccomp($taxAmount, '0.00', 2) > 0) {
            $lines[] = [
                'account_id'     => (int) $taxAccount['id'],
                'debit'          => $taxAmount,
                'credit'         => '0.00',
                'description_ar' => 'ضريبة مدخلات — فاتورة مورد ' . $invoiceNumber,
            ];
        }

        // Cr. Accounts Payable (total)
        $lines[] = [
            'account_id'     => (int) $apAccount['id'],
            'debit'          => '0.00',
            'credit'         => $totalAmount,
            'description_ar' => 'دائنون — فاتورة مورد ' . $invoiceNumber,
        ];

        $result = JournalService::createEntry([
            'entry_date'        => $invoice['invoice_date'] ?? date('Y-m-d'),
            'description_ar'    => $description,
            'description_en'    => 'Vendor invoice — ' . $invoiceNumber,
            'reference_type'    => 'vendor_invoice',
            'reference_id'      => $invoiceId,
            'reference_number'  => $invoiceNumber,
            'source_module'     => 'procurement',
            'is_auto_generated' => 1,
        ], $lines, true);

        if (!$result['success']) {
            Logger::error("Vendor invoice auto-post failed", [
                'invoice_id' => $invoiceId,
                'error'      => $result['error'] ?? '',
            ]);
            return;
        }

        // Link journal entry back to vendor invoice
        $db->update('vendor_invoices', [
            'journal_entry_id' => $result['journal_entry_id'],
        ], '`id` = ?', [$invoiceId]);

        // Create accounts_payable record
        $db->insert('accounts_payable', [
            'supplier_id'       => $supplierId,
            'invoice_number'    => $invoiceNumber,
            'invoice_date'      => $invoice['invoice_date'] ?? date('Y-m-d'),
            'due_date'          => $invoice['due_date'] ?? date('Y-m-d', strtotime('+30 days')),
            'description_ar'    => $description,
            'description_en'    => 'Vendor invoice — ' . $invoiceNumber,
            'total_amount'      => $totalAmount,
            'paid_amount'       => '0.00',
            'balance'           => $totalAmount,
            'currency'          => $invoice['currency'] ?? 'EGP',
            'status'            => 'pending',
            'journal_entry_id'  => $result['journal_entry_id'],
            'purchase_order_id' => !empty($invoice['purchase_order_id']) ? (int) $invoice['purchase_order_id'] : null,
            'branch_id'         => $invoice['branch_id'] ?? null,
            'created_at'        => date('Y-m-d H:i:s'),
            'updated_at'        => date('Y-m-d H:i:s'),
        ]);

        $apId = $db->lastInsertId();

        // Link AP record back to vendor invoice
        $db->update('vendor_invoices', [
            'ap_record_id' => $apId,
        ], '`id` = ?', [$invoiceId]);

        Logger::info("Vendor invoice #{$invoiceId} — journal posted, AP record #{$apId} created");
    }

    /**
     * Auto-post AP payment when a vendor payment is completed.
     * Dr. 2101 Accounts Payable (amount)
     * Cr. 1101/1102 Cash or Bank (amount)
     *
     * Also updates AP paid_amount/balance/status.
     */
    public static function onVendorPaymentCompleted(array $data): void
    {
        $db = App::getInstance()->db();

        $paymentId     = (int) ($data['payment_id'] ?? 0);
        $invoiceId     = (int) ($data['invoice_id'] ?? 0);
        $supplierId    = (int) ($data['supplier_id'] ?? 0);
        $amount        = (string) ($data['amount'] ?? '0.00');
        $paymentMethod = $data['payment_method'] ?? 'cash';
        $paymentNumber = $data['payment_number'] ?? '';

        if ($paymentId <= 0 || bccomp($amount, '0.00', 2) <= 0) {
            return;
        }

        $apAccount = self::getAccountByCode('2101');
        $cashBankCode = in_array($paymentMethod, ['bank_transfer', 'check', 'wire'], true) ? '1102' : '1101';
        $cashBankAccount = self::getAccountByCode($cashBankCode);

        if (!$apAccount || !$cashBankAccount) {
            Logger::error("Vendor payment auto-post failed: accounts not found", ['payment_id' => $paymentId]);
            return;
        }

        $payment = $db->selectOne("SELECT * FROM `vendor_payments` WHERE `id` = ?", [$paymentId]);
        if (!$payment) {
            return;
        }

        $description = 'دفعة مورد — ' . $paymentNumber;

        $result = JournalService::createEntry([
            'entry_date'        => $payment['payment_date'] ?? date('Y-m-d'),
            'description_ar'    => $description,
            'description_en'    => 'Vendor payment — ' . $paymentNumber,
            'reference_type'    => 'vendor_payment',
            'reference_id'      => $paymentId,
            'reference_number'  => $paymentNumber,
            'source_module'     => 'procurement',
            'is_auto_generated' => 1,
        ], [
            [
                'account_id'     => (int) $apAccount['id'],
                'debit'          => $amount,
                'credit'         => '0.00',
                'description_ar' => 'تسديد دائنون — ' . $paymentNumber,
            ],
            [
                'account_id'     => (int) $cashBankAccount['id'],
                'debit'          => '0.00',
                'credit'         => $amount,
                'description_ar' => 'صرف نقدي/بنكي — ' . $paymentNumber,
            ],
        ], true);

        if (!$result['success']) {
            Logger::error("Vendor payment auto-post failed", [
                'payment_id' => $paymentId,
                'error'      => $result['error'] ?? '',
            ]);
            return;
        }

        // Link journal entry back to vendor payment
        $db->update('vendor_payments', [
            'journal_entry_id' => $result['journal_entry_id'],
        ], '`id` = ?', [$paymentId]);

        // Update AP record for the linked invoice
        if ($invoiceId > 0) {
            $invoice = $db->selectOne("SELECT `ap_record_id` FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
            if ($invoice && !empty($invoice['ap_record_id'])) {
                $apRecord = $db->selectOne("SELECT * FROM `accounts_payable` WHERE `id` = ?", [(int) $invoice['ap_record_id']]);
                if ($apRecord) {
                    $newPaid = bcadd((string) $apRecord['paid_amount'], $amount, 2);
                    $newBalance = bcsub((string) $apRecord['total_amount'], $newPaid, 2);
                    if (bccomp($newBalance, '0.00', 2) < 0) {
                        $newBalance = '0.00';
                    }
                    $status = bccomp($newBalance, '0.00', 2) <= 0 ? 'paid' : 'partial';

                    $db->update('accounts_payable', [
                        'paid_amount'      => $newPaid,
                        'balance'          => $newBalance,
                        'status'           => $status,
                        'payment_entry_id' => $result['journal_entry_id'],
                        'updated_at'       => date('Y-m-d H:i:s'),
                    ], '`id` = ?', [(int) $invoice['ap_record_id']]);
                }
            }
        }

        Logger::info("Vendor payment #{$paymentId} — journal posted, AP updated");
    }

    /**
     * Reverse journal entry when a vendor payment is voided.
     * Also reverts AP balance.
     */
    public static function onVendorPaymentVoided(array $data): void
    {
        $db = App::getInstance()->db();

        $paymentId  = (int) ($data['payment_id'] ?? 0);
        $invoiceId  = (int) ($data['invoice_id'] ?? 0);
        $amount     = (string) ($data['amount'] ?? '0.00');
        $reason     = $data['reason'] ?? 'إلغاء دفعة مورد';

        // Reverse the journal entry
        $entry = \App\Modules\Accounting\Models\JournalEntry::findByReference('vendor_payment', $paymentId);
        if ($entry && $entry->isPosted()) {
            JournalService::reverseEntry((int) $entry->id, $reason);
        }

        // Revert AP balance
        if ($invoiceId > 0 && bccomp($amount, '0.00', 2) > 0) {
            $invoice = $db->selectOne("SELECT `ap_record_id` FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
            if ($invoice && !empty($invoice['ap_record_id'])) {
                $apRecord = $db->selectOne("SELECT * FROM `accounts_payable` WHERE `id` = ?", [(int) $invoice['ap_record_id']]);
                if ($apRecord) {
                    $newPaid = bcsub((string) $apRecord['paid_amount'], $amount, 2);
                    if (bccomp($newPaid, '0.00', 2) < 0) {
                        $newPaid = '0.00';
                    }
                    $newBalance = bcsub((string) $apRecord['total_amount'], $newPaid, 2);
                    $status = bccomp($newPaid, '0.00', 2) <= 0 ? 'pending' : 'partial';

                    $db->update('accounts_payable', [
                        'paid_amount' => $newPaid,
                        'balance'     => $newBalance,
                        'status'      => $status,
                        'updated_at'  => date('Y-m-d H:i:s'),
                    ], '`id` = ?', [(int) $invoice['ap_record_id']]);
                }
            }
        }

        Logger::info("Vendor payment #{$paymentId} voided — journal reversed, AP reverted");
    }

    /**
     * Auto-post RTV journal when return-to-vendor is completed.
     * Dr. 2101 Accounts Payable (total_amount) — reduce what we owe
     * Cr. 1104 Inventory (total_amount) — stock left the warehouse
     */
    public static function onReturnToVendorCompleted(array $data): void
    {
        $db = App::getInstance()->db();

        $rtvId       = (int) ($data['rtv_id'] ?? 0);
        $supplierId  = (int) ($data['supplier_id'] ?? 0);
        $totalAmount = (string) ($data['total_amount'] ?? '0.00');
        $warehouseId = (int) ($data['warehouse_id'] ?? 0);

        if ($rtvId <= 0 || bccomp($totalAmount, '0.00', 2) <= 0) {
            return;
        }

        $rtv = $db->selectOne("SELECT * FROM `return_to_vendor` WHERE `id` = ?", [$rtvId]);
        if (!$rtv) {
            return;
        }

        $rtvNumber = $rtv['rtv_number'] ?? '';

        $apAccount        = self::getAccountByCode('2101');
        $inventoryAccount = self::getAccountByCode('1104');

        if (!$apAccount || !$inventoryAccount) {
            Logger::error("RTV auto-post failed: accounts not found", ['rtv_id' => $rtvId]);
            return;
        }

        $description = 'مرتجع مورد — ' . $rtvNumber;

        $result = JournalService::createEntry([
            'entry_date'        => $rtv['return_date'] ?? date('Y-m-d'),
            'description_ar'    => $description,
            'description_en'    => 'Return to vendor — ' . $rtvNumber,
            'reference_type'    => 'return_to_vendor',
            'reference_id'      => $rtvId,
            'reference_number'  => $rtvNumber,
            'source_module'     => 'procurement',
            'is_auto_generated' => 1,
        ], [
            [
                'account_id'     => (int) $apAccount['id'],
                'debit'          => $totalAmount,
                'credit'         => '0.00',
                'description_ar' => 'تخفيض دائنون — مرتجع مورد ' . $rtvNumber,
            ],
            [
                'account_id'     => (int) $inventoryAccount['id'],
                'debit'          => '0.00',
                'credit'         => $totalAmount,
                'description_ar' => 'خصم من المخزون — مرتجع مورد ' . $rtvNumber,
            ],
        ], true);

        if (!$result['success']) {
            Logger::error("RTV auto-post failed", ['rtv_id' => $rtvId, 'error' => $result['error'] ?? '']);
            return;
        }

        // Link journal entry back to RTV
        $db->update('return_to_vendor', [
            'journal_entry_id' => $result['journal_entry_id'],
        ], '`id` = ?', [$rtvId]);

        // Reduce AP balance if the RTV is linked to an invoice via the supplier
        // Find the most recent pending/partial AP record for this supplier
        $apRecord = $db->selectOne(
            "SELECT * FROM `accounts_payable`
             WHERE `supplier_id` = ? AND `status` IN ('pending', 'partial') AND `is_archived` = 0
             ORDER BY `created_at` DESC LIMIT 1",
            [$supplierId]
        );
        if ($apRecord) {
            $newBalance = bcsub((string) $apRecord['balance'], $totalAmount, 2);
            if (bccomp($newBalance, '0.00', 2) < 0) {
                $newBalance = '0.00';
            }
            $newPaid = bcsub((string) $apRecord['total_amount'], $newBalance, 2);
            $status = bccomp($newBalance, '0.00', 2) <= 0 ? 'paid' : 'partial';

            $db->update('accounts_payable', [
                'paid_amount' => $newPaid,
                'balance'     => $newBalance,
                'status'      => $status,
                'updated_at'  => date('Y-m-d H:i:s'),
            ], '`id` = ?', [(int) $apRecord['id']]);
        }

        Logger::info("RTV #{$rtvId} — journal posted, AP reduced");
    }

    // ────────────────────────────────────────────────────────────
    // HELPERS
    // ────────────────────────────────────────────────────────────

    private static function getAccountByCode(string $code): ?array
    {
        $db = App::getInstance()->db();
        return $db->selectOne(
            "SELECT id, account_code, name_ar, account_nature FROM chart_of_accounts WHERE account_code = ? AND is_archived = 0",
            [$code]
        );
    }

    private static function getPaymentTypeLabel(string $type): string
    {
        return match ($type) {
            'form_fee'              => 'رسوم استمارة',
            'membership_fee'        => 'قيمة العضوية',
            'addition_fee'          => 'رسوم إضافة',
            'annual_subscription'   => 'اشتراك سنوي',
            'development_fee'       => 'رسوم تنمية',
            'down_payment'          => 'مقدم تقسيط',
            'installment'           => 'قسط شهري',
            'fine'                  => 'غرامة',
            'separation_fee'        => 'رسوم فصل',
            'divorce_fee'           => 'رسوم طلاق',
            'death_fee'             => 'رسوم نقل وفاة',
            'waiver_fee'            => 'رسوم تنازل',
            'carnet_replacement'    => 'بدل فاقد كارنيه',
            'seasonal_fee'          => 'رسوم عضوية موسمية',
            'sports_conversion'     => 'رسوم تحويل رياضي',
            'inventory_sale'        => 'مبيعات مخزون',
            'activity_subscription' => 'اشتراك نشاط',
            default                 => $type,
        };
    }
}
