<?php

namespace App\Services\Hr;

use App\Models\FnfSettlement;
use App\Models\LeaveBalance;
use App\Models\LeaveType;
use App\Models\Offboarding;
use App\Models\User;
use App\Services\Leave\LeaveLedgerService;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

class FnfSettlementCalculator
{
    public function __construct(
        protected LeaveLedgerService $leaveLedger
    ) {}

    public function createDraftFromOffboarding(Offboarding $offboarding): FnfSettlement
    {
        $existing = FnfSettlement::withoutGlobalScopes()
            ->where('offboarding_id', $offboarding->id)
            ->first();

        if ($existing) {
            return $this->recalculate($existing);
        }

        $user = User::withoutGlobalScopes()->findOrFail($offboarding->user_id);
        $lwd = $offboarding->last_working_date ?? $offboarding->offboarding_date;

        $settlement = FnfSettlement::create([
            'company_id' => $offboarding->company_id,
            'user_id' => $offboarding->user_id,
            'offboarding_id' => $offboarding->id,
            'last_working_date' => $lwd,
            'notice_period_days' => $offboarding->notice_period_days ?? 30,
            'status' => FnfSettlement::STATUS_DRAFT,
        ]);

        return $this->recalculate($settlement, [
            'notice_served_days' => $this->estimateNoticeServedDays($user, $lwd, (int) ($offboarding->notice_period_days ?? 30)),
        ]);
    }

    /**
     * @param  array<string, mixed>  $overrides
     */
    public function recalculate(FnfSettlement $settlement, array $overrides = []): FnfSettlement
    {
        $user = User::withoutGlobalScopes()->findOrFail($settlement->user_id);
        $dailyRate = (float) ($overrides['daily_rate'] ?? $settlement->daily_rate);
        if ($dailyRate <= 0) {
            $dailyRate = $this->resolveDailyRate($user);
        }

        $noticePeriod = (int) ($overrides['notice_period_days'] ?? $settlement->notice_period_days ?? 30);
        $noticeServed = (int) ($overrides['notice_served_days'] ?? $settlement->notice_served_days ?? 0);

        if ($noticeServed <= 0 && $settlement->last_working_date) {
            $noticeServed = $this->estimateNoticeServedDays($user, $settlement->last_working_date, $noticePeriod);
        }

        $noticeShortfall = max(0, $noticePeriod - $noticeServed);
        $noticeRecovery = round($noticeShortfall * $dailyRate, 2);
        $noticePay = max(0, (float) ($overrides['notice_pay_amount'] ?? $settlement->notice_pay_amount ?? 0));

        $leaveData = $this->calculateLeaveEncashment($user);
        $leaveDays = (float) ($overrides['leave_encashment_days'] ?? $leaveData['days']);
        $leaveLines = $leaveData['lines'];

        if ($leaveData['days'] > 0 && abs($leaveDays - $leaveData['days']) > 0.01) {
            $ratio = $leaveDays / $leaveData['days'];
            $leaveLines = array_map(function (array $line) use ($ratio) {
                $line['days'] = round((float) $line['days'] * $ratio, 2);

                return $line;
            }, $leaveLines);
        }

        $leaveAmount = round($leaveDays * $dailyRate, 2);

        $salaryDues = round((float) ($overrides['salary_dues_amount'] ?? $settlement->salary_dues_amount ?? 0), 2);
        $bonusGratuity = round((float) ($overrides['bonus_gratuity_amount'] ?? $settlement->bonus_gratuity_amount ?? 0), 2);
        $otherEarnings = round((float) ($overrides['other_earnings'] ?? $settlement->other_earnings ?? 0), 2);

        $loanRecovery = round((float) ($overrides['loan_recovery'] ?? $settlement->loan_recovery ?? 0), 2);
        $assetRecovery = round((float) ($overrides['asset_recovery'] ?? $settlement->asset_recovery ?? 0), 2);
        $otherRecovery = round((float) ($overrides['other_recovery'] ?? $settlement->other_recovery ?? 0), 2);

        $totalRecovery = round($noticeRecovery + $loanRecovery + $assetRecovery + $otherRecovery, 2);
        $totalEarnings = round($noticePay + $leaveAmount + $salaryDues + $bonusGratuity + $otherEarnings, 2);
        $netPayable = round($totalEarnings - $totalRecovery, 2);

        $breakdown = [
            'daily_rate' => $dailyRate,
            'notice' => [
                'period_days' => $noticePeriod,
                'served_days' => $noticeServed,
                'shortfall_days' => $noticeShortfall,
                'pay_amount' => $noticePay,
                'recovery_amount' => $noticeRecovery,
            ],
            'leave_encashment' => [
                'days' => $leaveDays,
                'amount' => $leaveAmount,
                'lines' => $leaveLines,
            ],
            'earnings' => [
                'salary_dues' => $salaryDues,
                'bonus_gratuity' => $bonusGratuity,
                'other' => $otherEarnings,
            ],
            'recoveries' => [
                'loan' => $loanRecovery,
                'asset' => $assetRecovery,
                'other' => $otherRecovery,
            ],
        ];

        $settlement->update(array_merge([
            'daily_rate' => $dailyRate,
            'notice_period_days' => $noticePeriod,
            'notice_served_days' => $noticeServed,
            'notice_pay_amount' => $noticePay,
            'notice_recovery_amount' => $noticeRecovery,
            'leave_encashment_days' => $leaveDays,
            'leave_encashment_amount' => $leaveAmount,
            'salary_dues_amount' => $salaryDues,
            'bonus_gratuity_amount' => $bonusGratuity,
            'other_earnings' => $otherEarnings,
            'loan_recovery' => $loanRecovery,
            'asset_recovery' => $assetRecovery,
            'other_recovery' => $otherRecovery,
            'total_earnings' => $totalEarnings,
            'total_recovery' => $totalRecovery,
            'net_payable' => $netPayable,
            'breakdown_json' => $breakdown,
        ], array_intersect_key($overrides, array_flip([
            'last_working_date', 'notes', 'status',
        ]))));

        return $settlement->fresh();
    }

    public function approve(FnfSettlement $settlement, int $approverId): FnfSettlement
    {
        $settlement->update([
            'status' => FnfSettlement::STATUS_APPROVED,
            'approved_at' => now(),
            'approved_by' => $approverId,
        ]);

        return $settlement->fresh();
    }

    public function markPaid(FnfSettlement $settlement, int $payerId): FnfSettlement
    {
        return DB::transaction(function () use ($settlement, $payerId) {
            $this->applyLeaveEncashment($settlement);

            $settlement->update([
                'status' => FnfSettlement::STATUS_PAID,
                'paid_at' => now(),
                'paid_by' => $payerId,
            ]);

            $offboarding = $settlement->offboarding;
            if ($offboarding) {
                $offboarding->update(['status' => Offboarding::STATUS_COMPLETED]);
            }

            $user = User::withoutGlobalScopes()->find($settlement->user_id);
            if ($user && $user->status === User::STATUS_ACTIVE) {
                $user->update(['status' => User::STATUS_INACTIVE]);
            }

            return $settlement->fresh();
        });
    }

    protected function applyLeaveEncashment(FnfSettlement $settlement): void
    {
        $breakdown = $settlement->breakdown_json['leave_encashment']['lines'] ?? [];
        $user = User::withoutGlobalScopes()->findOrFail($settlement->user_id);
        $year = $this->leaveLedger->leaveYear($settlement->last_working_date ? Carbon::parse($settlement->last_working_date) : null);

        foreach ($breakdown as $line) {
            $leaveTypeId = (int) ($line['leave_type_id'] ?? 0);
            $days = (float) ($line['days'] ?? 0);
            if ($leaveTypeId <= 0 || $days <= 0) {
                continue;
            }

            $leaveType = LeaveType::withoutGlobalScopes()->find($leaveTypeId);
            if (! $leaveType) {
                continue;
            }

            $this->leaveLedger->encashLeave($user, $leaveType, $days, $year, 'F&F settlement #' . $settlement->id);
        }
    }

    /**
     * @return array{days: float, lines: array<int, array{leave_type_id: int, name: string, days: float}>}
     */
    public function calculateLeaveEncashment(User $user): array
    {
        $year = $this->leaveLedger->leaveYear();
        $balances = LeaveBalance::withoutGlobalScopes()
            ->where('company_id', $user->company_id)
            ->where('user_id', $user->id)
            ->where('year', $year)
            ->with('leaveType')
            ->get();

        $totalDays = 0.0;
        $lines = [];

        foreach ($balances as $balance) {
            $type = $balance->leaveType;
            if (! $type || ! $type->is_paid) {
                continue;
            }

            $available = $balance->available();
            if ($available <= 0) {
                continue;
            }

            $lines[] = [
                'leave_type_id' => $type->id,
                'name' => $type->name,
                'days' => $available,
            ];
            $totalDays += $available;
        }

        return ['days' => round($totalDays, 2), 'lines' => $lines];
    }

    protected function resolveDailyRate(User $user): float
    {
        $revision = $user->latestEffectiveSalaryRevision();
        $annual = (float) ($revision?->ctc_annual ?? 0);

        if ($annual <= 0) {
            return 0.0;
        }

        return round($annual / 365, 2);
    }

    protected function estimateNoticeServedDays(User $user, $lastWorkingDate, int $noticePeriod): int
    {
        if (! $lastWorkingDate) {
            return 0;
        }

        $lwd = $lastWorkingDate instanceof Carbon ? $lastWorkingDate : Carbon::parse($lastWorkingDate);
        $start = $user->joining_date ? Carbon::parse($user->joining_date) : $lwd->copy()->subDays($noticePeriod);

        $resignationStart = $lwd->copy()->subDays($noticePeriod);
        if ($user->joining_date && $resignationStart->lt(Carbon::parse($user->joining_date))) {
            $resignationStart = Carbon::parse($user->joining_date);
        }

        return (int) min($noticePeriod, max(0, $resignationStart->diffInDays($lwd)));
    }
}
