<?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->execute("DELETE FROM hr_payroll_components_log WHERE payroll_run_id IN (SELECT id FROM hr_payroll_runs WHERE period_id = ?)", [$periodId]);
        $db->execute("DELETE FROM hr_insurance_records WHERE period_id = ?", [$periodId]);
        $db->execute("DELETE FROM hr_tax_records WHERE period_id = ?", [$periodId]);
        $db->execute("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,
            'employee_count'   => $processedCount,
            'calculated_at'    => date('Y-m-d H:i:s'),
            'calculated_by'    => $employee ? (int) $employee->id : null,
            'updated_at'       => date('Y-m-d H:i:s'),
        ], '`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 9: Net salary ──
        $totalDeductions = bcadd(
            bcadd(bcadd(bcadd($insuranceEmployee, $taxAmount, 2), $loanDeduction, 2), $penaltyDeduction, 2),
            $absenceDeduction,
            2
        );
        $netSalary = bcsub($grossEarnings, $totalDeductions, 2);
        if (bccomp($netSalary, '0', 2) < 0) {
            $netSalary = '0.00';
        }

        // ── Step 10: Persist records ──
        $calculationJson = json_encode([
            'overtime'   => $overtime,
            'insurance'  => $insurance,
            'tax'        => $tax,
            'attendance' => $attendance,
            'working_days' => $workingDays,
        ], JSON_UNESCAPED_UNICODE);

        $runId = $db->insert('hr_payroll_runs', [
            'period_id'            => $periodId,
            'employee_profile_id'  => $profileId,
            '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'     => '0.00',
            '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,
            '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);
        }

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