<?php

namespace App\Services\Finance;

use App\Enums\EmployeeAdvanceStatus;
use App\Enums\PayrollAdjustmentType;
use App\Enums\PayrollPeriodStatus;
use App\Enums\PayrollStatus;
use App\Enums\Role;
use App\Models\Employee;
use App\Models\EmployeeAdvance;
use App\Models\Payroll;
use App\Models\PayrollAdjustment;
use App\Models\PayrollPeriod;
use App\Models\User;
use App\Services\System\PushNotificationService;
use Carbon\Carbon;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;

class PayrollService
{
    public function __construct(
        private readonly CashService $cashService,
        private readonly PushNotificationService $pushNotificationService,
    ) {}

    public function listPeriods(): Collection
    {
        return PayrollPeriod::query()
            ->orderByDesc('year')
            ->orderByDesc('month')
            ->get();
    }

    public function resolvePeriod(?int $periodId): ?PayrollPeriod
    {
        if ($periodId !== null) {
            return PayrollPeriod::query()->find($periodId);
        }

        return PayrollPeriod::query()
            ->where('status', PayrollPeriodStatus::OPEN)
            ->orderByDesc('year')
            ->orderByDesc('month')
            ->first()
            ?? PayrollPeriod::query()
                ->orderByDesc('year')
                ->orderByDesc('month')
                ->first();
    }

    public function periodSummary(PayrollPeriod $period, User $user): array
    {
        $query = Payroll::query()
            ->where('payroll_period_id', $period->id);

        if (! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
            $employeeId = $user->employee?->id ?? -1;
            $query->where('employee_id', $employeeId);
        }

        $totalAmount = (int) $query->sum('total_amount');

        $totalCount = $query->count();

        return [
            'total_amount' => $totalAmount,
            'total_amount_formatted' => 'Rp '.number_format($totalAmount, 0, ',', '.'),
            'total_count' => $totalCount,
            'status' => $period->status->value,
            'status_label' => $period->status->label(),
        ];
    }

    public function paginateForPeriod(PayrollPeriod $period, array $tableQuery, User $user): LengthAwarePaginator
    {
        $query = Payroll::query()
            ->with(['employee.user.profile', 'payrollPeriod', 'adjustments.createdBy.profile'])
            ->where('payroll_period_id', $period->id)
            ->when(! $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]), function (Builder $query) use ($user): void {
                $employeeId = $user->employee?->id ?? -1;
                $query->where('employee_id', $employeeId);
            })
            ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
                $search = $tableQuery['search'];
                $query->where(function (Builder $query) use ($search): void {
                    $query->whereHas('employee.user.profile', fn (Builder $query) => $query->where('full_name', 'like', "%{$search}%"))
                        ->orWhereHas('employee.user', fn (Builder $query) => $query->where('username', 'like', "%{$search}%"));
                });
            });

        $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);

        return $query
            ->paginate(25)
            ->withQueryString();
    }

    public function openCurrentPeriod(?User $closedBy = null): PayrollPeriod
    {
        $user = $closedBy
            ?? auth()->user()
            ?? User::query()->whereHas('roles', fn (Builder $roleQuery) => $roleQuery->whereIn('name', [Role::DEVELOPER->value, Role::OWNER->value]))->first()
            ?? User::query()->first();

        try {
            $period = DB::transaction(function () use ($user): PayrollPeriod {
                $now = now();
                $year = $now->year;
                $month = $now->month;

                $openPeriods = PayrollPeriod::query()
                    ->where('status', PayrollPeriodStatus::OPEN)
                    ->get();

                foreach ($openPeriods as $oldPeriod) {
                    $unpaidPayrolls = Payroll::query()
                        ->where('payroll_period_id', $oldPeriod->id)
                        ->where('status', PayrollStatus::UNPAID)
                        ->get();

                    foreach ($unpaidPayrolls as $payroll) {
                        if ($user) {
                            $this->pay($payroll, $user);
                        }
                    }

                    $oldPeriod->status = PayrollPeriodStatus::CLOSED;
                    $oldPeriod->closed_at = now();
                    $oldPeriod->closed_by_id = $user?->id;
                    $oldPeriod->save();
                }

                $period = PayrollPeriod::query()
                    ->where('year', $year)
                    ->where('month', $month)
                    ->first();

                if ($period === null) {
                    $period = PayrollPeriod::create([
                        'year' => $year,
                        'month' => $month,
                        'status' => PayrollPeriodStatus::OPEN,
                    ]);
                } elseif ($period->status === PayrollPeriodStatus::CLOSED) {
                    $period->status = PayrollPeriodStatus::OPEN;
                    $period->closed_at = null;
                    $period->closed_by_id = null;
                    $period->save();
                }

                $this->generatePayrollsForPeriod($period);

                return $period->fresh();
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal membuka periode payroll: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }

        return $period;
    }

    public function generatePayrollsForPeriod(PayrollPeriod $period): void
    {
        $employees = $this->payrollEligibleEmployees($period);

        foreach ($employees as $employee) {
            $exists = Payroll::query()
                ->where('payroll_period_id', $period->id)
                ->where('employee_id', $employee->id)
                ->exists();

            if ($exists) {
                continue;
            }

            $payroll = new Payroll([
                'payroll_period_id' => $period->id,
                'employee_id' => $employee->id,
                'base_salary' => $employee->base_salary ?? 0,
                'bonus_amount' => 0,
                'deduction_amount' => 0,
                'total_amount' => 0,
                'status' => PayrollStatus::UNPAID,
            ]);

            $payroll->save();
            $payroll->recalculateAmounts();
            $payroll->save();
        }
    }

    public function addAdjustment(Payroll $payroll, array $validated, User $user): void
    {

        try {
            DB::transaction(function () use ($payroll, $validated, $user): void {
                $payroll->adjustments()->create([
                    'type' => PayrollAdjustmentType::from($validated['type']),
                    'amount' => (int) $validated['amount'],
                    'description' => $validated['description'],
                    'created_by_id' => $user->id,
                ]);

                $payroll->load('adjustments');
                $payroll->recalculateAmounts();
                $payroll->save();
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal menambahkan penyesuaian gaji: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }

        if ($payroll->employee?->user_id) {
            $typeLabel = PayrollAdjustmentType::from($validated['type'])->label();
            $formattedAmount = 'Rp '.number_format($validated['amount'], 0, ',', '.');
            $this->pushNotificationService->sendToUser(
                "📊 Penyesuaian Gaji: {$typeLabel}",
                "Gaji periode {$payroll->payrollPeriod->period_label} disesuaikan: {$typeLabel} sebesar {$formattedAmount} ({$validated['description']}).",
                $payroll->employee->user_id,
                route('admin.finance.payroll.index'),
            );
        }
    }

    public function updateAdjustment(PayrollAdjustment $adjustment, array $validated, User $user): void
    {
        $payroll = $adjustment->payroll;
        $payroll->loadMissing(['payrollPeriod', 'employee.user']);

        try {
            DB::transaction(function () use ($payroll, $adjustment, $validated): void {
                $adjustment->type = PayrollAdjustmentType::from($validated['type']);
                $adjustment->amount = (int) $validated['amount'];
                $adjustment->description = $validated['description'];
                $adjustment->save();

                $payroll->load('adjustments');
                $payroll->recalculateAmounts();
                $payroll->save();
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal memperbarui penyesuaian gaji: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }

        if ($payroll->employee?->user_id) {
            $typeLabel = PayrollAdjustmentType::from($validated['type'])->label();
            $formattedAmount = 'Rp '.number_format($validated['amount'], 0, ',', '.');
            $this->pushNotificationService->sendToUser(
                "📊 Penyesuaian Gaji Diperbarui: {$typeLabel}",
                "Penyesuaian gaji Anda untuk periode {$payroll->payrollPeriod->period_label} diperbarui: {$typeLabel} menjadi {$formattedAmount} ({$validated['description']}).",
                $payroll->employee->user_id,
                route('admin.finance.payroll.index'),
            );
        }
    }

    public function deleteAdjustment(PayrollAdjustment $adjustment): void
    {
        $payroll = $adjustment->payroll;
        $payroll->loadMissing(['payrollPeriod', 'employee.user']);

        try {
            DB::transaction(function () use ($payroll, $adjustment): void {
                $adjustment->delete();

                $payroll->load('adjustments');
                $payroll->recalculateAmounts();
                $payroll->save();
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal menghapus penyesuaian gaji: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }

        if ($payroll->employee?->user_id) {
            $this->pushNotificationService->sendToUser(
                '📊 Penyesuaian Gaji Dihapus',
                "Penyesuaian gaji Anda untuk periode {$payroll->payrollPeriod->period_label} telah dihapus.",
                $payroll->employee->user_id,
                route('admin.finance.payroll.index'),
            );
        }
    }

    public function pay(Payroll $payroll, User $user): void
    {
        $payroll->loadMissing(['payrollPeriod', 'employee.user.profile']);

        if ($payroll->total_amount <= 0) {
            try {
                DB::transaction(function () use ($payroll, $user): void {
                    $payroll->status = PayrollStatus::PAID;
                    $payroll->paid_at = now();
                    $payroll->paid_by_id = $user->id;
                    $payroll->save();

                    $this->settleKasbonFromPayroll($payroll, $user);
                });
            } catch (ValidationException $e) {
                throw $e;
            } catch (\Throwable $e) {
                Log::error('Gagal membayar gaji (total 0): '.$e->getMessage(), [
                    'trace' => $e->getTraceAsString(),
                ]);

                throw ValidationException::withMessages([
                    'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
                ]);
            }

            return;
        }

        try {
            DB::transaction(function () use ($payroll, $user): void {
                $description = sprintf(
                    'Pembayaran gaji: %s (%s)',
                    $payroll->employeeName,
                    $payroll->payrollPeriod->period_label,
                );

                $cashTransaction = $this->cashService->recordOutgoing(
                    $payroll,
                    $payroll->total_amount,
                    $description,
                    $user,
                );

                $payroll->cash_transaction_id = $cashTransaction->id;
                $payroll->status = PayrollStatus::PAID;
                $payroll->paid_at = now();
                $payroll->paid_by_id = $user->id;
                $payroll->save();

                $this->settleKasbonFromPayroll($payroll, $user);
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal membayar gaji: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }

        if ($payroll->employee?->user_id) {
            $this->pushNotificationService->sendToUser(
                '💸 Gaji Dibayarkan',
                "Gaji Anda untuk periode {$payroll->payrollPeriod->period_label} senilai {$payroll->total_amount_formatted} telah dibayarkan.",
                $payroll->employee->user_id,
                route('admin.finance.payroll.index'),
            );
        }
    }

    private function payrollEligibleEmployees(PayrollPeriod $period): Collection
    {
        $periodStart = Carbon::create($period->year, $period->month, 1)->startOfMonth();

        return Employee::query()
            ->with('user.roles')
            ->whereHas('user', function (Builder $query): void {
                $query->active()
                    ->whereDoesntHave('roles', fn (Builder $query) => $query->whereIn('name', [
                        Role::DEVELOPER->value,
                        Role::OWNER->value,
                    ]));
            })
            ->where(function (Builder $query) use ($periodStart): void {
                $query->whereNull('resign_date')
                    ->orWhere('resign_date', '>=', $periodStart);
            })
            ->get();
    }

    private function settleKasbonFromPayroll(Payroll $payroll, User $user): void
    {
        $remaining = (int) $payroll->deduction_amount;

        if ($remaining <= 0) {
            return;
        }

        $advances = EmployeeAdvance::query()
            ->where('employee_id', $payroll->employee_id)
            ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID])
            ->orderBy('created_at')
            ->get();

        foreach ($advances as $advance) {
            if ($remaining <= 0) {
                break;
            }

            $advanceRemaining = $advance->amount - $advance->paid_amount;
            $paymentAmount = min($remaining, $advanceRemaining);
            $advance->paid_amount += $paymentAmount;

            if ($advance->paid_amount >= $advance->amount) {
                $advance->paid_at = now();
                $advance->paid_by_id = $user->id;
                $advance->status = EmployeeAdvanceStatus::PAID;
            } else {
                $advance->status = EmployeeAdvanceStatus::PARTIALLY_PAID;
            }

            $advance->save();

            $remaining -= $paymentAmount;
        }
    }

    private function applySorting(Builder $query, string $sort, string $direction): void
    {
        if (in_array($sort, ['base_salary', 'bonus_amount', 'deduction_amount', 'total_amount', 'status', 'created_at'], true)) {
            $query->orderBy($sort, $direction);

            return;
        }

        $query->orderBy('employee_id');
    }
}
