<?php

namespace App\Services\Payroll;

use App\Models\Attendance;
use App\Models\Holiday;
use App\Models\Leave;
use App\Models\User;
use App\Models\WeekoffRule;
use App\Services\Attendance\WeekoffResolver;
use Carbon\Carbon;

class LeaveLopCalculator
{
    public function __construct(
        protected ?WeekoffResolver $weekoffResolver = null
    ) {}

    /**
     * Working days in period for an employee: scheduled working days per weekoff rules, minus holidays.
     *
     * @param array<string> $holidayDates list of 'Y-m-d' dates
     */
    public function workingDaysInPeriod(
        Carbon $start,
        Carbon $end,
        array $holidayDates,
        ?int $companyId = null,
        ?int $userId = null
    ): int {
        if ($companyId !== null && $userId !== null) {
            return $this->workingDaysForUser($companyId, $userId, $start, $end, $holidayDates);
        }

        return $this->legacyWeekdayWorkingDays($start, $end, $holidayDates);
    }

    /**
     * @param array<string> $holidayDates
     */
    public function workingDaysForUser(int $companyId, int $userId, Carbon $start, Carbon $end, array $holidayDates): int
    {
        $weekoffMap = $this->weekoffResolver()->dayTypeMap($companyId, $userId, $start, $end);
        $days = 0;
        $cursor = $start->copy();
        while ($cursor->lte($end)) {
            $date = $cursor->format('Y-m-d');
            $isWeekoff = ($weekoffMap[$date] ?? null) === WeekoffRule::DAY_WEEKLY_OFF;
            if (! $isWeekoff && ! in_array($date, $holidayDates, true)) {
                $days++;
            }
            $cursor->addDay();
        }

        return $days;
    }

    /**
     * Fallback when no employee context: Mon–Fri minus holidays.
     *
     * @param array<string> $holidayDates
     */
    protected function legacyWeekdayWorkingDays(Carbon $start, Carbon $end, array $holidayDates): int
    {
        $days = 0;
        $cursor = $start->copy();
        while ($cursor->lte($end)) {
            if ($cursor->isWeekday() && ! in_array($cursor->format('Y-m-d'), $holidayDates, true)) {
                $days++;
            }
            $cursor->addDay();
        }

        return $days;
    }

    protected function weekoffResolver(): WeekoffResolver
    {
        return $this->weekoffResolver ??= app(WeekoffResolver::class);
    }

    /**
     * Resolve payable window, per-employee weekoff-based working days, and join/exit proration.
     *
     * @param  array<string>  $holidayDates
     * @return array{
     *     eligible: bool,
     *     eligible_start: Carbon,
     *     eligible_end: Carbon,
     *     full_month_working_days: int,
     *     payable_working_days: int,
     *     proration_factor: float
     * }
     */
    public function buildPayrollContext(
        User $user,
        int $companyId,
        Carbon $periodStart,
        Carbon $periodEnd,
        array $holidayDates
    ): array {
        $eligibleStart = $periodStart->copy()->startOfDay();
        $eligibleEnd = $periodEnd->copy()->startOfDay();

        if ($user->joining_date) {
            $joining = Carbon::parse($user->joining_date)->startOfDay();
            if ($joining->gt($periodEnd)) {
                return $this->emptyPayrollContext($periodStart, $periodEnd);
            }
            if ($joining->gt($eligibleStart)) {
                $eligibleStart = $joining;
            }
        }

        if ($user->last_working_date) {
            $lastWorking = Carbon::parse($user->last_working_date)->startOfDay();
            if ($lastWorking->lt($periodStart)) {
                return $this->emptyPayrollContext($periodStart, $periodEnd);
            }
            if ($lastWorking->lt($eligibleEnd)) {
                $eligibleEnd = $lastWorking;
            }
        }

        if ($eligibleStart->gt($eligibleEnd)) {
            return $this->emptyPayrollContext($periodStart, $periodEnd);
        }

        $fullMonthWorkingDays = $this->workingDaysForUser($companyId, $user->id, $periodStart, $periodEnd, $holidayDates);
        $payableWorkingDays = $this->workingDaysForUser($companyId, $user->id, $eligibleStart, $eligibleEnd, $holidayDates);
        $prorationFactor = $fullMonthWorkingDays > 0
            ? round($payableWorkingDays / $fullMonthWorkingDays, 6)
            : 0.0;

        return [
            'eligible' => $payableWorkingDays > 0,
            'eligible_start' => $eligibleStart,
            'eligible_end' => $eligibleEnd,
            'full_month_working_days' => $fullMonthWorkingDays,
            'payable_working_days' => $payableWorkingDays,
            'proration_factor' => min(1.0, max(0.0, $prorationFactor)),
        ];
    }

    public function isEligibleForPayrollPeriod(User $user, Carbon $periodStart, Carbon $periodEnd): bool
    {
        $eligibleStart = $periodStart->copy()->startOfDay();
        $eligibleEnd = $periodEnd->copy()->startOfDay();

        if ($user->joining_date) {
            $joining = Carbon::parse($user->joining_date)->startOfDay();
            if ($joining->gt($periodEnd)) {
                return false;
            }
            if ($joining->gt($eligibleStart)) {
                $eligibleStart = $joining;
            }
        }

        if ($user->last_working_date) {
            $lastWorking = Carbon::parse($user->last_working_date)->startOfDay();
            if ($lastWorking->lt($periodStart)) {
                return false;
            }
            if ($lastWorking->lt($eligibleEnd)) {
                $eligibleEnd = $lastWorking;
            }
        }

        return $eligibleStart->lte($eligibleEnd);
    }

    /**
     * @return array{
     *     eligible: bool,
     *     eligible_start: Carbon,
     *     eligible_end: Carbon,
     *     full_month_working_days: int,
     *     payable_working_days: int,
     *     proration_factor: float
     * }
     */
    protected function emptyPayrollContext(Carbon $periodStart, Carbon $periodEnd): array
    {
        return [
            'eligible' => false,
            'eligible_start' => $periodStart->copy()->startOfDay(),
            'eligible_end' => $periodEnd->copy()->startOfDay(),
            'full_month_working_days' => 0,
            'payable_working_days' => 0,
            'proration_factor' => 0.0,
        ];
    }

    /**
     * Fetch approved leaves for user in period and return paid/unpaid working-day counts inside the range.
     *
     * @return array{paid: float, unpaid: float, half_days: int}
     */
    public function leaveDaysForUser(int $userId, int $companyId, Carbon $start, Carbon $end): array
    {
        $holidayDates = $this->getHolidayDates($companyId, $start, $end);
        $weekoffMap = $this->weekoffResolver()->dayTypeMap($companyId, $userId, $start, $end);

        $leaves = Leave::withoutGlobalScopes()
            ->with('leaveType')
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->whereIn('status', Leave::approvedLikeStatuses())
            ->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;

        foreach ($leaves as $leave) {
            $isPaid = $this->leaveIsPaid($leave);
            $leaveStart = Carbon::parse($leave->start_date)->startOfDay();
            $leaveEnd = Carbon::parse($leave->end_date)->startOfDay();
            $overlapStart = $leaveStart->gt($start) ? $leaveStart : $start->copy()->startOfDay();
            $overlapEnd = $leaveEnd->lt($end) ? $leaveEnd : $end->copy()->startOfDay();

            if ($overlapStart->gt($overlapEnd)) {
                continue;
            }

            if ($leave->is_half_day && $leaveStart->isSameDay($leaveEnd)) {
                $days = $this->isWorkingDay($overlapStart, $holidayDates, $weekoffMap) ? 0.5 : 0.0;
            } else {
                $days = $this->countWorkingDaysInRange($overlapStart, $overlapEnd, $holidayDates, $weekoffMap);
            }

            if ($isPaid) {
                $paid += $days;
            } else {
                $unpaid += $days;
            }
        }

        return [
            'paid' => $paid,
            'unpaid' => $unpaid,
            'half_days' => 0,
        ];
    }

    protected function leaveIsPaid(Leave $leave): bool
    {
        return (bool) ($leave->is_paid || ($leave->leaveType?->is_paid ?? false));
    }

    /**
     * @param  array<string>  $holidayDates
     * @param  array<string, string|null>  $weekoffMap
     */
    protected function countWorkingDaysInRange(
        Carbon $start,
        Carbon $end,
        array $holidayDates,
        array $weekoffMap
    ): float {
        $days = 0.0;
        $cursor = $start->copy()->startOfDay();
        $end = $end->copy()->startOfDay();
        while ($cursor->lte($end)) {
            if ($this->isWorkingDay($cursor, $holidayDates, $weekoffMap)) {
                $days++;
            }
            $cursor->addDay();
        }

        return $days;
    }

    /**
     * @param  array<string>  $holidayDates
     * @param  array<string, string|null>  $weekoffMap
     */
    protected function isWorkingDay(Carbon $date, array $holidayDates, array $weekoffMap): bool
    {
        $dateKey = $date->format('Y-m-d');
        $isWeekoff = ($weekoffMap[$dateKey] ?? null) === WeekoffRule::DAY_WEEKLY_OFF;

        return ! $isWeekoff && ! in_array($dateKey, $holidayDates, true);
    }

    /** Present days (full + half counted as 0.5) from attendance in period. */
    public 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);
    }

    /**
     * LOP amount = (gross_monthly / working_days) * lop_days when working_days > 0.
     */
    public function lopAmount(float $grossMonthly, int $workingDays, float $lopDays): float
    {
        if ($workingDays <= 0) {
            return 0.0;
        }
        return round(($grossMonthly / $workingDays) * $lopDays, 2);
    }

    /**
     * Get holiday dates as Y-m-d for a company in range.
     *
     * @return array<string>
     */
    public function getHolidayDates(int $companyId, Carbon $start, Carbon $end): array
    {
        return Holiday::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereBetween('date', [$start->toDateString(), $end->toDateString()])
            ->get()
            ->map(fn ($h) => $h->date instanceof \DateTimeInterface ? $h->date->format('Y-m-d') : $h->date)
            ->all();
    }
}
