<?php
declare(strict_types=1);

namespace App\Modules\Accounting\Services;

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

/**
 * Syncs existing financial data into the General Ledger.
 *
 * Called from:
 *   - CLI seed (Phase_36_003)
 *   - Web UI (Accounting Dashboard sync button)
 *
 * For each financial record (payment, fine, installment, sale, payroll, refund,
 * rental deposit), checks whether a journal entry already exists. If not,
 * replays the same AccountingIntegrationService handler that would have fired
 * if the accounting module had been active at the time.
 *
 * Idempotent — safe to call multiple times.
 */
final class GLSyncService
{
    /**
     * Run the full GL sync.
     *
     * @return array{
     *   payments: int, fines: int, installments: int, sales: int,
     *   payroll: int, refunds: int, rentals: int, errors: int, messages: string[]
     * }
     */
    public static function syncAll(): array
    {
        $db = App::getInstance()->db();

        $result = [
            'payments'     => 0,
            'fines'        => 0,
            'installments' => 0,
            'sales'        => 0,
            'payroll'      => 0,
            'refunds'      => 0,
            'rentals'      => 0,
            'errors'       => 0,
            'messages'     => [],
        ];

        // Ensure fiscal years exist for all data
        self::ensureFiscalYears($db, $result);

        // 1. Payments
        self::syncPayments($db, $result);

        // 2. Fines
        self::syncFines($db, $result);

        // 3. Installment Plans
        self::syncInstallments($db, $result);

        // 4. Sales COGS
        self::syncSales($db, $result);

        // 5. Payroll
        self::syncPayroll($db, $result);

        // 6. Sale Refunds
        self::syncRefunds($db, $result);

        // 7. Rental Deposits
        self::syncRentalDeposits($db, $result);

        $total = $result['payments'] + $result['fines'] + $result['installments']
               + $result['sales'] + $result['payroll'] + $result['refunds'] + $result['rentals'];

        $result['messages'][] = "تم مزامنة {$total} قيد محاسبي";
        if ($result['errors'] > 0) {
            $result['messages'][] = "فشل {$result['errors']} عملية — راجع السجلات";
        }

        Logger::info("GL Sync completed", [
            'payments' => $result['payments'],
            'fines' => $result['fines'],
            'installments' => $result['installments'],
            'sales' => $result['sales'],
            'payroll' => $result['payroll'],
            'refunds' => $result['refunds'],
            'rentals' => $result['rentals'],
            'errors' => $result['errors'],
        ]);

        return $result;
    }

    /**
     * Ensure fiscal years exist for all years that have financial data.
     */
    private static function ensureFiscalYears($db, array &$result): void
    {
        $now = date('Y-m-d H:i:s');
        $currentYear = (int) date('Y');

        $queries = [
            "SELECT MIN(payment_date) as min_date, MAX(payment_date) as max_date FROM payments WHERE is_voided = 0",
        ];

        $optionalTables = [
            'fines'             => "SELECT MIN(created_at) as min_date, MAX(created_at) as max_date FROM fines",
            'sales'             => "SELECT MIN(sale_date) as min_date, MAX(sale_date) as max_date FROM sales WHERE status = 'completed'",
            'hr_payroll_runs'   => "SELECT MIN(paid_at) as min_date, MAX(paid_at) as max_date FROM hr_payroll_runs WHERE status = 'paid'",
            'installment_plans' => "SELECT MIN(created_at) as min_date, MAX(created_at) as max_date FROM installment_plans",
        ];

        foreach ($optionalTables as $table => $query) {
            if ($db->selectOne("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?", [$table])) {
                $queries[] = $query;
            }
        }

        $minYear = $currentYear;
        $maxYear = $currentYear;

        foreach ($queries as $query) {
            try {
                $row = $db->selectOne($query);
                if ($row && $row['min_date']) {
                    $y = (int) substr($row['min_date'], 0, 4);
                    if ($y > 2000 && $y < $minYear) $minYear = $y;
                }
                if ($row && $row['max_date']) {
                    $y = (int) substr($row['max_date'], 0, 4);
                    if ($y > 2000 && $y > $maxYear) $maxYear = $y;
                }
            } catch (\Throwable $e) {
                continue;
            }
        }

        $created = 0;
        for ($year = $minYear; $year <= $maxYear; $year++) {
            $startDate = $year . '-01-01';
            $endDate   = $year . '-12-31';

            $existing = $db->selectOne(
                "SELECT id, status FROM fiscal_years WHERE start_date <= ? AND end_date >= ? AND is_archived = 0",
                [$startDate, $endDate]
            );

            if ($existing) {
                if ($existing['status'] !== 'open') {
                    $db->update('fiscal_years', ['status' => 'open', 'updated_at' => $now], '`id` = ?', [(int) $existing['id']]);
                }
                if ($year === $currentYear) {
                    $db->update('fiscal_years', ['is_current' => 1, 'updated_at' => $now], '`id` = ?', [(int) $existing['id']]);
                }
                continue;
            }

            $db->insert('fiscal_years', [
                'name_ar' => 'السنة المالية ' . $year, 'name_en' => 'Fiscal Year ' . $year,
                'start_date' => $startDate, 'end_date' => $endDate,
                'status' => 'open', 'is_current' => ($year === $currentYear) ? 1 : 0,
                'is_archived' => 0, 'created_at' => $now, 'updated_at' => $now,
            ]);
            $created++;
        }

        if ($created > 0) {
            $result['messages'][] = "تم إنشاء {$created} سنة مالية ({$minYear}–{$maxYear})";
        }
    }

    private static function syncPayments($db, array &$result): void
    {
        $payments = $db->select(
            "SELECT p.id, p.member_id, p.payment_type, p.amount, p.payment_method,
                    p.payment_date, p.receipt_id
             FROM payments p
             WHERE p.is_voided = 0
               AND NOT EXISTS (
                   SELECT 1 FROM journal_entries je
                   WHERE je.reference_type = 'payment' AND je.reference_id = p.id
                     AND je.is_archived = 0 AND je.status != 'reversed'
               )
             ORDER BY p.payment_date ASC, p.id ASC"
        );

        foreach ($payments as $p) {
            if (bccomp((string) $p['amount'], '0.00', 2) <= 0) continue;

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

            try {
                AccountingIntegrationService::onPaymentCompleted([
                    'payment_id'     => (int) $p['id'],
                    'receipt_id'     => (int) ($p['receipt_id'] ?? 0),
                    'receipt_number' => $receiptNumber,
                    'member_id'      => (int) ($p['member_id'] ?? 0),
                    'type'           => $p['payment_type'] ?? '',
                    'amount'         => (string) $p['amount'],
                    'method'         => $p['payment_method'] ?? 'cash',
                ]);
                $result['payments']++;
            } catch (\Throwable $e) {
                $result['errors']++;
                Logger::error("GL Sync payment error", ['id' => $p['id'], 'error' => $e->getMessage()]);
            }
        }
    }

    private static function syncFines($db, array &$result): void
    {
        $tableExists = $db->selectOne("SHOW TABLES LIKE 'fines'");
        if (!$tableExists) return;

        $fines = $db->select(
            "SELECT f.id, f.member_id, f.amount, f.status
             FROM fines f
             WHERE f.penalty_type = 'fine' AND f.amount > 0
               AND NOT EXISTS (
                   SELECT 1 FROM journal_entries je
                   WHERE je.reference_type = 'fine' AND je.reference_id = f.id
                     AND je.is_archived = 0 AND je.status != 'reversed'
               )
             ORDER BY f.created_at ASC, f.id ASC"
        );

        foreach ($fines as $f) {
            try {
                AccountingIntegrationService::onFineImposed([
                    'fine_id'   => (int) $f['id'],
                    'member_id' => (int) $f['member_id'],
                    'amount'    => (string) $f['amount'],
                ]);
                $result['fines']++;

                if ($f['status'] === 'paid' && bccomp((string) $f['amount'], '0.00', 2) > 0) {
                    AccountingIntegrationService::onFineCollected([
                        'fine_id'   => (int) $f['id'],
                        'member_id' => (int) $f['member_id'],
                        'amount'    => (string) $f['amount'],
                    ]);
                }
            } catch (\Throwable $e) {
                $result['errors']++;
                Logger::error("GL Sync fine error", ['id' => $f['id'], 'error' => $e->getMessage()]);
            }
        }
    }

    private static function syncInstallments($db, array &$result): void
    {
        $tableExists = $db->selectOne("SHOW TABLES LIKE 'installment_plans'");
        if (!$tableExists) return;

        $plans = $db->select(
            "SELECT ip.id, ip.member_id, ip.total_amount, ip.created_at
             FROM installment_plans ip
             WHERE ip.total_amount > 0
               AND NOT EXISTS (
                   SELECT 1 FROM journal_entries je
                   WHERE je.reference_type = 'installment_plan' AND je.reference_id = ip.id
                     AND je.is_archived = 0 AND je.status != 'reversed'
               )
             ORDER BY ip.created_at ASC, ip.id ASC"
        );

        foreach ($plans as $plan) {
            try {
                AccountingIntegrationService::onInstallmentPlanCreated([
                    'plan_id'      => (int) $plan['id'],
                    'member_id'    => (int) $plan['member_id'],
                    'total_amount' => (string) $plan['total_amount'],
                ]);
                $result['installments']++;

                $paidSchedules = $db->select(
                    "SELECT s.id FROM installment_schedule s
                     WHERE s.installment_plan_id = ? AND s.status = 'paid'
                     ORDER BY s.due_date ASC",
                    [(int) $plan['id']]
                );
                foreach ($paidSchedules as $sched) {
                    try {
                        AccountingIntegrationService::onInstallmentPaid([
                            'plan_id'     => (int) $plan['id'],
                            'schedule_id' => (int) $sched['id'],
                            'member_id'   => (int) $plan['member_id'],
                        ]);
                    } catch (\Throwable $e) {
                        // Non-fatal
                    }
                }
            } catch (\Throwable $e) {
                $result['errors']++;
                Logger::error("GL Sync installment error", ['id' => $plan['id'], 'error' => $e->getMessage()]);
            }
        }
    }

    private static function syncSales($db, array &$result): void
    {
        $tableExists = $db->selectOne("SHOW TABLES LIKE 'sales'");
        if (!$tableExists) return;

        $sales = $db->select(
            "SELECT s.id, s.invoice_number, s.customer_type, s.total_amount, s.sale_date
             FROM sales s
             WHERE s.status = 'completed'
               AND NOT EXISTS (
                   SELECT 1 FROM journal_entries je
                   WHERE je.reference_type = 'sale_cogs' AND je.reference_id = s.id
                     AND je.is_archived = 0 AND je.status != 'reversed'
               )
             ORDER BY s.sale_date ASC, s.id ASC"
        );

        foreach ($sales as $sale) {
            try {
                AccountingIntegrationService::onSaleCompleted([
                    'sale_id'        => (int) $sale['id'],
                    'invoice_number' => $sale['invoice_number'] ?? '',
                    'customer_type'  => $sale['customer_type'] ?? 'guest',
                    'total_amount'   => (string) $sale['total_amount'],
                ]);
                $result['sales']++;
            } catch (\Throwable $e) {
                $result['errors']++;
                Logger::error("GL Sync sale error", ['id' => $sale['id'], 'error' => $e->getMessage()]);
            }
        }
    }

    private static function syncPayroll($db, array &$result): void
    {
        $tableExists = $db->selectOne("SHOW TABLES LIKE 'hr_payroll_runs'");
        if (!$tableExists) return;

        $runs = $db->select(
            "SELECT pr.id, pr.period_id, pr.paid_at
             FROM hr_payroll_runs pr
             WHERE pr.status = 'paid'
               AND NOT EXISTS (
                   SELECT 1 FROM journal_entries je
                   WHERE je.reference_type = 'payroll' AND je.reference_id = pr.id
                     AND je.is_archived = 0 AND je.status != 'reversed'
               )
             ORDER BY pr.paid_at ASC, pr.id ASC"
        );

        foreach ($runs as $run) {
            try {
                AccountingIntegrationService::onPayrollPaid([
                    'payroll_run_id' => (int) $run['id'],
                    'period_id'      => (int) $run['period_id'],
                    'payment_date'   => $run['paid_at'] ? substr($run['paid_at'], 0, 10) : date('Y-m-d'),
                ]);
                $result['payroll']++;
            } catch (\Throwable $e) {
                $result['errors']++;
                Logger::error("GL Sync payroll error", ['id' => $run['id'], 'error' => $e->getMessage()]);
            }
        }
    }

    private static function syncRefunds($db, array &$result): void
    {
        $tableExists = $db->selectOne("SHOW TABLES LIKE 'sale_refunds'");
        if (!$tableExists) return;

        $refunds = $db->select(
            "SELECT r.id, r.sale_id, r.refund_number, r.refund_amount
             FROM sale_refunds r
             WHERE r.refund_amount > 0
               AND NOT EXISTS (
                   SELECT 1 FROM journal_entries je
                   WHERE je.reference_type = 'sale_refund' AND je.reference_id = r.id
                     AND je.is_archived = 0 AND je.status != 'reversed'
               )
             ORDER BY r.id ASC"
        );

        foreach ($refunds as $refund) {
            try {
                AccountingIntegrationService::onSaleRefunded([
                    'sale_id'       => (int) $refund['sale_id'],
                    'refund_id'     => (int) $refund['id'],
                    'refund_number' => $refund['refund_number'] ?? '',
                    'amount'        => (string) $refund['refund_amount'],
                ]);
                $result['refunds']++;
            } catch (\Throwable $e) {
                $result['errors']++;
                Logger::error("GL Sync refund error", ['id' => $refund['id'], 'error' => $e->getMessage()]);
            }
        }
    }

    private static function syncRentalDeposits($db, array &$result): void
    {
        $tableExists = $db->selectOne("SHOW TABLES LIKE 'rental_contracts'");
        if (!$tableExists) return;

        $contracts = $db->select(
            "SELECT rc.id, rc.deposit_payment_id
             FROM rental_contracts rc
             WHERE rc.deposit_payment_id IS NOT NULL AND rc.is_archived = 0
               AND NOT EXISTS (
                   SELECT 1 FROM journal_entries je
                   WHERE je.reference_type = 'rental_deposit' AND je.reference_id = rc.id
                     AND je.is_archived = 0 AND je.status != 'reversed'
               )
             ORDER BY rc.id ASC"
        );

        foreach ($contracts as $contract) {
            try {
                AccountingIntegrationService::onRentalDepositCollected([
                    'contract_id' => (int) $contract['id'],
                    'payment_id'  => (int) $contract['deposit_payment_id'],
                ]);
                $result['rentals']++;
            } catch (\Throwable $e) {
                $result['errors']++;
                Logger::error("GL Sync rental error", ['id' => $contract['id'], 'error' => $e->getMessage()]);
            }
        }
    }
}
