<?php
class PayrollEngine
{
    private PDO $pdo;

    const OT_WEEKDAY = 1.5;
    const OT_WEEKEND = 2.0;
    const OT_HOLIDAY = 3.0;

    const PIT_BRACKETS = [
        [5_000_000,   0.05],
        [10_000_000,  0.10],
        [18_000_000,  0.15],
        [32_000_000,  0.20],
        [52_000_000,  0.25],
        [80_000_000,  0.30],
        [PHP_INT_MAX, 0.35],
    ];

    const SI_EMPLOYEE_RATE    = 0.105;
    const SI_COMPANY_RATE     = 0.215;
    const PERSONAL_DEDUCTION  = 15_500_000;
    const DEPENDANT_DEDUCTION = 6_200_000;
    const LATE_GRACE_MINUTES  = 5;
    const LATE_BLOCK_MINUTES  = 30;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    // ── Tính ngày công chuẩn ────────────────────────────────────────────
    public function calcWorkingDays(string $from, string $to): int
    {
        $stmt = $this->pdo->prepare(
            "SELECT holiday_date FROM holidays WHERE holiday_date BETWEEN ? AND ?"
        );
        $stmt->execute([$from, $to]);
        $holidays = $stmt->fetchAll(PDO::FETCH_COLUMN);

        $count   = 0;
        $current = new DateTime($from);
        $end     = new DateTime($to);
        while ($current <= $end) {
            if ($current->format('N') != 7 && !in_array($current->format('Y-m-d'), $holidays))
                $count++;
            $current->modify('+1 day');
        }
        return $count;
    }

    // ── Tính lương 1 nhân viên ──────────────────────────────────────────
    public function calculate(int $periodId, int $userId): array
    {
        $period  = $this->getPeriod($periodId);
        $profile = $this->getProfile($userId);
        $salary  = $this->getSalaryComponents($userId);

        // ── Các khoản cố định theo code ───────────────────────────────
        $basicSalary    = (int)($salary['basic']            ?? 0);
        $mealAllow      = (int)($salary['meal']             ?? 0);
        $clothesAllow   = (int)($salary['clothes']          ?? 0);
        $phoneAllow     = (int)($salary['phone']            ?? 0);
        $transportAllow = (int)($salary['transport']        ?? 0);
        $performBonus   = (int)($salary['performance']      ?? 0);
        $attendBonus    = (int)($salary['attendance_bonus'] ?? 0);

        // ── Tổng lương (KHÔNG bao gồm chuyên cần) ────────────────────
        // Dùng cho: lương/ngày, trừ muộn, tính công
        $totalSalaryNoAttend = 0;
        foreach ($salary['all_components'] as $comp) {
            if ($comp['component_code'] === 'attendance_bonus') continue;
            if (in_array($comp['component_type'], ['earning', 'bonus']))
                $totalSalaryNoAttend += (int)$comp['amount'];
        }

        $workingDays = (int)$period['working_days'];

        // ── Lương 1 ngày = tổng (trừ chuyên cần) / ngày công ─────────
        $salaryPerDay = $workingDays > 0
            ? round($totalSalaryNoAttend / $workingDays) : 0;

        // ── Lương 1 giờ tổng — dùng trừ muộn ────────────────────────
        $totalPerHour = round($salaryPerDay / 8);

        // ── Lương 1 giờ CƠ BẢN — chỉ dùng cho OT ────────────────────
        $basicPerDay   = $workingDays > 0 ? round($basicSalary / $workingDays) : 0;
        $salaryPerHour = round($basicPerDay / 8);

        // ── Dữ liệu chấm công / nghỉ phép / OT ───────────────────────
        $att = $this->getAttendanceData($userId, $period['period_from'], $period['period_to']);
        $lv  = $this->getLeaveData($userId, $period['period_from'], $period['period_to']);
        $ot  = $this->getOTData($userId, $period['period_from'], $period['period_to']);

        $actualWorkdays     = (float)($att['actual_workdays']      ?? 0);
        $paidLeaveDays      = (float)($lv['paid_leave_days']        ?? 0);
        $otherPaidLeaveDays = (float)($lv['other_paid_leave_days']  ?? 0);
        $unpaidLeaveDays    = (float)($lv['unpaid_leave_days']      ?? 0);

        $lateMinutes    = (float)($att['total_late_minutes']  ?? 0)
                        + (float)($att['total_early_minutes'] ?? 0);
        $otWeekdayHours = (float)($ot['ot_weekday_hours'] ?? 0);
        $otWeekendHours = (float)($ot['ot_weekend_hours'] ?? 0);
        $otHolidayHours = (float)($ot['ot_holiday_hours'] ?? 0);
        $isLateWarning  = (bool)($att['has_early_leave']    ?? false);
        $lateWarningNote = (string)($att['early_leave_dates'] ?? '');

        // ── Tổng ngày hưởng lương ─────────────────────────────────────
        $totalPaidDays = $actualWorkdays + $paidLeaveDays + $otherPaidLeaveDays;

        // ── Đi muộn/về sớm ───────────────────────────────────────────
        [$lateHours, $lateDeduction] = $this->calcLateDeduction($lateMinutes, $totalPerHour);

        // ── KPI ───────────────────────────────────────────────────────
        $kpiData      = $this->getKpiData($userId, $period['period_from'], $period['period_to']);
        $kpiDaysCount = count($kpiData);
        $kpiDeduction = 0;
        foreach ($kpiData as $kDay) {
            if ((int)$kDay['is_deducted'] === 1) {
                $diff = (float)$kDay['salary_per_day'] - (float)$kDay['salary_actual'];
                $kpiDeduction += max(0, $diff);
            }
        }
        $kpiDeduction = (int)round($kpiDeduction);

        // ── Lương cơ bản thực nhận ────────────────────────────────────
        $basicReceived = $workingDays > 0
            ? round($basicSalary * ($totalPaidDays / $workingDays)) : 0;

        // ── Tỷ lệ trợ cấp ────────────────────────────────────────────
        $allowanceRatio = $workingDays > 0
            ? min(1.0, $totalPaidDays / $workingDays) : 0;

        $mealReceived      = round($mealAllow      * $allowanceRatio);
        $clothesReceived   = round($clothesAllow   * $allowanceRatio);
        $phoneReceived     = round($phoneAllow     * $allowanceRatio);
        $transportReceived = round($transportAllow * $allowanceRatio);

        // ── Các khoản earning+bonus khác ──────────────────────────────
        $otherComponentsReceived = 0;
        foreach ($salary['all_components'] as $comp) {
            $code = $comp['component_code'];
            if (in_array($code, [
                'basic', 'meal', 'clothes', 'phone',
                'transport', 'performance', 'attendance_bonus'
            ])) continue;
            if (!in_array($comp['component_type'], ['earning', 'bonus'])) continue;
            $otherComponentsReceived += round((int)$comp['amount'] * $allowanceRatio);
        }

        // ── Chuyên cần ────────────────────────────────────────────────
        // Điều kiện: không nghỉ không phép (unpaid = 0) VÀ không bị trừ KPI
        $kpiHasDeduction = $kpiDeduction > 0;
        $attendEligible  = ($unpaidLeaveDays == 0 && !$kpiHasDeduction);
        $attendReceived  = ($attendEligible && $attendBonus > 0) ? $attendBonus : 0;

        // ── OT ────────────────────────────────────────────────────────
        $otWeekdayAmt = round($otWeekdayHours * $salaryPerHour * self::OT_WEEKDAY);
        $otWeekendAmt = round($otWeekendHours * $salaryPerHour * self::OT_WEEKEND);
        $otHolidayAmt = round($otHolidayHours * $salaryPerHour * self::OT_HOLIDAY);
        $totalOtAmt   = $otWeekdayAmt + $otWeekendAmt + $otHolidayAmt;

        // ── Phép tồn ──────────────────────────────────────────────────
        $leaveTotal     = $this->calcAnnualLeave($userId, (int)$period['period_year']);
        $leaveUsed      = $this->getLeaveUsed($userId, (int)$period['period_year']);
        $leaveRemaining = max(0, $leaveTotal - $leaveUsed);
        $leavePayout    = 0;

        // ── BHXH ──────────────────────────────────────────────────────
        $hasInsurance = (int)($profile['has_insurance'] ?? 0);
        $siEmployee   = $hasInsurance ? round($basicSalary * self::SI_EMPLOYEE_RATE) : 0;
        $siCompany    = $hasInsurance ? round($basicSalary * self::SI_COMPANY_RATE)  : 0;

        // ── Thuế TNCN ─────────────────────────────────────────────────
        $dependants         = (int)($profile['dependants'] ?? 0);
        $dependantDeduction = $dependants * self::DEPENDANT_DEDUCTION;

        $grossForTax = $basicReceived
                     + $mealReceived + $clothesReceived
                     + $phoneReceived + $transportReceived
                     + $performBonus
                     + $otherComponentsReceived
                     + $attendReceived
                     + $totalOtAmt;

        $taxableIncome = max(0,
            $grossForTax - $siEmployee
            - self::PERSONAL_DEDUCTION
            - $dependantDeduction
        );
        $pitAmount = $this->calcPIT($taxableIncome);

        // ── Gross & Net ───────────────────────────────────────────────
        $grossSalary = $grossForTax + $leavePayout;
        $netSalary   = max(0,
            $grossSalary - $siEmployee - $pitAmount - $lateDeduction - $kpiDeduction
        );

        // ── Ghi chú tự động ───────────────────────────────────────────
        $remarkParts = [];
        if ($kpiDeduction > 0)
            $remarkParts[] = "Tru KPI: " . number_format($kpiDeduction) . " d ($kpiDaysCount ngay khong dat)";
        if ($lateDeduction > 0)
            $remarkParts[] = "Tru muon/som: " . number_format($lateDeduction) . " d";
        if (!$attendEligible && $attendBonus > 0)
            $remarkParts[] = "Khong duoc chuyen can: nghi khong phep=$unpaidLeaveDays ngay, KPI deduct=$kpiHasDeduction";

        return [
            'period_id'                  => $periodId,
            'user_id'                    => $userId,
            'basic_salary'               => $basicSalary,
            'working_days_standard'      => $workingDays,
            'salary_per_day'             => $salaryPerDay,
            'salary_per_hour'            => $totalPerHour,
            'basic_salary_per_hour'      => $salaryPerHour,
            'actual_workdays'            => $actualWorkdays,
            'paid_leave_days'            => $paidLeaveDays,
            'other_paid_leave_days'      => $otherPaidLeaveDays,
            'unpaid_leave_days'          => $unpaidLeaveDays,
            'late_early_hours'           => $lateHours,
            'late_early_deduction'       => $lateDeduction,
            'total_paid_days'            => $totalPaidDays,
            'basic_salary_received'      => $basicReceived,
            'meal_allowance'             => $mealAllow,
            'meal_received'              => $mealReceived,
            'clothes_allowance'          => $clothesAllow,
            'clothes_received'           => $clothesReceived,
            'phone_allowance'            => $phoneAllow,
            'phone_received'             => $phoneReceived,
            'transport_allowance'        => $transportAllow,
            'transport_received'         => $transportReceived,
            'performance_bonus'          => $performBonus,
            'attendance_bonus'           => $attendReceived,
            'attendance_bonus_eligible'  => $attendEligible ? 1 : 0,
            'ot_weekday_hours'           => $otWeekdayHours,
            'ot_weekend_hours'           => $otWeekendHours,
            'ot_holiday_hours'           => $otHolidayHours,
            'ot_weekday_amount'          => $otWeekdayAmt,
            'ot_weekend_amount'          => $otWeekendAmt,
            'ot_holiday_amount'          => $otHolidayAmt,
            'total_ot_amount'            => $totalOtAmt,
            'annual_leave_total'         => $leaveTotal,
            'annual_leave_used'          => $leaveUsed,
            'annual_leave_remaining'     => $leaveRemaining,
            'annual_leave_payout'        => $leavePayout,
            'other_income'               => 0,
            'adjustment'                 => 0,
            'other_bonus'                => 0,
            'has_social_insurance'       => $hasInsurance,
            'si_employee'                => $siEmployee,
            'si_company'                 => $siCompany,
            'dependants'                 => $dependants,
            'personal_deduction'         => self::PERSONAL_DEDUCTION,
            'dependant_deduction'        => $dependantDeduction,
            'ot_exclude_pit'             => 0,
            'taxable_income'             => $taxableIncome,
            'pit_amount'                 => $pitAmount,
            'late_deduction'             => $lateDeduction,
            'kpi_deduction'              => $kpiDeduction,
            'gross_salary'               => $grossSalary,
            'advance_payment'            => 0,
            'net_salary'                 => $netSalary,
            'pit_adjustment'             => 0,
            'bank_transfer'              => $netSalary,
            'remark'                     => implode('; ', $remarkParts),
            'is_late_warning'            => $isLateWarning ? 1 : 0,
            'late_warning_note'          => $lateWarningNote,
            'manually_adjusted'          => 0,
        ];
    }

    // ── Các method private giữ nguyên ───────────────────────────────────
    private function getKpiData(int $userId, string $from, string $to): array
    {
        try {
            $stmt = $this->pdo->prepare("
                SELECT kr.salary_actual, kr.salary_per_day,
                       kr.is_deducted,  ka.assign_date
                FROM kpi_results kr
                JOIN kpi_assignments ka ON kr.kpi_assignment_id = ka.id
                WHERE ka.user_id = ? AND ka.assign_date BETWEEN ? AND ?
            ");
            $stmt->execute([$userId, $from, $to]);
            return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (\Throwable $e) {
            error_log("getKpiData error uid=$userId: " . $e->getMessage());
            return [];
        }
    }

    private function calcAnnualLeave(int $userId, int $periodYear): int
    {
        $stmt = $this->pdo->prepare("
            SELECT date_joined, annual_leave_total
            FROM employee_profiles WHERE user_id = ?
        ");
        $stmt->execute([$userId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        $leavePerYear = (int)($row['annual_leave_total'] ?? 12);
        $dateJoined   = $row['date_joined'] ?? null;
        if (!$dateJoined) return $leavePerYear;

        $joined   = new DateTime($dateJoined);
        $joinYear = (int)$joined->format('Y');
        if ($joinYear < $periodYear) return $leavePerYear;
        if ($joinYear > $periodYear) return 0;

        $joinMonth    = (int)$joined->format('m');
        $joinDay      = (int)$joined->format('d');
        $startMonth   = ($joinDay > 1) ? $joinMonth + 1 : $joinMonth;
        $monthsWorked = max(0, 12 - $startMonth + 1);
        return (int)round($monthsWorked * $leavePerYear / 12);
    }

    private function getAttendanceData(int $userId, string $from, string $to): array
    {
        try {
            $stmt = $this->pdo->prepare("
                SELECT
                    COUNT(CASE WHEN check_in IS NOT NULL THEN 1 END)
                        AS actual_workdays,
                    COALESCE(SUM(CASE WHEN is_late = 1 THEN late_minutes ELSE 0 END), 0)
                        AS total_late_minutes,
                    COALESCE(SUM(CASE WHEN early_leave = 1 THEN early_leave_minutes ELSE 0 END), 0)
                        AS total_early_minutes,
                    MAX(CASE WHEN early_leave = 1 THEN 1 ELSE 0 END)
                        AS has_early_leave,
                    GROUP_CONCAT(
                        CASE WHEN early_leave = 1
                        THEN DATE_FORMAT(work_date, '%d/%m/%Y') END
                        ORDER BY work_date SEPARATOR ', '
                    ) AS early_leave_dates
                FROM attendance_logs
                WHERE user_id = ? AND work_date BETWEEN ? AND ?
            ");
            $stmt->execute([$userId, $from, $to]);
            return $stmt->fetch(PDO::FETCH_ASSOC) ?: $this->emptyAttendance();
        } catch (\Throwable $e) {
            error_log("getAttendanceData error uid=$userId: " . $e->getMessage());
            return $this->emptyAttendance();
        }
    }

    private function getLeaveData(int $userId, string $from, string $to): array
    {
        try {
            $stmt = $this->pdo->prepare("
                SELECT
                    COALESCE(SUM(CASE WHEN leave_type = 'annual'
                        THEN total_days ELSE 0 END), 0)        AS paid_leave_days,
                    COALESCE(SUM(CASE WHEN leave_type IN ('other','sick')
                        THEN total_days ELSE 0 END), 0)        AS other_paid_leave_days,
                    COALESCE(SUM(CASE WHEN leave_type = 'unpaid'
                        THEN total_days ELSE 0 END), 0)        AS unpaid_leave_days
                FROM leave_requests
                WHERE user_id = ? AND status = 'approved'
                  AND start_date BETWEEN ? AND ?
            ");
            $stmt->execute([$userId, $from, $to]);
            return $stmt->fetch(PDO::FETCH_ASSOC) ?: $this->emptyLeave();
        } catch (\Throwable $e) {
            error_log("getLeaveData error uid=$userId: " . $e->getMessage());
            return $this->emptyLeave();
        }
    }

    private function getOTData(int $userId, string $from, string $to): array
    {
        try {
            $stmt = $this->pdo->prepare("
                SELECT
                    COALESCE(SUM(CASE WHEN ot_type = 'weekday'
                        THEN hours ELSE 0 END), 0) AS ot_weekday_hours,
                    COALESCE(SUM(CASE WHEN ot_type = 'weekend'
                        THEN hours ELSE 0 END), 0) AS ot_weekend_hours,
                    COALESCE(SUM(CASE WHEN ot_type = 'holiday'
                        THEN hours ELSE 0 END), 0) AS ot_holiday_hours
                FROM overtime_requests
                WHERE user_id = ? AND status = 'approved'
                  AND ot_date BETWEEN ? AND ?
            ");
            $stmt->execute([$userId, $from, $to]);
            return $stmt->fetch(PDO::FETCH_ASSOC) ?: $this->emptyOT();
        } catch (\Throwable $e) {
            error_log("getOTData error uid=$userId: " . $e->getMessage());
            return $this->emptyOT();
        }
    }

    private function getLeaveUsed(int $userId, int $year): float
    {
        try {
            $stmt = $this->pdo->prepare("
                SELECT COALESCE(SUM(total_days), 0)
                FROM leave_requests
                WHERE user_id = ? AND status = 'approved'
                  AND leave_type = 'annual' AND YEAR(start_date) = ?
            ");
            $stmt->execute([$userId, $year]);
            return (float)$stmt->fetchColumn();
        } catch (\Throwable $e) {
            error_log("getLeaveUsed error uid=$userId: " . $e->getMessage());
            return 0;
        }
    }

    private function calcLateDeduction(float $totalMinutes, int $salaryPerHour): array
    {
        if ($totalMinutes <= self::LATE_GRACE_MINUTES) return [0.0, 0];
        $over      = $totalMinutes - self::LATE_GRACE_MINUTES;
        $blocks    = (int)ceil($over / self::LATE_BLOCK_MINUTES);
        $lateHours = $blocks * (self::LATE_BLOCK_MINUTES / 60);
        $deduction = (int)round($blocks * ($salaryPerHour / 2));
        return [$lateHours, $deduction];
    }

    private function calcPIT(int $taxableIncome): int
    {
        if ($taxableIncome <= 0) return 0;
        $tax = 0; $prev = 0;
        foreach (self::PIT_BRACKETS as [$limit, $rate]) {
            if ($taxableIncome <= $prev) break;
            $tax += (min($taxableIncome, $limit) - $prev) * $rate;
            $prev = $limit;
            if ($taxableIncome <= $limit) break;
        }
        return (int)round($tax);
    }

    private function emptyAttendance(): array
    {
        return [
            'actual_workdays'     => 0,
            'total_late_minutes'  => 0,
            'total_early_minutes' => 0,
            'has_early_leave'     => 0,
            'early_leave_dates'   => '',
        ];
    }

    private function emptyLeave(): array
    {
        return [
            'paid_leave_days'       => 0,
            'other_paid_leave_days' => 0,
            'unpaid_leave_days'     => 0,
        ];
    }

    private function emptyOT(): array
    {
        return [
            'ot_weekday_hours' => 0,
            'ot_weekend_hours' => 0,
            'ot_holiday_hours' => 0,
        ];
    }

    private function getPeriod(int $id): array
    {
        $stmt = $this->pdo->prepare("SELECT * FROM payroll_periods WHERE id = ?");
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
    }

    private function getProfile(int $userId): array
    {
        $stmt = $this->pdo->prepare("SELECT * FROM employee_profiles WHERE user_id = ?");
        $stmt->execute([$userId]);
        $profile = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$profile) return [];
        if (!isset($profile['annual_leave_total'])) $profile['annual_leave_total'] = 12;
        if (!isset($profile['dependants']))          $profile['dependants']         = 0;
        if (!isset($profile['has_insurance']))       $profile['has_insurance']      = 0;
        return $profile;
    }

    private function getSalaryComponents(int $userId): array
    {
        $stmt = $this->pdo->prepare("
            SELECT sc.component_code, sc.component_type,
                   sc.component_name,  es.amount
            FROM employee_salaries es
            JOIN salary_components sc ON es.component_id = sc.id
            WHERE es.user_id = ? AND es.is_active = 1
        ");
        $stmt->execute([$userId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $map = [];
        foreach ($rows as $r) {
            $map[$r['component_code']] = (int)$r['amount'];
        }
        $map['all_components'] = $rows;
        return $map;
    }

    // ── Public: lấy lương/ngày cho 1 user theo kỳ lương hiện hành ───────
    public function getSalaryPerDay(int $userId): array
    {
        // Lấy kỳ lương đang mở gần nhất
        $stmt = $this->pdo->prepare("
            SELECT working_days FROM payroll_periods
            WHERE status != 'locked'
            ORDER BY period_from DESC LIMIT 1
        ");
        $stmt->execute();
        $period = $stmt->fetch(PDO::FETCH_ASSOC);
        $workingDays = $period ? (int)$period['working_days'] : 26;

        $salary = $this->getSalaryComponents($userId);

        $totalNoAttend = 0;
        foreach ($salary['all_components'] as $comp) {
            if ($comp['component_code'] === 'attendance_bonus') continue;
            if (in_array($comp['component_type'], ['earning', 'bonus']))
                $totalNoAttend += (int)$comp['amount'];
        }

        $salaryPerDay = $workingDays > 0 ? round($totalNoAttend / $workingDays) : 0;

        return [
            'salary_per_day' => $salaryPerDay,
            'working_days'   => $workingDays,
            'total_salary'   => $totalNoAttend,
        ];
    }
}