<?php

namespace App\Services\Leave;

use App\Models\Leave;
use App\Models\LeaveBalance;
use App\Models\LeaveTransaction;
use App\Models\LeaveType;
use App\Models\LeaveTypeRule;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class LeaveLedgerService
{
    public const ACCRUAL_YEARLY = 'yearly';

    public const ACCRUAL_MONTHLY = 'monthly';

    public function leaveYear(?Carbon $date = null): int
    {
        return (int) ($date ?? now())->year;
    }

    public function annualEntitlement(LeaveType $leaveType): float
    {
        if ($leaveType->is_unlimited) {
            return PHP_FLOAT_MAX;
        }

        $count = $leaveType->leave_count ?? $leaveType->total_leaves ?? 0;

        return (float) $count;
    }

    public function isUnlimited(LeaveType $leaveType): bool
    {
        return (bool) $leaveType->is_unlimited;
    }

    public function availableLeaveDays(User $user, LeaveType $leaveType, ?int $year = null): float
    {
        if ($this->isUnlimited($leaveType)) {
            return PHP_FLOAT_MAX;
        }

        $year = $year ?? $this->leaveYear();
        $balance = $this->ensureBalance($user, $leaveType, $year);

        return $balance->available();
    }

    public function ensureBalance(User $user, LeaveType $leaveType, int $year): LeaveBalance
    {
        $balance = LeaveBalance::query()->firstOrCreate(
            [
                'company_id' => $user->company_id,
                'user_id' => $user->id,
                'leave_type_id' => $leaveType->id,
                'year' => $year,
            ],
            [
                'opening_balance' => 0,
                'allocated' => 0,
                'accrued' => 0,
                'carried_forward' => 0,
                'used' => 0,
                'pending' => 0,
                'encashed' => 0,
            ]
        );

        if ($this->isUnlimited($leaveType)) {
            return $balance;
        }

        $this->grantAnnualAllocationIfNeeded($user, $leaveType, $balance, $year);

        return $balance->fresh() ?? $balance;
    }

    public function holdPending(Leave $leave): void
    {
        $leave->loadMissing(['user', 'leaveType']);
        $user = $leave->user;
        $leaveType = $leave->leaveType;

        if (! $user || ! $leaveType || $this->isUnlimited($leaveType)) {
            return;
        }

        $year = $this->leaveYear(Carbon::parse($leave->start_date));
        $amount = (float) $leave->total_days;

        DB::transaction(function () use ($leave, $user, $leaveType, $year, $amount) {
            $balance = $this->ensureBalance($user, $leaveType, $year);
            $before = $balance->available();

            if ($amount > $before) {
                throw new InvalidArgumentException("Insufficient leave balance. Available: {$before} day(s).");
            }

            $balance->pending = round((float) $balance->pending + $amount, 2);
            $balance->save();

            $this->recordTransaction(
                $balance,
                LeaveTransaction::TYPE_PENDING_HOLD,
                $amount,
                $before,
                $balance->available(),
                $year,
                Carbon::parse($leave->start_date),
                'Pending leave request #' . $leave->id,
                $leave->id,
                'pending_hold:leave:' . $leave->id
            );
        });
    }

    public function releasePending(Leave $leave): void
    {
        $leave->loadMissing(['user', 'leaveType']);
        $user = $leave->user;
        $leaveType = $leave->leaveType;

        if (! $user || ! $leaveType || $this->isUnlimited($leaveType)) {
            return;
        }

        $year = $this->leaveYear(Carbon::parse($leave->start_date));
        $amount = (float) $leave->total_days;

        DB::transaction(function () use ($leave, $user, $leaveType, $year, $amount) {
            $balance = LeaveBalance::query()
                ->where('user_id', $user->id)
                ->where('leave_type_id', $leaveType->id)
                ->where('year', $year)
                ->lockForUpdate()
                ->first();

            if (! $balance) {
                return;
            }

            $before = $balance->available();
            $balance->pending = round(max(0, (float) $balance->pending - $amount), 2);
            $balance->save();

            $this->recordTransaction(
                $balance,
                LeaveTransaction::TYPE_PENDING_RELEASE,
                $amount,
                $before,
                $balance->available(),
                $year,
                now(),
                'Released pending hold for leave #' . $leave->id,
                $leave->id,
                'pending_release:leave:' . $leave->id
            );
        });
    }

    public function consumePending(Leave $leave): void
    {
        $leave->loadMissing(['user', 'leaveType']);
        $user = $leave->user;
        $leaveType = $leave->leaveType;

        if (! $user || ! $leaveType || $this->isUnlimited($leaveType)) {
            return;
        }

        $year = $this->leaveYear(Carbon::parse($leave->start_date));
        $amount = (float) $leave->total_days;

        DB::transaction(function () use ($leave, $user, $leaveType, $year, $amount) {
            $balance = LeaveBalance::query()
                ->where('user_id', $user->id)
                ->where('leave_type_id', $leaveType->id)
                ->where('year', $year)
                ->lockForUpdate()
                ->first();

            if (! $balance) {
                $balance = $this->ensureBalance($user, $leaveType, $year);
            }

            $before = $balance->available();
            $balance->pending = round(max(0, (float) $balance->pending - $amount), 2);
            $balance->used = round((float) $balance->used + $amount, 2);
            $balance->save();

            $this->recordTransaction(
                $balance,
                LeaveTransaction::TYPE_DEBIT,
                $amount,
                $before + $amount,
                $balance->available(),
                $year,
                Carbon::parse($leave->start_date),
                'Approved leave #' . $leave->id,
                $leave->id,
                'debit:leave:' . $leave->id
            );
        });
    }

    public function reverseApproved(Leave $leave): void
    {
        $leave->loadMissing(['user', 'leaveType']);
        $user = $leave->user;
        $leaveType = $leave->leaveType;

        if (! $user || ! $leaveType || $this->isUnlimited($leaveType)) {
            return;
        }

        $year = $this->leaveYear(Carbon::parse($leave->start_date));
        $amount = (float) $leave->total_days;

        DB::transaction(function () use ($leave, $user, $leaveType, $year, $amount) {
            $balance = LeaveBalance::query()
                ->where('user_id', $user->id)
                ->where('leave_type_id', $leaveType->id)
                ->where('year', $year)
                ->lockForUpdate()
                ->first();

            if (! $balance) {
                return;
            }

            $before = $balance->available();
            $balance->used = round(max(0, (float) $balance->used - $amount), 2);
            $balance->save();

            $this->recordTransaction(
                $balance,
                LeaveTransaction::TYPE_CREDIT,
                $amount,
                $before,
                $balance->available(),
                $year,
                now(),
                'Reversed approved leave #' . $leave->id,
                $leave->id,
                'credit:reverse:leave:' . $leave->id
            );
        });
    }

    public function encashLeave(
        User $user,
        LeaveType $leaveType,
        float $days,
        int $year,
        ?string $remarks = null,
        ?string $referenceKey = null
    ): LeaveBalance {
        if ($this->isUnlimited($leaveType)) {
            throw new InvalidArgumentException('Unlimited leave types cannot be encashed.');
        }

        if ($days <= 0) {
            throw new InvalidArgumentException('Encashment days must be positive.');
        }

        return DB::transaction(function () use ($user, $leaveType, $days, $year, $remarks, $referenceKey) {
            $balance = $this->ensureBalance($user, $leaveType, $year);
            $available = $balance->available();

            if ($days > $available) {
                throw new InvalidArgumentException('Encashment exceeds available leave balance.');
            }

            $before = $available;
            $balance->encashed = round((float) $balance->encashed + $days, 2);
            $balance->save();

            $this->recordTransaction(
                $balance,
                LeaveTransaction::TYPE_ENCASHMENT,
                $days,
                $before,
                $balance->available(),
                $year,
                now(),
                $remarks ?? 'Leave encashment',
                null,
                $referenceKey ?? 'encash:' . $user->id . ':' . $leaveType->id . ':' . $year
            );

            return $balance->fresh() ?? $balance;
        });
    }

    public function creditOpeningBalance(
        User $user,
        LeaveType $leaveType,
        int $year,
        float $amount,
        ?int $actorId = null,
        ?string $referenceKey = null,
        bool $replace = false
    ): LeaveBalance {
        if ($amount < 0) {
            throw new InvalidArgumentException('Opening balance cannot be negative.');
        }

        return DB::transaction(function () use ($user, $leaveType, $year, $amount, $actorId, $referenceKey, $replace) {
            $balance = $this->ensureBalance($user, $leaveType, $year);
            $before = $balance->available();
            $previousOpening = (float) $balance->opening_balance;
            $creditAmount = $replace ? ($amount - $previousOpening) : $amount;

            if ($creditAmount == 0.0) {
                return $balance;
            }

            $balance->opening_balance = $replace ? $amount : round($previousOpening + $amount, 2);
            $balance->save();

            $this->recordTransaction(
                $balance,
                LeaveTransaction::TYPE_OPENING,
                abs($creditAmount),
                $before,
                $balance->available(),
                $year,
                now(),
                $replace ? 'Opening balance set via import' : 'Opening balance import',
                null,
                $referenceKey ?? 'opening:' . $year . ':user:' . $user->id . ':type:' . $leaveType->id . ':' . uniqid(),
                $actorId
            );

            return $balance->fresh();
        });
    }

    public function runMonthlyAccrual(?int $companyId = null, ?Carbon $asOf = null): int
    {
        $asOf = ($asOf ?? now())->copy()->startOfMonth();
        $year = $this->leaveYear($asOf);
        $monthKey = $asOf->format('Y-m');
        $processed = 0;

        $typesQuery = LeaveType::query()
            ->where('is_unlimited', false)
            ->whereHas('rule', fn ($q) => $q->where('accrual_mode', self::ACCRUAL_MONTHLY));

        if ($companyId) {
            $typesQuery->where('company_id', $companyId);
        }

        foreach ($typesQuery->with('rule')->get() as $leaveType) {
            $users = app(LeaveEligibilityService::class)->eligibleUsersQuery($leaveType);
            if ($companyId) {
                $users->where('company_id', $companyId);
            }

            foreach ($users->get() as $user) {
                if ($this->accrueMonthlyForUser($user, $leaveType, $asOf, $year, $monthKey)) {
                    $processed++;
                }
            }
        }

        return $processed;
    }

    public function runCarryForward(?int $companyId = null, ?int $fromYear = null): int
    {
        $fromYear = $fromYear ?? ($this->leaveYear() - 1);
        $toYear = $fromYear + 1;
        $processed = 0;

        $balancesQuery = LeaveBalance::query()->where('year', $fromYear);
        if ($companyId) {
            $balancesQuery->where('company_id', $companyId);
        }

        foreach ($balancesQuery->with(['user', 'leaveType.rule'])->get() as $balance) {
            $user = $balance->user;
            $leaveType = $balance->leaveType;
            $rule = $leaveType?->rule;

            if (! $user || ! $leaveType || $leaveType->is_unlimited || ! $rule?->carry_forward_enabled) {
                continue;
            }

            $unused = max(0, $balance->available());
            $limit = $rule->carry_forward_limit;
            $carry = $limit !== null ? min($unused, (float) $limit) : $unused;

            if ($carry <= 0) {
                continue;
            }

            $referenceKey = "carry_forward:{$fromYear}:to:{$toYear}:user:{$user->id}:type:{$leaveType->id}";
            if (LeaveTransaction::query()->where('reference_key', $referenceKey)->exists()) {
                continue;
            }

            DB::transaction(function () use ($user, $leaveType, $toYear, $carry, $referenceKey, $fromYear) {
                $newBalance = $this->ensureBalance($user, $leaveType, $toYear);
                $before = $newBalance->available();
                $newBalance->carried_forward = round((float) $newBalance->carried_forward + $carry, 2);
                $newBalance->save();

                $this->recordTransaction(
                    $newBalance,
                    LeaveTransaction::TYPE_CARRY_FORWARD,
                    $carry,
                    $before,
                    $newBalance->available(),
                    $toYear,
                    Carbon::create($toYear, 1, 1),
                    "Carry forward from {$fromYear}",
                    null,
                    $referenceKey
                );
            });

            $processed++;
        }

        return $processed;
    }

    public function syncBalancesForCompany(int $companyId, ?int $year = null): int
    {
        $year = $year ?? $this->leaveYear();
        $count = 0;

        $leaveTypes = LeaveType::query()
            ->where('company_id', $companyId)
            ->where('is_unlimited', false)
            ->get();

        $users = User::query()
            ->forTenantDirectory()
            ->where('company_id', $companyId)
            ->get();

        foreach ($leaveTypes as $leaveType) {
            $eligibleIds = app(LeaveEligibilityService::class)
                ->eligibleUsersQuery($leaveType)
                ->pluck('id')
                ->all();

            foreach ($users as $user) {
                if (! in_array($user->id, $eligibleIds, true)) {
                    continue;
                }

                DB::transaction(function () use ($user, $leaveType, $year, &$count) {
                    $balance = $this->ensureBalance($user, $leaveType, $year);
                    $this->grantAnnualAllocationIfNeeded($user, $leaveType, $balance, $year);
                    $this->syncUsageFromLeaves($user, $leaveType, $year);
                    $count++;
                });
            }
        }

        return $count;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function balancesForUser(User $user, ?int $year = null): array
    {
        $year = $year ?? $this->leaveYear();

        $types = LeaveType::query()
            ->when($user->company_id, fn ($q) => $q->where('company_id', $user->company_id))
            ->orderBy('name')
            ->get();

        $rows = [];
        foreach ($types as $leaveType) {
            if ($this->isUnlimited($leaveType)) {
                $rows[] = [
                    'leave_type_id' => $leaveType->id,
                    'name' => $leaveType->name,
                    'short_code' => $leaveType->short_code,
                    'is_unlimited' => true,
                    'available' => null,
                    'used' => null,
                    'pending' => null,
                    'entitlement' => null,
                ];

                continue;
            }

            $balance = $this->ensureBalance($user, $leaveType, $year);
            $entitlement = $this->annualEntitlement($leaveType);

            $rows[] = [
                'leave_type_id' => $leaveType->id,
                'name' => $leaveType->name,
                'short_code' => $leaveType->short_code,
                'is_unlimited' => false,
                'available' => $balance->available(),
                'used' => (float) $balance->used,
                'pending' => (float) $balance->pending,
                'opening' => (float) $balance->opening_balance,
                'allocated' => (float) $balance->allocated,
                'accrued' => (float) $balance->accrued,
                'carried_forward' => (float) $balance->carried_forward,
                'entitlement' => $entitlement,
            ];
        }

        return $rows;
    }

    protected function grantAnnualAllocationIfNeeded(User $user, LeaveType $leaveType, LeaveBalance $balance, int $year): void
    {
        if ($this->isUnlimited($leaveType)) {
            return;
        }

        $rule = $leaveType->rule;
        $accrualMode = $rule?->accrual_mode ?? self::ACCRUAL_YEARLY;

        if ($accrualMode === self::ACCRUAL_MONTHLY) {
            return;
        }

        if ((float) $balance->allocated > 0) {
            return;
        }

        $entitlement = $this->annualEntitlement($leaveType);
        if ($entitlement <= 0) {
            return;
        }

        $referenceKey = "annual_allocation:{$year}:user:{$user->id}:type:{$leaveType->id}";
        if (LeaveTransaction::query()->where('reference_key', $referenceKey)->exists()) {
            return;
        }

        $before = $balance->available();
        $balance->allocated = $entitlement;
        $balance->save();

        $this->recordTransaction(
            $balance,
            LeaveTransaction::TYPE_ANNUAL_ALLOCATION,
            $entitlement,
            $before,
            $balance->available(),
            $year,
            Carbon::create($year, 1, 1),
            'Annual leave allocation',
            null,
            $referenceKey
        );
    }

    protected function accrueMonthlyForUser(User $user, LeaveType $leaveType, Carbon $asOf, int $year, string $monthKey): bool
    {
        $rule = $leaveType->rule;
        if (! $rule || $rule->accrual_mode !== self::ACCRUAL_MONTHLY) {
            return false;
        }

        $referenceKey = "accrual:{$monthKey}:user:{$user->id}:type:{$leaveType->id}";
        if (LeaveTransaction::query()->where('reference_key', $referenceKey)->exists()) {
            return false;
        }

        $monthlyAmount = round($this->annualEntitlement($leaveType) / 12, 2);
        if ($monthlyAmount <= 0) {
            return false;
        }

        if ($rule->pro_rata_on_joining && $user->joining_date) {
            $join = Carbon::parse($user->joining_date)->startOfMonth();
            if ($asOf->lt($join)) {
                return false;
            }

            if ($join->isSameMonth($asOf) && $join->year === $asOf->year) {
                $daysInMonth = $join->daysInMonth;
                $daysRemaining = $daysInMonth - $join->day + 1;
                $monthlyAmount = round($monthlyAmount * ($daysRemaining / $daysInMonth), 2);
            }
        }

        $balance = $this->ensureBalance($user, $leaveType, $year);
        $before = $balance->available();
        $balance->accrued = round((float) $balance->accrued + $monthlyAmount, 2);
        $balance->save();

        $this->recordTransaction(
            $balance,
            LeaveTransaction::TYPE_ACCRUAL,
            $monthlyAmount,
            $before,
            $balance->available(),
            $year,
            $asOf->copy()->endOfMonth(),
            "Monthly accrual for {$monthKey}",
            null,
            $referenceKey
        );

        return true;
    }

    protected function syncUsageFromLeaves(User $user, LeaveType $leaveType, int $year): void
    {
        $balance = LeaveBalance::query()
            ->where('user_id', $user->id)
            ->where('leave_type_id', $leaveType->id)
            ->where('year', $year)
            ->first();

        if (! $balance) {
            return;
        }

        $used = (float) Leave::query()
            ->where('user_id', $user->id)
            ->where('leave_type_id', $leaveType->id)
            ->where('status', 'approved')
            ->whereYear('start_date', $year)
            ->sum('total_days');

        $pending = (float) Leave::query()
            ->where('user_id', $user->id)
            ->where('leave_type_id', $leaveType->id)
            ->where('status', 'pending')
            ->whereYear('start_date', $year)
            ->sum('total_days');

        $balance->used = $used;
        $balance->pending = $pending;
        $balance->save();
    }

    protected function recordTransaction(
        LeaveBalance $balance,
        string $type,
        float $amount,
        float $balanceBefore,
        float $balanceAfter,
        int $year,
        Carbon $effectiveDate,
        ?string $remarks = null,
        ?int $leaveId = null,
        ?string $referenceKey = null,
        ?int $createdBy = null
    ): LeaveTransaction {
        return LeaveTransaction::query()->create([
            'company_id' => $balance->company_id,
            'user_id' => $balance->user_id,
            'leave_type_id' => $balance->leave_type_id,
            'leave_balance_id' => $balance->id,
            'leave_id' => $leaveId,
            'transaction_type' => $type,
            'amount' => round($amount, 2),
            'balance_before' => round($balanceBefore, 2),
            'balance_after' => round($balanceAfter, 2),
            'year' => $year,
            'effective_date' => $effectiveDate->toDateString(),
            'remarks' => $remarks,
            'created_by' => $createdBy,
            'reference_key' => $referenceKey,
        ]);
    }
}
