<?php

namespace App\Services\Payroll;

use App\Models\CompanySetting;
use App\Models\EmployeePayroll;
use App\Models\EmployeePayrollComponent;
use App\Models\PayrollComponentDefinition;
use App\Models\PayrollCycle;
use App\Models\PayrollPtSlab;
use App\Models\PayrollRunLog;
use App\Models\User;
use App\Services\Finance\ExpensePayrollService;
use App\Services\Finance\PrePaymentService;
use App\Services\Payroll\IncomeTaxDeclarationService;
use Carbon\Carbon;
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

class PayrollRunService
{
    public function __construct(
        protected PayrollCycleService $cycleService,
        protected EmployeePayrollCalculator $calculator,
        protected IncomeTaxDeclarationService $incomeTaxService,
        protected PrePaymentService $prePaymentService,
        protected ExpensePayrollService $expensePayrollService
    ) {}

    /**
     * Run payroll for a cycle: recalculate all employees and persist. Idempotent when not locked.
     *
     * @return array{processed: int, skipped: int, errors: array<int, string>}
     */
    public function run(PayrollCycle $cycle, ?int $actorUserId = null): array
    {
        $companyId = (int) $cycle->company_id;
        $lock = Cache::lock("payroll-run:{$companyId}:{$cycle->id}", 120);
        try {
            $lock->block(5);
        } catch (LockTimeoutException) {
            return [
                'processed' => 0,
                'skipped' => 0,
                'errors' => ['Payroll run is already in progress for this cycle.'],
            ];
        }

        try {
            $this->cycleService->ensureNotLocked($cycle);
            $this->cycleService->transitionToProcessing($cycle);
            $this->prePaymentService->revertDeductedForPeriod($companyId, (int) $cycle->year, (int) $cycle->month);
            $this->expensePayrollService->revertForPeriod($companyId, (int) $cycle->year, (int) $cycle->month);
            $this->log($companyId, $cycle->id, $actorUserId, 'run_started', ['at' => now()->toIso8601String()]);

            $settings = $this->loadSettings($companyId);
            $ptSlabsByState = $this->loadPtSlabsByState($companyId, $cycle->end_date);
            $definitions = PayrollComponentDefinition::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->active()
                ->orderBy('display_order')
                ->get();

            $start = $cycle->start_date instanceof \DateTimeInterface ? Carbon::parse($cycle->start_date) : Carbon::parse($cycle->start_date);
            $end = $cycle->end_date instanceof \DateTimeInterface ? Carbon::parse($cycle->end_date) : Carbon::parse($cycle->end_date);

            $users = User::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->forPayrollPeriod($start, $end)
                ->get();

            $processed = 0;
            $skipped = 0;
            $errors = [];
            $processedUserIds = [];

            foreach ($users as $user) {
                try {
                    $ptSlabs = $ptSlabsByState[$user->work_state ?? ''] ?? [];
                    $result = $this->calculator->calculate($user, $companyId, $start, $end, $settings, $ptSlabs, $definitions->isEmpty() ? null : $definitions);
                    if ($result === null) {
                        $skipped++;
                        continue;
                    }
                    $result = $this->applyMonthlyTds($companyId, $cycle, $user->id, $result);
                    $this->persistEmployeePayroll($companyId, $cycle, $user->id, $result);
                    $advanceAmount = $this->sumDeductionByCode($result, 'ADVANCE');
                    if ($advanceAmount > 0) {
                        $this->prePaymentService->markDeductedForPeriod(
                            (int) $user->id,
                            $companyId,
                            (int) $cycle->year,
                            (int) $cycle->month
                        );
                    }
                    $hasExpensePayroll = $this->sumEarningByCode($result, 'EXP_REIMB') > 0
                        || $this->sumDeductionByCode($result, 'EXP_RECOVERY') > 0;
                    if ($hasExpensePayroll) {
                        $this->expensePayrollService->markProcessedForEmployee(
                            (int) $user->id,
                            $companyId,
                            (int) $cycle->id,
                            (int) $cycle->year,
                            (int) $cycle->month
                        );
                    }
                    $processedUserIds[] = (int) $user->id;
                    $processed++;
                } catch (\Throwable $e) {
                    $skipped++;
                    $errors[] = "User {$user->id} ({$user->name}): " . $e->getMessage();
                }
            }

            $this->removeOrphanEmployeePayrolls($companyId, $cycle, $processedUserIds);

            $this->cycleService->transitionToDraft($cycle);
            $this->log($companyId, $cycle->id, $actorUserId, 'recalculated', [
                'processed' => $processed,
                'skipped' => $skipped,
                'errors' => $errors,
            ]);

            return ['processed' => $processed, 'skipped' => $skipped, 'errors' => $errors];
        } finally {
            optional($lock)->release();
        }
    }

    /**
     * @return array{pf_enabled: bool, pf_rate: float, pf_ceiling: float, esi_enabled: bool, esi_employee_rate: float, esi_employer_rate: float, esi_cap: float, pt_enabled: bool}
     */
    public function getStatutorySettings(int $companyId): array
    {
        return $this->loadSettings($companyId);
    }

    /**
     * @return array<string, array<int, array{min_gross: float, max_gross: float|null, amount: float}>>
     */
    public function getPtSlabsByState(int $companyId, $effectiveDate): array
    {
        return $this->loadPtSlabsByState($companyId, $effectiveDate);
    }

    protected function loadSettings(int $companyId): array
    {
        $keys = [
            'payroll_pf_enabled', 'payroll_pf_rate', 'payroll_pf_cap',
            'payroll_esi_enabled', 'payroll_esi_rate', 'payroll_esi_cap',
            'payroll_pt_enabled', 'payroll_pt_amount', 'pf_excluded_in_ctc' ,'esi_excluded_in_ctc'
        ];
        $rows = CompanySetting::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('key', $keys)
            ->pluck('value', 'key');
        return [
            'pf_enabled' => ($rows['payroll_pf_enabled'] ?? '0') === '1',
            'pf_rate' => (float) ($rows['payroll_pf_rate'] ?? 12),
            'pf_ceiling' => isset($rows['payroll_pf_cap']) ? (float) $rows['payroll_pf_cap'] : 15000,
            'esi_enabled' => ($rows['payroll_esi_enabled'] ?? '0') === '1',
            'esi_employee_rate' => (float) ($rows['payroll_esi_rate'] ?? 0.75),
            'esi_employer_rate' => 3.25,
            'esi_cap' => isset($rows['payroll_esi_cap']) ? (float) $rows['payroll_esi_cap'] : 21000,
            'pt_enabled' => ($rows['payroll_pt_enabled'] ?? '0') === '1',
            'pt_amount' => isset($rows['payroll_pt_amount']) ? (float) $rows['payroll_pt_amount'] : 200,
            'pf_excluded_in_ctc' => ($rows['pf_excluded_in_ctc'] ?? '0') === '1',
            'esi_excluded_in_ctc' => ($rows['esi_excluded_in_ctc'] ?? '0') === '1',
        ];
    }

    /**
     * @return array<string, array<int, array{min_gross: float, max_gross: float|null, amount: float}>>
     */
    protected function loadPtSlabsByState(int $companyId, $effectiveDate): array
    {
        $date = $effectiveDate instanceof \DateTimeInterface ? $effectiveDate->format('Y-m-d') : $effectiveDate;
        $slabs = PayrollPtSlab::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->effectiveAt($date)
            ->orderBy('min_gross')
            ->get();
        $byState = [];
        foreach ($slabs as $s) {
            $state = $s->state ?? '';
            if (! isset($byState[$state])) {
                $byState[$state] = [];
            }
            $byState[$state][] = [
                'min_gross' => (float) $s->min_gross,
                'max_gross' => $s->max_gross !== null ? (float) $s->max_gross : null,
                'amount' => (float) $s->amount,
            ];
        }
        return $byState;
    }

    protected function applyMonthlyTds(int $companyId, PayrollCycle $cycle, int $userId, array $result): array
    {
        $month = (int) $cycle->month;
        $year = (int) $cycle->year;
        $tdsAmount = $this->incomeTaxService->monthlyTdsForPayroll($companyId, $userId, $month, $year);

        if ($tdsAmount <= 0) {
            $result['tds_amount'] = 0.0;

            return $result;
        }

        $result['deductions'][] = [
            'code' => 'TDS',
            'name' => 'Income Tax (TDS)',
            'amount' => $tdsAmount,
            'meta' => ['financial_year' => \App\Support\FinancialYear::forDate(\Carbon\Carbon::create($year, $month, 1))],
        ];
        $result['total_deductions'] = round((float) $result['total_deductions'] + $tdsAmount, 2);
        $result['net_monthly'] = max(0, round((float) $result['net_monthly'] - $tdsAmount, 2));
        $result['tds_amount'] = $tdsAmount;

        return $result;
    }

    protected function persistEmployeePayroll(int $companyId, PayrollCycle $cycle, int $userId, array $result): void
    {
        DB::transaction(function () use ($companyId, $cycle, $userId, $result) {
            $empPayroll = EmployeePayroll::withoutGlobalScopes()->updateOrCreate(
                [
                    'payroll_cycle_id' => $cycle->id,
                    'user_id' => $userId,
                ],
                [
                    'company_id' => $companyId,
                    'ctc_annual' => $result['ctc_annual'],
                    'gross_monthly' => $result['gross_monthly'],
                    'net_monthly' => $result['net_monthly'],
                    'total_deductions' => $result['total_deductions'],
                    'lop_days' => $result['lop_days'],
                    'lop_amount' => $result['lop_amount'],
                    'working_days' => $result['working_days'],
                    'paid_days' => $result['paid_days'],
                    'status' => 'pending',
                    'payment_date' => null,
                ]
            );

            $empPayroll->components()->delete();
            $order = 0;
            foreach ($result['earnings'] ?? [] as $e) {
                EmployeePayrollComponent::withoutGlobalScopes()->create([
                    'company_id' => $companyId,
                    'employee_payroll_id' => $empPayroll->id,
                    'component_code' => $e['code'] ?? 'EARNING',
                    'name' => $e['name'] ?? 'Earning',
                    'category' => 'earning',
                    'amount' => $e['amount'] ?? 0,
                    'meta' => $e['meta'] ?? null,
                    'display_order' => $order++,
                ]);
            }
            foreach ($result['deductions'] ?? [] as $d) {
                EmployeePayrollComponent::withoutGlobalScopes()->create([
                    'company_id' => $companyId,
                    'employee_payroll_id' => $empPayroll->id,
                    'component_code' => $d['code'] ?? 'DEDUCTION',
                    'name' => $d['name'] ?? 'Deduction',
                    'category' => 'deduction',
                    'amount' => -abs((float) ($d['amount'] ?? 0)),
                    'meta' => $d['meta'] ?? null,
                    'display_order' => $order++,
                ]);
            }
            foreach ($result['employer_contributions'] ?? [] as $c) {
                EmployeePayrollComponent::withoutGlobalScopes()->create([
                    'company_id' => $companyId,
                    'employee_payroll_id' => $empPayroll->id,
                    'component_code' => $c['code'] ?? 'EMPLOYER',
                    'name' => $c['name'] ?? 'Employer',
                    'category' => 'employer_contribution',
                    'amount' => $c['amount'] ?? 0,
                    'meta' => $c['meta'] ?? null,
                    'display_order' => $order++,
                ]);
            }

            $tdsAmount = (float) ($result['tds_amount'] ?? 0);
            if ($tdsAmount > 0) {
                $this->incomeTaxService->recordTdsDeduction(
                    $companyId,
                    $userId,
                    (int) $cycle->id,
                    (int) $empPayroll->id,
                    (int) $cycle->month,
                    (int) $cycle->year,
                    $tdsAmount,
                    (float) ($result['gross_monthly'] ?? 0)
                );
            }
        });
    }

    protected function sumDeductionByCode(array $result, string $code): float
    {
        foreach ($result['deductions'] ?? [] as $deduction) {
            if (($deduction['code'] ?? '') === $code) {
                return (float) ($deduction['amount'] ?? 0);
            }
        }

        return 0.0;
    }

    protected function sumEarningByCode(array $result, string $code): float
    {
        foreach ($result['earnings'] ?? [] as $earning) {
            if (($earning['code'] ?? '') === $code) {
                return (float) ($earning['amount'] ?? 0);
            }
        }

        return 0.0;
    }

    /**
     * @param  array<int>  $processedUserIds
     */
    protected function removeOrphanEmployeePayrolls(int $companyId, PayrollCycle $cycle, array $processedUserIds): void
    {
        $query = EmployeePayroll::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('payroll_cycle_id', $cycle->id);

        if ($processedUserIds !== []) {
            $query->whereNotIn('user_id', $processedUserIds);
        }

        $query->each(function (EmployeePayroll $empPayroll) {
            $empPayroll->components()->delete();
            $empPayroll->delete();
        });
    }

    protected function log(int $companyId, ?int $cycleId, ?int $actorUserId, string $action, array $payload = []): void
    {
        PayrollRunLog::withoutGlobalScopes()->create([
            'company_id' => $companyId,
            'payroll_cycle_id' => $cycleId,
            'actor_user_id' => $actorUserId,
            'action' => $action,
            'payload' => $payload,
        ]);
    }
}
