<?php

namespace Modules\Payroll\Services;

use App\Models\ActivityLogModel;
use App\Models\AttendanceRecordModel;
use App\Models\EmployeeModel;
use App\Models\EmployeeSalaryStructureModel;
use App\Models\HolidayModel;
use App\Models\LeaveRequestModel;
use App\Models\LeaveTypeModel;
use App\Models\PayrollComponentModel;
use App\Models\PayrollItemLineModel;
use App\Models\PayrollItemModel;
use App\Models\PayrollRunModel;
use App\Models\PayrollSettingsModel;

class PayrollCalculationService
{
    protected PayrollRunModel $runs;
    protected PayrollItemModel $items;
    protected PayrollItemLineModel $lines;
    protected PayrollComponentModel $components;
    protected PayrollSettingsModel $settings;
    protected EmployeeModel $employees;
    protected EmployeeSalaryStructureModel $salaries;
    protected AttendanceRecordModel $attendance;
    protected LeaveRequestModel $leaveRequests;
    protected LeaveTypeModel $leaveTypes;
    protected HolidayModel $holidays;
    protected ActivityLogModel $activity;

    public function __construct()
    {
        $this->runs          = new PayrollRunModel();
        $this->items         = new PayrollItemModel();
        $this->lines         = new PayrollItemLineModel();
        $this->components    = new PayrollComponentModel();
        $this->settings      = new PayrollSettingsModel();
        $this->employees     = new EmployeeModel();
        $this->salaries      = new EmployeeSalaryStructureModel();
        $this->attendance    = new AttendanceRecordModel();
        $this->leaveRequests = new LeaveRequestModel();
        $this->leaveTypes    = new LeaveTypeModel();
        $this->holidays      = new HolidayModel();
        $this->activity      = new ActivityLogModel();
    }

    public function createRun(int $year, int $month, ?int $actingUserId): array
    {
        if ($month < 1 || $month > 12) {
            return ['success' => false, 'errors' => ['period_month' => 'Invalid month.']];
        }

        if ($this->runs->findByPeriod($year, $month)) {
            return ['success' => false, 'errors' => ['period' => 'Payroll run already exists for this period.']];
        }

        $title = date('F Y', mktime(0, 0, 0, $month, 1, $year)) . ' Payroll';
        $id    = $this->runs->insert([
            'period_year'  => $year,
            'period_month' => $month,
            'title'        => $title,
            'status'       => 'draft',
            'run_date'     => date('Y-m-d'),
            'created_by'   => $actingUserId,
            'updated_by'   => $actingUserId,
        ]);

        $this->activity->log('payroll_runs', 'created', $actingUserId, "Created payroll run #{$id} for {$title}", (string) $id);

        return ['success' => true, 'id' => $id];
    }

    public function calculateRun(int $runId, ?int $actingUserId): array
    {
        $run = $this->runs->find($runId);
        if (! $run) {
            return ['success' => false, 'errors' => ['id' => 'Payroll run not found.']];
        }

        if (in_array($run['status'], ['paid', 'locked'], true)) {
            return ['success' => false, 'errors' => ['status' => 'Cannot recalculate a paid or locked payroll run.']];
        }

        $settings    = $this->settings->getSettings();
        $componentMap = $this->components->getCodeMap();
        $year        = (int) $run['period_year'];
        $month       = (int) $run['period_month'];
        $periodStart = sprintf('%04d-%02d-01', $year, $month);
        $periodEnd   = date('Y-m-t', strtotime($periodStart));

        $unpaidTypeIds = array_column(
            $this->leaveTypes->where('is_paid', 0)->findAll(),
            'id'
        );
        $holidayDates = $this->getHolidayDates($periodStart, $periodEnd);

        $this->items->deleteForRun($runId);

        $employees = $this->employees->scopeForList(null, 'active')->findAll();
        $processed = 0;
        $skipped   = [];

        foreach ($employees as $employee) {
            $employeeId = (int) $employee['id'];
            $structure  = $this->salaries->getEffectiveForEmployee($employeeId, $periodEnd);

            if (! $structure) {
                $skipped[] = $employee['full_name'] . ' (no salary structure)';
                continue;
            }

            $attendanceSummary = $this->summarizeAttendance(
                $employeeId,
                $periodStart,
                $periodEnd,
                $holidayDates
            );
            $unpaidLeaveDays = $this->getUnpaidLeaveDays(
                $employeeId,
                $periodStart,
                $periodEnd,
                $unpaidTypeIds
            );

            $calc = $this->calculateEmployeePay(
                $employee,
                $structure,
                $settings,
                $componentMap,
                $attendanceSummary,
                $unpaidLeaveDays
            );

            $itemId = $this->items->insert([
                'payroll_run_id'         => $runId,
                'employee_id'            => $employeeId,
                'days_worked'            => $attendanceSummary['days_worked'],
                'days_absent'            => $attendanceSummary['days_absent'],
                'unpaid_leave_days'      => $unpaidLeaveDays,
                'overtime_minutes_day'   => $attendanceSummary['overtime_minutes_day'],
                'overtime_minutes_night' => $attendanceSummary['overtime_minutes_night'],
                'gross_earnings'         => $calc['gross_earnings'],
                'total_deductions'       => $calc['total_deductions'],
                'net_pay'                => $calc['net_pay'],
                'employer_sio'           => $calc['employer_sio'],
                'employer_eosb'          => $calc['employer_eosb'],
                'employer_total_cost'    => $calc['employer_total_cost'],
                'status'                 => 'draft',
            ]);

            $this->lines->replaceForItem((int) $itemId, $calc['lines']);
            $processed++;
        }

        $this->runs->update($runId, [
            'status'     => 'calculated',
            'updated_by' => $actingUserId,
        ]);

        $this->activity->log(
            'payroll_runs',
            'calculated',
            $actingUserId,
            "Calculated payroll run #{$runId}: {$processed} employees",
            (string) $runId
        );

        return [
            'success'   => true,
            'processed' => $processed,
            'skipped'   => $skipped,
        ];
    }

    public function approveRun(int $runId, ?int $actingUserId): array
    {
        $run = $this->runs->find($runId);
        if (! $run) {
            return ['success' => false, 'errors' => ['id' => 'Payroll run not found.']];
        }

        if ($run['status'] !== 'calculated') {
            return ['success' => false, 'errors' => ['status' => 'Only calculated runs can be approved.']];
        }

        $this->runs->update($runId, [
            'status'      => 'approved',
            'approved_by' => $actingUserId,
            'approved_at' => date('Y-m-d H:i:s'),
            'updated_by'  => $actingUserId,
        ]);

        $this->items->where('payroll_run_id', $runId)->set(['status' => 'approved'])->update();

        $this->activity->log('payroll_runs', 'approved', $actingUserId, "Approved payroll run #{$runId}", (string) $runId);

        return ['success' => true];
    }

    public function markPaid(int $runId, ?int $actingUserId): array
    {
        $run = $this->runs->find($runId);
        if (! $run) {
            return ['success' => false, 'errors' => ['id' => 'Payroll run not found.']];
        }

        if ($run['status'] !== 'approved') {
            return ['success' => false, 'errors' => ['status' => 'Only approved runs can be marked as paid.']];
        }

        $this->runs->update($runId, [
            'status'  => 'paid',
            'paid_at' => date('Y-m-d H:i:s'),
            'updated_by' => $actingUserId,
        ]);

        $this->items->where('payroll_run_id', $runId)->set(['status' => 'paid'])->update();

        $this->activity->log('payroll_runs', 'paid', $actingUserId, "Marked payroll run #{$runId} as paid", (string) $runId);

        return ['success' => true];
    }

    public function deleteRun(int $runId, ?int $actingUserId): array
    {
        $run = $this->runs->find($runId);
        if (! $run) {
            return ['success' => false, 'error' => 'Payroll run not found.'];
        }

        if (in_array($run['status'], ['paid', 'locked'], true)) {
            return ['success' => false, 'error' => 'Cannot delete a paid or locked payroll run.'];
        }

        $this->items->deleteForRun($runId);
        $this->runs->delete($runId);

        $this->activity->log('payroll_runs', 'deleted', $actingUserId, "Deleted payroll run #{$runId}", (string) $runId);

        return ['success' => true];
    }

    protected function calculateEmployeePay(
        array $employee,
        array $structure,
        array $settings,
        array $componentMap,
        array $attendance,
        float $unpaidLeaveDays
    ): array {
        $basic           = (float) $structure['basic_salary'];
        $social          = (float) $structure['social_allowance'];
        $housing         = (float) $structure['housing_allowance'];
        $transport       = (float) $structure['transport_allowance'];
        $other           = (float) $structure['other_allowances'];
        $workingDays     = (int) $settings['working_days_per_month'];
        $dailyHours      = (float) $settings['daily_work_hours'];
        $sioBase         = min($this->salaries->sioBase($structure), (float) $settings['sio_cap_amount']);

        $dailyRate  = $workingDays > 0 ? $basic / $workingDays : 0;
        $hourlyRate = ($workingDays > 0 && $dailyHours > 0) ? $basic / ($workingDays * $dailyHours) : 0;

        $otDayAmount = ($attendance['overtime_minutes_day'] / 60)
            * $hourlyRate * (float) $settings['overtime_day_multiplier'];
        $otNightAmount = ($attendance['overtime_minutes_night'] / 60)
            * $hourlyRate * (float) $settings['overtime_night_multiplier'];

        $absenceDeduction   = $attendance['days_absent'] * $dailyRate;
        $unpaidLeaveDeduction = $unpaidLeaveDays * $dailyRate;

        $isBahraini = $this->isBahrainiNational($employee);
        if ($isBahraini) {
            $sioEmployee = $sioBase * ((float) $settings['sio_bahraini_employee_rate'] / 100);
            $sioEmployer = $sioBase * ((float) $settings['sio_bahraini_employer_rate'] / 100);
            $eosbEmployer = 0.0;
        } else {
            $sioEmployee = $sioBase * ((float) $settings['sio_expat_employee_rate'] / 100);
            $sioEmployer = $sioBase * ((float) $settings['sio_expat_employer_rate'] / 100);
            $years       = $this->yearsOfService($employee['hire_date'] ?? null);
            $eosbRate    = $years < 3
                ? (float) $settings['eosb_rate_first_3_years']
                : (float) $settings['eosb_rate_after_3_years'];
            $eosbEmployer = $sioBase * ($eosbRate / 100);
        }

        $grossEarnings = $basic + $social + $housing + $transport + $other + $otDayAmount + $otNightAmount;
        $totalDeductions = $sioEmployee + $absenceDeduction + $unpaidLeaveDeduction;
        $netPay = max(0, $grossEarnings - $totalDeductions);
        $employerTotalCost = $grossEarnings + $sioEmployer + $eosbEmployer;

        $lines = [];
        $addLine = static function (string $code, float $amount, ?string $notes = null) use (&$lines, $componentMap): void {
            if (! isset($componentMap[$code]) || abs($amount) < 0.0005) {
                return;
            }
            $lines[] = [
                'component_id' => (int) $componentMap[$code]['id'],
                'amount'       => round($amount, 3),
                'notes'        => $notes,
            ];
        };

        $addLine('BASIC', $basic);
        $addLine('SOCIAL', $social);
        $addLine('HOUSING', $housing);
        $addLine('TRANSPORT', $transport);
        $addLine('OTHER', $other);
        $addLine('OT_DAY', $otDayAmount);
        $addLine('OT_NIGHT', $otNightAmount);
        $addLine('SIO_EE', $sioEmployee);
        $addLine('ABSENCE', -$absenceDeduction);
        $addLine('UNPAID_LV', -$unpaidLeaveDeduction);
        $addLine('SIO_ER', $sioEmployer);
        $addLine('EOSB', $eosbEmployer);

        return [
            'gross_earnings'      => round($grossEarnings, 3),
            'total_deductions'    => round($totalDeductions, 3),
            'net_pay'             => round($netPay, 3),
            'employer_sio'        => round($sioEmployer, 3),
            'employer_eosb'       => round($eosbEmployer, 3),
            'employer_total_cost' => round($employerTotalCost, 3),
            'lines'               => $lines,
        ];
    }

    protected function summarizeAttendance(
        int $employeeId,
        string $periodStart,
        string $periodEnd,
        array $holidayDates
    ): array {
        $records = $this->attendance->scopeForList($periodStart, $periodEnd, $employeeId)->findAll();

        $daysWorked = 0.0;
        $daysAbsent = 0.0;
        $otDay      = 0;
        $otNight    = 0;

        foreach ($records as $record) {
            $date   = $record['attendance_date'];
            $status = $record['status'];
            $otMin  = (int) $record['overtime_minutes'];

            if (in_array($status, ['present', 'late', 'auto_present'], true)) {
                $daysWorked += 1;
            } elseif ($status === 'half_day') {
                $daysWorked += 0.5;
                $daysAbsent += 0.5;
            } elseif ($status === 'absent') {
                $daysAbsent += 1;
            }

            if ($otMin > 0) {
                if (isset($holidayDates[$date])) {
                    $otNight += $otMin;
                } else {
                    $otDay += $otMin;
                }
            }
        }

        return [
            'days_worked'            => $daysWorked,
            'days_absent'            => $daysAbsent,
            'overtime_minutes_day'   => $otDay,
            'overtime_minutes_night' => $otNight,
        ];
    }

    protected function getUnpaidLeaveDays(
        int $employeeId,
        string $periodStart,
        string $periodEnd,
        array $unpaidTypeIds
    ): float {
        if ($unpaidTypeIds === []) {
            return 0.0;
        }

        $requests = $this->leaveRequests
            ->where('employee_id', $employeeId)
            ->where('status', 'approved')
            ->whereIn('leave_type_id', $unpaidTypeIds)
            ->where('end_date >=', $periodStart)
            ->where('start_date <=', $periodEnd)
            ->findAll();

        $total = 0.0;
        foreach ($requests as $req) {
            $start = max($req['start_date'], $periodStart);
            $end   = min($req['end_date'], $periodEnd);
            if ($end >= $start) {
                $total += LeaveRequestModel::calculateDays($start, $end);
            }
        }

        return $total;
    }

    protected function getHolidayDates(string $periodStart, string $periodEnd): array
    {
        $rows = $this->holidays
            ->where('date >=', $periodStart)
            ->where('date <=', $periodEnd)
            ->findAll();

        $map = [];
        foreach ($rows as $row) {
            $map[$row['date']] = true;
        }

        return $map;
    }

    public function isBahrainiNational(array $employee): bool
    {
        $n = strtolower(trim((string) ($employee['nationality'] ?? '')));

        return in_array($n, ['bahrain', 'bahraini', 'bh', 'kingdom of bahrain'], true)
            || str_contains($n, 'bahrain');
    }

    public function yearsOfService(?string $hireDate): float
    {
        if (! $hireDate) {
            return 0.0;
        }

        $start = new \DateTime($hireDate);
        $now   = new \DateTime();
        $diff  = $start->diff($now);

        return (float) $diff->y + ($diff->m / 12) + ($diff->d / 365);
    }
}
