<?php

namespace App\Services;

use App\Models\Attendance;
use App\Models\CompanySetting;
use App\Models\Holiday;
use App\Models\Leave;
use App\Models\Payroll;
use App\Models\PayrollComponent;
use App\Models\User;
use App\Services\Payroll\SalaryStructureResolver;
use App\Services\Payroll\V2\LeaveLopCalculator;
use Carbon\Carbon;

class PayrollService
{
    public function __construct(
        protected SalaryStructureResolver $salaryResolver,
        protected LeaveLopCalculator $lopCalculator
    ) {}

    public function generate(int $companyId, int $year, int $month): array
    {
        $selectedPeriod = sprintf('%04d-%02d', $year, $month);
        $currentPeriod = now()->format('Y-m');
        if ($selectedPeriod > $currentPeriod) {
            throw new \InvalidArgumentException('Future payroll periods are not allowed.');
        }

        $start = Carbon::createFromDate($year, $month, 1)->startOfDay();
        $end = $start->copy()->endOfMonth();
        $totalDaysInMonth = (int) $end->daysInMonth;

        $users = User::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->forPayrollPeriod($start, $end)
            ->get();

        $holidayDates = Holiday::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereBetween('date', [$start->toDateString(), $end->toDateString()])
            ->pluck('date')
            ->map(fn ($d) => $d instanceof \DateTimeInterface ? $d->format('Y-m-d') : $d)
            ->all();

        $generated = 0;
        $skipped = 0;

        foreach ($users as $user) {
            $periodEnd = Carbon::createFromDate($year, $month, 1)->endOfMonth();
            $context = $this->lopCalculator->buildPayrollContext($user, $companyId, $start, $end, $holidayDates);
            if (! $context['eligible']) {
                $skipped++;
                continue;
            }

            $eligibleStart = $context['eligible_start'];
            $eligibleEnd = $context['eligible_end'];
            $workingDays = $context['payable_working_days'];
            $prorationFactor = $context['proration_factor'];

            $basicSalary = $this->salaryResolver->resolveMonthlyBasic($user, $companyId, $periodEnd);
            if ($basicSalary === null) {
                $skipped++;
                continue;
            }
            $basicSalary = max(0, round((float) $basicSalary * $prorationFactor, 2));
            $components = $this->salaryResolver->resolveMonthlyComponents($user, $companyId, $periodEnd);
            $SalaryRevision = $this->salaryResolver->getEffectiveRevision($user->id, $companyId, $periodEnd);
            if(! $SalaryRevision) {
                $skipped++;
                continue;
            }
            $earningsFromGroup = array_values(array_map(
                fn (array $row) => ['name' => $row['name'], 'value_type' => $row['value_type'] ?? 'fixed', 'value' => $row['value'] ?? 0, 'annual' => $row['annual'] ?? 0, 'monthly' => $row['monthly'] ?? round(($row['annual'] ?? 0) / 12, 2), 'type' => $row['type'] ?? 'earning'],
                array_filter($components, fn (array $row) => ($row['type'] ?? '') === 'earning')
            ));
            $groupDeductions = array_values(array_map(
                fn (array $row) => ['name' => $row['name'], 'value_type' => $row['value_type'] ?? 'fixed', 'value' => $row['value'] ?? 0, 'annual' => $row['annual'] ?? 0, 'monthly' => $row['monthly'] ?? round(($row['annual'] ?? 0) / 12, 2), 'type' => $row['type'] ?? 'deduction'],
                array_filter($components, fn (array $row) => ($row['type'] ?? '') === 'deduction')
            ));

            $grossFromEarnings = $SalaryRevision->ctc_annual - ($SalaryRevision->pf_employer * 12) - ($SalaryRevision->esi_employer * 12);
            $grossSalary = round(($grossFromEarnings / 12) * $prorationFactor, 2);

            $presentDays = $this->lopCalculator->presentDaysForUser($user->id, $companyId, $eligibleStart, $eligibleEnd);
            $leaveData = $this->lopCalculator->leaveDaysForUser($user->id, $companyId, $eligibleStart, $eligibleEnd);
            $paidLeaves = $leaveData['paid'];
            $unpaidLeaves = $leaveData['unpaid'];
            $halfDays = $leaveData['half_days'];

            $paidDays = $presentDays + $paidLeaves + ($halfDays * 0.5);
            $lopDays = max(0, $workingDays - $paidDays);
            $lopAmount = $workingDays > 0 ? round(($grossSalary / $workingDays) * $lopDays, 2) : 0;

            $prePaymentAmount = $this->getPrePaymentAmount($user->id, $companyId, $year, $month);
            $expenseAmount = $this->getExpenseDeductionAmount($user->id, $companyId, $year, $month);

            if ($grossSalary <= 0) {
                $grossSalary = $basicSalary;
            }
            $groupDeductionAmount = round(array_sum(array_column($groupDeductions, 'monthly')) * $prorationFactor, 2);
            $totalDeductions = $lopAmount + $prePaymentAmount + $expenseAmount + $groupDeductionAmount;
            $statutoryDeductions = [
                'pf' => round(($SalaryRevision->pf_employee ?? 0) * $prorationFactor, 2),
                'esi' => round(($SalaryRevision->esi_employee ?? 0) * $prorationFactor, 2),
                'pt' => round(($SalaryRevision->pt ?? 0) * $prorationFactor, 2),
                'total' => round((($SalaryRevision->pf_employee ?? 0) + ($SalaryRevision->esi_employee ?? 0) + ($SalaryRevision->pt ?? 0)) * $prorationFactor, 2),
            ];

            $totalDeductions += $statutoryDeductions['total'];
            $netSalary = max(0, round($grossSalary - $totalDeductions, 2));

            $existing = Payroll::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('user_id', $user->id)
                ->where('year', $year)
                ->where('month', $month)
                ->first();

            $extraFields = [
                'total_office_time' => 0,
                'total_worked_time' => 0,
                'half_days' => (int) $halfDays,
                'late_days' => 0,
                'holiday_count' => 0,
            ];

            if ($existing) {
                $payload = array_merge([
                    'basic_salary' => $basicSalary,
                    'salary_amount' => $grossSalary,
                    'pre_payment_amount' => $prePaymentAmount,
                    'expense_amount' => $expenseAmount,
                    'net_salary' => $netSalary,
                    'total_days' => $totalDaysInMonth,
                    'working_days' => $workingDays,
                    'present_days' => $presentDays,
                    'paid_leaves' => $paidLeaves,
                    'unpaid_leaves' => $lopDays,
                    'status' => 'pending',
                    'payment_date' => null,
                ], $this->filterPayrollExtraFields($existing, $extraFields));

                $existing->update($this->filterPayrollColumns($existing, $payload));
                $payroll = $existing;
                $existing->payrollComponents()->delete();
            } else {
                $payload = array_merge([
                    'company_id' => $companyId,
                    'user_id' => $user->id,
                    'month' => $month,
                    'year' => $year,
                    'basic_salary' => $basicSalary,
                    'salary_amount' => $grossSalary,
                    'pre_payment_amount' => $prePaymentAmount,
                    'expense_amount' => $expenseAmount,
                    'net_salary' => $netSalary,
                    'total_days' => $totalDaysInMonth,
                    'working_days' => $workingDays,
                    'present_days' => $presentDays,
                    'paid_leaves' => $paidLeaves,
                    'unpaid_leaves' => $lopDays,
                    'status' => 'pending',
                    'payment_date' => null,
                ], $this->filterPayrollExtraFields(new Payroll(), $extraFields));

                $payroll = Payroll::withoutGlobalScopes()->create($this->filterPayrollColumns(new Payroll(), $payload));
            }

            $this->createPayrollComponents($companyId, $payroll, $basicSalary, $earningsFromGroup, $groupDeductions, $grossSalary, $lopAmount, $prePaymentAmount, $expenseAmount, $statutoryDeductions);
            $generated++;
        }

        return ['generated' => $generated, 'skipped' => $skipped];
    }

    protected function presentDaysForUser(int $userId, int $companyId, Carbon $start, Carbon $end): float
    {
        $count = Attendance::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->whereBetween('date', [$start->toDateString(), $end->toDateString()])
            ->whereNotNull('clock_in_date_time')
            ->count();
        $halfDays = Attendance::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->whereBetween('date', [$start->toDateString(), $end->toDateString()])
            ->whereNotNull('clock_in_date_time')
            ->where('is_half_day', true)
            ->count();
        return $count - ($halfDays * 0.5);
    }

    protected function leaveDaysForUser(int $userId, int $companyId, Carbon $start, Carbon $end): array
    {
        $leaves = Leave::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->where('status', 'approved')
            ->where(function ($q) use ($start, $end) {
                $q->whereBetween('start_date', [$start->toDateString(), $end->toDateString()])
                    ->orWhereBetween('end_date', [$start->toDateString(), $end->toDateString()])
                    ->orWhere(fn ($q2) => $q2->where('start_date', '<=', $start)->where('end_date', '>=', $end));
            })
            ->get();

        $paid = 0.0;
        $unpaid = 0.0;
        $halfDays = 0;

        foreach ($leaves as $leave) {
            $days = (float) ($leave->total_days ?? 0);
            if ($leave->is_half_day) {
                $halfDays++;
                $days = 0;
            }
            if ($leave->is_paid) {
                $paid += $days;
            } else {
                $unpaid += $days;
            }
        }

        return ['paid' => $paid, 'unpaid' => $unpaid, 'half_days' => $halfDays];
    }

    protected function getPrePaymentAmount(int $userId, int $companyId, int $year, int $month): float
    {
        if (! class_exists(\App\Models\PrePayment::class)) {
            return 0.0;
        }

        $model = new \App\Models\PrePayment;
        $table = $model->getTable();
        $query = \App\Models\PrePayment::withoutGlobalScopes();

        if (\Illuminate\Support\Facades\Schema::hasColumn($table, 'company_id')) {
            $query->where('company_id', $companyId);
        }
        if (\Illuminate\Support\Facades\Schema::hasColumn($table, 'user_id')) {
            $query->where('user_id', $userId);
        }

        $hasYear = \Illuminate\Support\Facades\Schema::hasColumn($table, 'year');
        $hasMonth = \Illuminate\Support\Facades\Schema::hasColumn($table, 'month');
        if ($hasYear && $hasMonth) {
            $query->where('year', $year)->where('month', $month);
        } elseif (\Illuminate\Support\Facades\Schema::hasColumn($table, 'created_at')) {
            $query->whereYear('created_at', $year)->whereMonth('created_at', $month);
        }

        if (\Illuminate\Support\Facades\Schema::hasColumn($table, 'status')) {
            $query->where('status', 'pending');
        }

        $total = $query->sum('amount');
        return (float) $total;
    }

    protected function getExpenseDeductionAmount(int $userId, int $companyId, int $year, int $month): float
    {
        if (! class_exists(\App\Models\Expense::class)) {
            return 0.0;
        }
        $model = new \App\Models\Expense;
        $table = $model->getTable();
        $query = \App\Models\Expense::withoutGlobalScopes();

        if (\Illuminate\Support\Facades\Schema::hasColumn($table, 'company_id')) {
            $query->where('company_id', $companyId);
        }
        if (\Illuminate\Support\Facades\Schema::hasColumn($table, 'user_id')) {
            $query->where('user_id', $userId);
        }

        if (\Illuminate\Support\Facades\Schema::hasColumn($table, 'date')) {
            $query->whereYear('date', $year)->whereMonth('date', $month);
        } elseif (\Illuminate\Support\Facades\Schema::hasColumn($table, 'expense_date')) {
            $query->whereYear('expense_date', $year)->whereMonth('expense_date', $month);
        } elseif (
            \Illuminate\Support\Facades\Schema::hasColumn($table, 'year') &&
            \Illuminate\Support\Facades\Schema::hasColumn($table, 'month')
        ) {
            $query->where('year', $year)->where('month', $month);
        } elseif (\Illuminate\Support\Facades\Schema::hasColumn($table, 'created_at')) {
            $query->whereYear('created_at', $year)->whereMonth('created_at', $month);
        } else {
            return 0.0;
        }

        if (\Illuminate\Support\Facades\Schema::hasColumn($table, 'deduct_from_payroll')) {
            $query->where('deduct_from_payroll', true);
        }
        return (float) $query->sum('amount');
    }

    protected function calculateStatutoryDeductions(int $companyId, float $grossAfterLop): array
    {
        $pf = 0.0;
        $esi = 0.0;
        $pt = 0.0;

        $pfEnabled = $this->getSetting($companyId, 'payroll_pf_enabled', '0') === '1';
        $esiEnabled = $this->getSetting($companyId, 'payroll_esi_enabled', '0') === '1';
        $ptEnabled = $this->getSetting($companyId, 'payroll_pt_enabled', '0') === '1';

        if ($pfEnabled) {
            $pfRate = (float) $this->getSetting($companyId, 'payroll_pf_rate', '12');
            $pfCap = (float) $this->getSetting($companyId, 'payroll_pf_cap', '15000');
            $pfBase = min($grossAfterLop, $pfCap);
            $pf = round($pfBase * ($pfRate / 100), 2);
        }
        if ($esiEnabled) {
            $esiRate = (float) $this->getSetting($companyId, 'payroll_esi_rate', '0.75');
            $esiCap = (float) $this->getSetting($companyId, 'payroll_esi_cap', '21000');
            if ($grossAfterLop <= $esiCap) {
                $esi = round($grossAfterLop * ($esiRate / 100), 2);
            }
        }
        if ($ptEnabled) {
            $ptAmount = (float) $this->getSetting($companyId, 'payroll_pt_amount', '200');
            $pt = $ptAmount;
        }

        return [
            'pf' => $pf,
            'esi' => $esi,
            'pt' => $pt,
            'total' => $pf + $esi + $pt,
        ];
    }

    /**
     * Return only extra payroll fields that exist as columns on the payrolls table.
     *
     * @param Payroll $model
     * @param array<string, int|float> $extra
     * @return array<string, int|float>
     */
    protected function filterPayrollExtraFields(Payroll $model, array $extra): array
    {
        return $this->filterPayrollColumns($model, $extra);
    }

    /**
     * Return only fields that exist as columns on payrolls table.
     *
     * @param Payroll $model
     * @param array<string, mixed> $fields
     * @return array<string, mixed>
     */
    protected function filterPayrollColumns(Payroll $model, array $fields): array
    {
        $table = $model->getTable();
        $schema = \Illuminate\Support\Facades\Schema::getColumnListing($table);
        $result = [];
        foreach ($fields as $column => $value) {
            if (in_array($column, $schema, true)) {
                $result[$column] = $value;
            }
        }
        return $result;
    }

    protected function getSetting(int $companyId, string $key, $default = null)
    {
        $s = CompanySetting::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('key', $key)
            ->first();
        return $s ? $s->value : $default;
    }

    protected function createPayrollComponents(
        int $companyId,
        Payroll $payroll,
        float $basicSalary,
        array $earningsFromGroup,
        array $groupDeductions,
        float $grossSalary,
        float $lopAmount,
        float $prePaymentAmount,
        float $expenseAmount,
        array $statutoryDeductions
    ): void {
        $earningsCreated = 0;
        foreach ($earningsFromGroup as $earning) {
            $amt = (float) ($earning['monthly'] ?? 0);
            if ($amt <= 0) {
                continue;
            }
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => $earning['name'] ?? 'Earning',
                'value_type' => 'fixed',
                'amount' => $amt,
                'is_earning' => true,
                'type' => 'salary',
            ]);
            $earningsCreated++;
        }
        if ($earningsCreated === 0) {
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => 'Basic Salary',
                'value_type' => 'fixed',
                'amount' => $basicSalary,
                'is_earning' => true,
                'type' => 'salary',
            ]);
        }
        foreach ($groupDeductions as $deduction) {
            $amt = max(0, (float) ($deduction['monthly'] ?? 0));
            if ($amt <= 0) {
                continue;
            }
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => $deduction['name'] ?? 'Deduction',
                'value_type' => 'fixed',
                'amount' => -$amt,
                'is_earning' => false,
                'type' => 'deduction',
            ]);
        }

        $specialAllowances = $grossSalary - array_sum(array_column($earningsFromGroup, 'monthly'));

        if ($specialAllowances > 0) {
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => 'Special Allowance',
                'value_type' => 'fixed',
                'amount' => round($specialAllowances, 2),
                'is_earning' => true,
                'type' => 'salary',
            ]);
        }

        if ($lopAmount > 0) {
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => 'Loss of Pay',
                'value_type' => 'fixed',
                'amount' => -$lopAmount,
                'is_earning' => false,
                'type' => 'deduction',
            ]);
        }
        if ($prePaymentAmount > 0) {
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => 'Advance / Pre-payment',
                'value_type' => 'fixed',
                'amount' => -$prePaymentAmount,
                'is_earning' => false,
                'type' => 'deduction',
            ]);
        }
        if ($expenseAmount > 0) {
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => 'Expense Deduction',
                'value_type' => 'fixed',
                'amount' => -$expenseAmount,
                'is_earning' => false,
                'type' => 'deduction',
            ]);
        }
        if ($statutoryDeductions['pf'] > 0) {
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => 'PF (Employee)',
                'value_type' => 'fixed',
                'amount' => -$statutoryDeductions['pf'],
                'is_earning' => false,
                'type' => 'deduction',
            ]);
        }
        if ($statutoryDeductions['esi'] > 0) {
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => 'ESI',
                'value_type' => 'fixed',
                'amount' => -$statutoryDeductions['esi'],
                'is_earning' => false,
                'type' => 'deduction',
            ]);
        }
        if ($statutoryDeductions['pt'] > 0) {
            PayrollComponent::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'payroll_id' => $payroll->id,
                'name' => 'Professional Tax',
                'value_type' => 'fixed',
                'amount' => -$statutoryDeductions['pt'],
                'is_earning' => false,
                'type' => 'deduction',
            ]);
        }
    }
}
