<?php
declare(strict_types=1);

namespace App\Modules\HR\Services;

use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\HR\Models\HrPayrollPeriod;
use App\Modules\HR\Models\HrPayrollRun;
use App\Modules\HR\Models\HrEmployeeProfile;
use App\Modules\HR\Models\HrEmployeeSalaryDetail;
use App\Modules\HR\Models\HrAttendance;
use App\Modules\HR\Models\HrEmployeeLoan;
use App\Modules\HR\Models\HrTaxRecord;

/**
 * Core Payroll Calculation Engine
 *
 * Orchestrates the full payroll pipeline for a period:
 *   1. Load salary structure + effective components per employee
 *   2. AttendanceService -> working days, absent days, overtime
 *   3. Calculate gross: basic + all earning components + overtime
 *   4. SocialInsuranceService -> employee/employer insurance shares
 *   5. IncomeTaxService -> monthly tax (YTD-aware progressive brackets)
 *   6. LoanService -> due installment deductions
 *   7. DisciplinaryService -> wage deduction penalties
 *   8. Absent day deductions
 *   9. Net = gross - all deductions
 *   10. INSERT payroll_runs + components_log + insurance_records + tax_records
 *   11. UPDATE period totals + status
 *
 * All monetary arithmetic uses bcmath with scale=2.
 */
final class PayrollCalculationService
{
    public static function calculateForPeriod(int $periodId): array
    {
        $db = App::getInstance()->db();
        $employee = App::getInstance()->currentEmployee();

        $period = $db->selectOne("SELECT * FROM hr_payroll_periods WHERE id = ?", [$periodId]);
        if (!$period) {
            return ['success' => false, 'error' => 'فترة الرواتب غير موجودة'];
        }
        if (!in_array($period['status'], ['open', 'processing'], true)) {
            return ['success' => false, 'error' => 'فترة الرواتب ليست مفتوحة للحساب'];
        }

        $year = (int) $period['year'];
        $month = (int) $period['month'];

        // Mark as processing
        $db->update('hr_payroll_periods', [
            'status'     => 'processing',
            'updated_at' => date('Y-m-d H:i:s'),
        ], '`id` = ?', [$periodId]);

        // Get all active employees
        $activeEmployees = HrEmployeeProfile::getActiveIds();
        if (empty($activeEmployees)) {
            return ['success' => false, 'error' => 'لا يوجد موظفون نشطون'];
        }

        // Delete previous runs for this period (recalculation)
        $db->query("DELETE FROM hr_payroll_components_log WHERE payroll_run_id IN (SELECT id FROM hr_payroll_runs WHERE period_id = ?)", [$periodId]);
        $db->query("DELETE FROM hr_insurance_records WHERE period_id = ?", [$periodId]);
        $db->query("DELETE FROM hr_tax_records WHERE period_id = ?", [$periodId]);
        $db->query("DELETE FROM hr_payroll_runs WHERE period_id = ?", [$periodId]);

        $totalGross = '0.00';
        $totalDeductions = '0.00';
        $totalNet = '0.00';
        $processedCount = 0;
        $errors = [];

        foreach ($activeEmployees as $emp) {
            $profileId = (int) $emp['id'];
            $employeeId = (int) $emp['employee_id'];

            try {
                $result = self::calculateForEmployee($profileId, $employeeId, $periodId, $year, $month);
                if ($result['success']) {
                    $totalGross = bcadd($totalGross, $result['gross_earnings'], 2);
                    $totalDeductions = bcadd($totalDeductions, $result['total_deductions'], 2);
                    $totalNet = bcadd($totalNet, $result['net_salary'], 2);
                    $processedCount++;
                } else {
                    $errors[] = "Employee #{$profileId}: " . ($result['error'] ?? 'Unknown error');
                }
            } catch (\Throwable $e) {
                $errors[] = "Employee #{$profileId}: " . $e->getMessage();
                Logger::error("Payroll calc failed for employee {$profileId}: " . $e->getMessage());
            }
        }

        // Update period totals
        $db->update('hr_payroll_periods', [
            'status'           => 'calculated',
            'total_gross'      => $totalGross,
            'total_deductions' => $totalDeductions,
            'total_net'        => $totalNet,
            'updated_at'       => date('Y-m-d H:i:s'),
            'updated_by'       => $employee ? (int) $employee->id : null,
        ], '`id` = ?', [$periodId]);

        Logger::info("Payroll calculated", [
            'period_id' => $periodId,
            'employees' => $processedCount,
            'total_net' => $totalNet,
        ]);

        return [
            'success'         => true,
            'processed_count' => $processedCount,
            'total_gross'     => $totalGross,
            'total_deductions'=> $totalDeductions,
            'total_net'       => $totalNet,
            'errors'          => $errors,
        ];
    }

    private static function calculateForEmployee(int $profileId, int $employeeId, int $periodId, int $year, int $month): array
    {
        $db = App::getInstance()->db();

        $profile = HrEmployeeProfile::find($profileId);
        if (!$profile) {
            return ['success' => false, 'error' => 'Profile not found'];
        }

        $basicSalary = $profile->basic_salary ?? '0.00';
        if (bccomp($basicSalary, '0', 2) <= 0) {
            return ['success' => false, 'error' => 'No basic salary set'];
        }

        // ── Step 1: Load salary components ──
        $components = HrEmployeeSalaryDetail::getForEmployee($profileId);
        $componentLog = [];
        $sortOrder = 0;

        // ── Step 2: Attendance summary ──
        $attendance = HrAttendance::getMonthlySummary($profileId, $year, $month);
        $workingDays = AttendanceService::getMonthlyWorkingDays($profileId, $year, $month);
        $presentDays = (int) ($attendance['present_days'] ?? 0) + (int) ($attendance['late_days'] ?? 0);
        $absentDays = (int) ($attendance['absent_days'] ?? 0);
        $totalOvertimeHours = $attendance['total_overtime_hours'] ?? '0.00';

        // ── Step 3: Calculate gross earnings ──
        $grossEarnings = $basicSalary;
        $totalAllowances = '0.00';
        $overtimeBase = $basicSalary; // Components that affect overtime
        $taxableTotal = $basicSalary;
        $insurableBasic = $basicSalary;
        $insurableVariable = '0.00';

        // Log basic salary
        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'BASIC',
            'name_ar'        => 'الراتب الأساسي',
            'type'           => 'earning',
            'amount'         => $basicSalary,
            'sort_order'     => $sortOrder,
        ];

        foreach ($components as $comp) {
            if ($comp['type'] !== 'earning' || $comp['is_system_calculated']) {
                continue;
            }

            $amount = $comp['amount'] ?? '0.00';

            // For percentage-based components, recalculate
            if ($comp['calculation_type'] === 'percentage_of_basic' && $comp['percentage_value']) {
                $amount = bcmul($basicSalary, bcdiv($comp['percentage_value'], '100', 6), 2);
            }

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

            $grossEarnings = bcadd($grossEarnings, $amount, 2);
            $totalAllowances = bcadd($totalAllowances, $amount, 2);

            if ($comp['affects_overtime']) {
                $overtimeBase = bcadd($overtimeBase, $amount, 2);
            }
            if ($comp['is_taxable']) {
                $taxableTotal = bcadd($taxableTotal, $amount, 2);
            }
            if ($comp['is_insurable'] && $comp['is_variable']) {
                $insurableVariable = bcadd($insurableVariable, $amount, 2);
            }

            $sortOrder++;
            $componentLog[] = [
                'component_code' => $comp['component_code'],
                'name_ar'        => $comp['name_ar'],
                'type'           => 'earning',
                'amount'         => $amount,
                'sort_order'     => $sortOrder,
            ];
        }

        // ── Overtime calculation ──
        $dayOT = $totalOvertimeHours; // Simplified: all counted as day OT
        $nightOT = '0.00';
        $scheduleData = AttendanceService::getScheduleForEmployee($profileId, sprintf('%04d-%02d-15', $year, $month));
        $hoursPerDay = $scheduleData['hours_per_day'] ?? '8.00';
        if (($scheduleData['is_night_shift'] ?? 0) && bccomp($totalOvertimeHours, '0', 2) > 0) {
            $nightOT = $totalOvertimeHours;
            $dayOT = '0.00';
        }

        $overtime = OvertimeService::calculate($basicSalary, $overtimeBase, $workingDays, $hoursPerDay, $dayOT, $nightOT);
        $overtimeAmount = $overtime['total_overtime_amount'];
        $grossEarnings = bcadd($grossEarnings, $overtimeAmount, 2);
        $taxableTotal = bcadd($taxableTotal, $overtimeAmount, 2);

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'OVERTIME',
            'name_ar'        => 'أجر إضافي',
            'type'           => 'earning',
            'amount'         => $overtimeAmount,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 4: Social Insurance ──
        $insurance = SocialInsuranceService::calculate($insurableBasic, $insurableVariable);
        $insuranceEmployee = $insurance['total_employee_share'];
        $insuranceEmployer = $insurance['total_employer_share'];

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'INSURANCE_DED',
            'name_ar'        => 'حصة الموظف في التأمينات',
            'type'           => 'deduction',
            'amount'         => $insuranceEmployee,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 5: Income Tax ──
        $ytdRecord = HrTaxRecord::getYtdForEmployee($employeeId, $year);
        $ytdTaxableIncome = $ytdRecord['ytd_taxable_income'] ?? '0.00';
        $ytdTaxPaid = $ytdRecord['ytd_tax'] ?? '0.00';

        $tax = IncomeTaxService::calculateMonthly($taxableTotal, $insuranceEmployee, $ytdTaxableIncome, $ytdTaxPaid, $month);
        $taxAmount = $tax['monthly_tax'];

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'TAX_DED',
            'name_ar'        => 'ضريبة كسب العمل',
            'type'           => 'deduction',
            'amount'         => $taxAmount,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 6: Loan deductions ──
        $dueInstallments = HrEmployeeLoan::getDueInstallments($profileId, $year, $month);
        $loanDeduction = '0.00';
        foreach ($dueInstallments as $inst) {
            $loanDeduction = bcadd($loanDeduction, $inst['amount'], 2);
        }

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'LOAN_DED',
            'name_ar'        => 'قسط سلفة / قرض',
            'type'           => 'deduction',
            'amount'         => $loanDeduction,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 7: Disciplinary deductions ──
        $penaltyDeduction = DisciplinaryService::getMonthlyDeductionAmount($profileId, $basicSalary, $year, $month);

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'PENALTY_DED',
            'name_ar'        => 'خصم جزاء تأديبي',
            'type'           => 'deduction',
            'amount'         => $penaltyDeduction,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 8: Absence deductions ──
        $dailyRate = bcdiv($basicSalary, '30', 4);
        $absenceDeduction = bcmul($dailyRate, (string) $absentDays, 2);

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'ABSENCE_DED',
            'name_ar'        => 'خصم غياب',
            'type'           => 'deduction',
            'amount'         => $absenceDeduction,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 8a: Martyrs Fund deduction ──
        $martyrsFundRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = ?", ['hr.deduction.martyrs_fund_amount']);
        $martyrsFundDeduction = $martyrsFundRow ? $martyrsFundRow['config_value'] : '0.00';

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'MARTYRS_FUND_DED',
            'name_ar'        => 'صندوق الشهداء',
            'type'           => 'deduction',
            'amount'         => $martyrsFundDeduction,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 8b: Solidarity Fund (صندوق التكافل) — % of gross ──
        $solidarityRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = ?", ['hr.deduction.solidarity_fund_rate']);
        $solidarityRate = ($solidarityRow && $solidarityRow['config_value'] !== '') ? $solidarityRow['config_value'] : '0.0025';
        $solidarityDeduction = bcmul($grossEarnings, $solidarityRate, 2);

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'SOLIDARITY_FUND_DED',
            'name_ar'        => 'مساهمة صندوق التكافل',
            'type'           => 'deduction',
            'amount'         => $solidarityDeduction,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 8c: External Loan Deduction (financing companies & bank loans) ──
        $externalLoanRow = $db->selectOne(
            "SELECT COALESCE(SUM(monthly_salary_impact), 0) AS total_external FROM hr_employee_loans WHERE employee_profile_id = ? AND loan_type IN ('financing_company', 'bank_loan') AND status IN ('approved', 'disbursed', 'repaying') AND is_archived = 0",
            [$profileId]
        );
        $externalLoanDeduction = $externalLoanRow ? $externalLoanRow['total_external'] : '0.00';

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'EXTERNAL_LOAN_DED',
            'name_ar'        => 'خصم قروض خارجية',
            'type'           => 'deduction',
            'amount'         => $externalLoanDeduction,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 9: Net before stamp duty ──
        $otherDeductions = bcadd(
            bcadd($martyrsFundDeduction, $solidarityDeduction, 2),
            $externalLoanDeduction,
            2
        );
        $totalDeductionsBeforeStamp = bcadd(
            bcadd(
                bcadd(bcadd(bcadd($insuranceEmployee, $taxAmount, 2), $loanDeduction, 2), $penaltyDeduction, 2),
                $absenceDeduction,
                2
            ),
            $otherDeductions,
            2
        );
        $netBeforeStamp = bcsub($grossEarnings, $totalDeductionsBeforeStamp, 2);
        if (bccomp($netBeforeStamp, '0', 2) < 0) {
            $netBeforeStamp = '0.00';
        }

        // ── Step 9a: Stamp Duty (دمغة عادية وإضافية) — % of net ──
        $stampRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = ?", ['hr.deduction.regular_stamp_duty']);
        $stampRate = ($stampRow && $stampRow['config_value'] !== '' && $stampRow['config_value'] !== '0') ? $stampRow['config_value'] : '0.03';
        $stampDeduction = bcmul($netBeforeStamp, $stampRate, 2);

        $sortOrder++;
        $componentLog[] = [
            'component_code' => 'STAMP_DUTY_DED',
            'name_ar'        => 'دمغة عادية وإضافية',
            'type'           => 'deduction',
            'amount'         => $stampDeduction,
            'sort_order'     => $sortOrder,
        ];

        // ── Step 9b: Final net salary ──
        $totalDeductions = bcadd($totalDeductionsBeforeStamp, $stampDeduction, 2);
        $netSalary = bcsub($netBeforeStamp, $stampDeduction, 2);
        if (bccomp($netSalary, '0', 2) < 0) {
            $netSalary = '0.00';
        }

        // Include stamp in other_deductions for storage
        $otherDeductions = bcadd($otherDeductions, $stampDeduction, 2);

        // ── Step 10: Persist records ──
        $calculationJson = json_encode([
            'overtime'           => $overtime,
            'insurance'          => $insurance,
            'tax'                => $tax,
            'attendance'         => $attendance,
            'working_days'       => $workingDays,
            'solidarity_fund'    => ['rate' => $solidarityRate, 'amount' => $solidarityDeduction],
            'stamp_duty'         => ['rate' => $stampRate, 'amount' => $stampDeduction, 'base' => $netBeforeStamp],
        ], JSON_UNESCAPED_UNICODE);

        $db->beginTransaction();
        try {

        $runId = $db->insert('hr_payroll_runs', [
            'period_id'            => $periodId,
            'employee_profile_id'  => $profileId,
            'employee_number'      => $profile->employee_number ?? '',
            'basic_salary'         => $basicSalary,
            'gross_earnings'       => $grossEarnings,
            'total_allowances'     => $totalAllowances,
            'overtime_amount'      => $overtimeAmount,
            'total_deductions'     => $totalDeductions,
            'insurance_employee'   => $insuranceEmployee,
            'insurance_employer'   => $insuranceEmployer,
            'tax_amount'           => $taxAmount,
            'loan_deduction'       => $loanDeduction,
            'penalty_deduction'    => $penaltyDeduction,
            'absence_deduction'    => $absenceDeduction,
            'other_deductions'     => $otherDeductions,
            'net_salary'           => $netSalary,
            'working_days'         => $workingDays,
            'present_days'         => $presentDays,
            'absent_days'          => $absentDays,
            'overtime_hours'       => $totalOvertimeHours,
            'calculation_json'     => $calculationJson,
            'status'               => 'calculated',
            'created_at'           => date('Y-m-d H:i:s'),
            'updated_at'           => date('Y-m-d H:i:s'),
        ]);

        // Insert component log
        foreach ($componentLog as $log) {
            $db->insert('hr_payroll_components_log', [
                'payroll_run_id'  => $runId,
                'component_code'  => $log['component_code'],
                'name_ar'         => $log['name_ar'],
                'type'            => $log['type'],
                'amount'          => $log['amount'],
                'sort_order'      => $log['sort_order'],
                'created_at'      => date('Y-m-d H:i:s'),
            ]);
        }

        // Insert insurance record
        $db->insert('hr_insurance_records', [
            'employee_profile_id'      => $profileId,
            'period_id'                => $periodId,
            'payroll_run_id'           => $runId,
            'basic_insurable_salary'   => $insurance['basic_insurable_salary'],
            'variable_insurable_salary'=> $insurance['variable_insurable_salary'],
            'employee_basic_share'     => $insurance['employee_basic_share'],
            'employee_variable_share'  => $insurance['employee_variable_share'],
            'total_employee_share'     => $insuranceEmployee,
            'employer_basic_share'     => $insurance['employer_basic_share'],
            'employer_variable_share'  => $insurance['employer_variable_share'],
            'total_employer_share'     => $insuranceEmployer,
            'total_contribution'       => $insurance['total_contribution'],
            'created_at'               => date('Y-m-d H:i:s'),
            'updated_at'               => date('Y-m-d H:i:s'),
        ]);

        // Insert tax record
        $db->insert('hr_tax_records', [
            'employee_id'            => $employeeId,
            'period_id'              => $periodId,
            'payroll_run_id'         => $runId,
            'month'                  => $month,
            'gross_taxable_monthly'  => $tax['gross_taxable_monthly'],
            'annual_taxable_income'  => $tax['annual_taxable_income'],
            'personal_exemption'     => $tax['personal_exemption'],
            'insurance_exemption'    => $tax['insurance_exemption'],
            'net_taxable_income'     => $tax['net_taxable_income'],
            'annual_tax'             => $tax['annual_tax'],
            'monthly_tax'            => $taxAmount,
            'ytd_tax'                => $tax['ytd_tax'],
            'ytd_taxable_income'     => $tax['ytd_taxable_income'],
            'calculation_json'       => json_encode($tax['calculation_json'], JSON_UNESCAPED_UNICODE),
            'created_at'             => date('Y-m-d H:i:s'),
            'updated_at'             => date('Y-m-d H:i:s'),
        ]);

        // Process loan installments
        foreach ($dueInstallments as $inst) {
            LoanService::processInstallmentDeduction((int) $inst['loan_id'], $inst['amount'], $runId);
        }

        $db->commit();
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }

        return [
            'success'          => true,
            'run_id'           => $runId,
            'gross_earnings'   => $grossEarnings,
            'total_deductions' => $totalDeductions,
            'net_salary'       => $netSalary,
        ];
    }
}
