<?php

namespace App\Services;

use App\Models\Account;
use App\Models\AttendanceCorrection;
use App\Models\AttendanceRecord;
use App\Models\AuditLog;
use App\Models\Employee;
use App\Models\EmployeeLoan;
use App\Models\EmployeeSalaryStructure;
use App\Models\JournalEntry;
use App\Models\JournalItem;
use App\Models\LeaveRequest;
use App\Models\LoanInstallment;
use App\Models\PayrollAdjustment;
use App\Models\PayrollItem;
use App\Models\PayrollItemComponent;
use App\Models\PayrollPeriod;
use App\Models\PayrollRun;
use App\Models\SalaryPaymentBatch;
use App\Models\Tenant;
use App\Models\User;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;

class PayrollService
{
    public function __construct(
        protected OperationalNumberSequenceService $operationalSequences,
        protected FinancialNumberSequenceService $financialSequences,
        protected ErpNotificationService $notifications,
    ) {}

    public function createAndCalculate(Tenant $tenant, array $data, User $actor): PayrollRun
    {
        return DB::transaction(function () use ($tenant, $data, $actor) {
            $start = Carbon::parse($data['period_start'])->startOfDay();
            $end = Carbon::parse($data['period_end'])->startOfDay();
            $period = PayrollPeriod::firstOrCreate(
                ['tenant_id' => $tenant->id, 'period_start' => $start->toDateString(), 'period_end' => $end->toDateString()],
                ['period_name' => $start->format('F Y'), 'payment_date' => $data['payment_date'] ?? null, 'status' => 'open'],
            );

            $duplicate = PayrollRun::where('payroll_period_id', $period->id)
                ->where(fn ($query) => isset($data['branch_id']) ? $query->where('branch_id', $data['branch_id']) : $query->whereNull('branch_id'))
                ->where(fn ($query) => isset($data['payroll_group_id']) ? $query->where('payroll_group_id', $data['payroll_group_id']) : $query->whereNull('payroll_group_id'))
                ->whereNotIn('status', ['reversed'])
                ->exists();
            if ($duplicate) {
                throw ValidationException::withMessages(['period_start' => 'A payroll run already exists for this period, branch, and payroll group.']);
            }

            $run = PayrollRun::create([
                'tenant_id' => $tenant->id,
                'branch_id' => $data['branch_id'] ?? null,
                'payroll_period_id' => $period->id,
                'payroll_group_id' => $data['payroll_group_id'] ?? null,
                'run_number' => $this->operationalSequences->next($tenant, 'payroll'),
                'status' => 'draft',
                'created_by' => $actor->id,
            ]);

            $this->calculate($run, $actor);
            AuditLog::record('payroll', 'payroll.run_created', $run, null, $run->fresh()->toArray(), $actor);
            $this->notifications->notifyPermission($tenant->id, 'payroll.review', 'payroll', 'Payroll calculated', $run->run_number.' is ready for controlled review.', route('app.payroll.show', $run->id), $actor->id);

            return $run->fresh(['period', 'items.employee']);
        });
    }

    public function calculate(PayrollRun $run, User $actor): PayrollRun
    {
        if ($run->is_locked) {
            throw ValidationException::withMessages(['payroll' => 'Approved payroll is locked. Use a controlled adjustment or reversal workflow.']);
        }

        return DB::transaction(function () use ($run, $actor) {
            $run->loadMissing(['period', 'tenant']);
            $employeeQuery = Employee::where('status', 'active')->whereDate('joining_date', '<=', $run->period->period_end);
            if ($run->branch_id) {
                $employeeQuery->where('branch_id', $run->branch_id);
            }
            $employees = $employeeQuery->orderBy('employee_code')->get();
            if ($employees->isEmpty()) {
                throw ValidationException::withMessages(['payroll' => 'No active employees match this payroll run.']);
            }

            $missingSalary = [];
            $salaryByEmployee = [];
            foreach ($employees as $employee) {
                $salary = $this->effectiveSalary($employee, $run->period->period_end, $run->payroll_group_id);
                if (! $salary) {
                    $missingSalary[] = $employee->employee_code.' — '.$employee->full_name;
                } else {
                    $salaryByEmployee[$employee->id] = $salary;
                }
            }
            if ($missingSalary) {
                throw ValidationException::withMessages(['payroll' => 'Missing effective salary structure for: '.implode(', ', $missingSalary).'.']);
            }

            $run->items()->delete();
            $warnings = [];
            foreach ($employees as $employee) {
                $this->calculateEmployee($run, $employee, $salaryByEmployee[$employee->id], $warnings);
            }

            $totals = $run->items()->selectRaw('COALESCE(SUM(gross_salary),0) gross, COALESCE(SUM(total_deductions),0) deductions, COALESCE(SUM(net_salary),0) net')->first();
            $run->update([
                'gross_total' => $totals->gross,
                'deduction_total' => $totals->deductions,
                'net_total' => $totals->net,
                'validation_warnings' => array_values(array_unique($warnings)),
                'status' => 'calculated',
            ]);
            AuditLog::record('payroll', 'payroll.calculated', $run, null, ['gross_total' => $totals->gross, 'net_total' => $totals->net, 'warnings' => $warnings], $actor);

            return $run->fresh(['items.employee', 'period']);
        });
    }

    public function addAdjustment(PayrollItem $item, string $type, float $amount, string $reason, User $actor): PayrollItem
    {
        $item->loadMissing('run');
        if (! in_array($item->run->status, ['draft', 'calculated'], true) || $item->run->is_locked) {
            throw ValidationException::withMessages(['payroll' => 'Adjustments are allowed only before payroll review and approval.']);
        }

        return DB::transaction(function () use ($item, $type, $amount, $reason, $actor) {
            PayrollAdjustment::create(['tenant_id' => $item->tenant_id, 'payroll_item_id' => $item->id, 'type' => $type, 'amount' => $amount, 'reason' => $reason, 'created_by' => $actor->id]);
            if ($type === 'earning') {
                $item->increment('other_earnings', $amount);
                $item->increment('gross_salary', $amount);
                $item->increment('net_salary', $amount);
            } else {
                $item->increment('other_deductions', $amount);
                $item->increment('total_deductions', $amount);
                $item->decrement('net_salary', $amount);
            }
            $this->refreshRunTotals($item->run);
            AuditLog::record('payroll', 'payroll.adjustment_added', $item, null, ['type' => $type, 'amount' => $amount, 'reason' => $reason], $actor);

            return $item->fresh(['employee', 'components', 'adjustments']);
        });
    }

    public function transition(PayrollRun $run, string $action, User $actor): PayrollRun
    {
        $allowed = [
            'hr_review' => ['from' => ['calculated'], 'to' => 'hr_reviewed'],
            'finance_review' => ['from' => ['hr_reviewed'], 'to' => 'finance_reviewed'],
            'approve' => ['from' => ['finance_reviewed'], 'to' => 'approved'],
            'pay' => ['from' => ['approved'], 'to' => 'paid'],
        ];
        $rule = $allowed[$action] ?? null;
        if (! $rule || ! in_array($run->status, $rule['from'], true)) {
            throw ValidationException::withMessages(['payroll' => "Payroll cannot perform [{$action}] while status is [{$run->status}]."]);
        }

        return DB::transaction(function () use ($run, $action, $actor, $rule) {
            $updates = ['status' => $rule['to']];
            if ($action === 'hr_review') {
                $updates += ['hr_reviewed_by' => $actor->id, 'hr_reviewed_at' => now()];
            } elseif ($action === 'finance_review') {
                $updates += ['finance_reviewed_by' => $actor->id, 'finance_reviewed_at' => now()];
            } elseif ($action === 'approve') {
                $updates += ['approved_by' => $actor->id, 'approved_at' => now(), 'locked_at' => now()];
                $this->commitLoanDeductions($run);
                $this->postAccounting($run, $actor);
            } elseif ($action === 'pay') {
                $updates += ['paid_by' => $actor->id, 'paid_at' => now(), 'locked_at' => $run->locked_at ?? now()];
                $run->items()->update(['status' => 'paid']);
                $run->period()->update(['status' => 'closed']);
            }
            $before = $run->toArray();
            $run->update($updates);
            AuditLog::record('payroll', 'payroll.'.$action, $run, $before, $run->fresh()->toArray(), $actor);
            $targetPermission = match ($action) {
                'hr_review' => 'payroll.review',
                'finance_review' => 'payroll.approve',
                default => null,
            };
            if ($targetPermission) {
                $this->notifications->notifyPermission($run->tenant_id, $targetPermission, 'payroll', 'Payroll moved to '.str_replace('_', ' ', $rule['to']), $run->run_number.' requires the next review action.', route('app.payroll.show', $run->id), $actor->id);
            }

            return $run->fresh(['period', 'items.employee']);
        });
    }

    public function prepareWps(PayrollRun $run, User $actor): SalaryPaymentBatch
    {
        if (! in_array($run->status, ['approved', 'paid'], true)) {
            throw ValidationException::withMessages(['wps' => 'WPS export is available only after payroll approval.']);
        }
        $run->loadMissing(['items.employee.bankDetail', 'tenant']);
        $missing = $run->items->filter(fn ($item) => ! $item->employee->bankDetail?->iban_encrypted);
        if ($missing->isNotEmpty()) {
            throw ValidationException::withMessages(['wps' => 'Missing bank/WPS details for: '.$missing->pluck('employee.full_name')->join(', ').'.']);
        }

        return DB::transaction(function () use ($run, $actor) {
            $batchNumber = $this->operationalSequences->next($run->tenant, 'salary_batch');
            $lines = [implode(',', ['Employee ID', 'Employee Name', 'IBAN', 'Net Salary', 'Currency', 'Payroll Period'])];
            foreach ($run->items as $item) {
                $lines[] = $this->csvLine([
                    $item->employee->employee_code,
                    $item->employee->full_name,
                    $item->employee->bankDetail->iban_encrypted,
                    number_format((float) $item->net_salary, 2, '.', ''),
                    $item->salaryStructure->currency ?? 'AED',
                    $run->period->period_name,
                ]);
            }
            $path = "payroll/{$run->tenant_id}/wps/{$batchNumber}.csv";
            Storage::disk('local')->put($path, implode("\r\n", $lines)."\r\n");
            $batch = SalaryPaymentBatch::create(['tenant_id' => $run->tenant_id, 'payroll_run_id' => $run->id, 'batch_number' => $batchNumber, 'format' => 'wps_csv', 'status' => 'prepared', 'file_path' => $path, 'prepared_by' => $actor->id]);
            AuditLog::record('payroll', 'wps.batch_prepared', $batch, null, ['payroll_run_id' => $run->id], $actor);

            return $batch;
        });
    }

    public function createLoan(Employee $employee, array $data, User $actor): EmployeeLoan
    {
        $loan = EmployeeLoan::create([
            'tenant_id' => $employee->tenant_id,
            'loan_number' => $this->operationalSequences->next($employee->tenant, 'loan'),
            'employee_id' => $employee->id,
            'type' => $data['type'],
            'amount' => $data['amount'],
            'loan_date' => $data['loan_date'],
            'installment_amount' => $data['installment_amount'],
            'start_month' => Carbon::parse($data['start_month'])->startOfMonth(),
            'remaining_amount' => $data['amount'],
            'status' => 'requested',
            'notes' => $data['notes'] ?? null,
            'requested_by' => $actor->id,
        ]);
        AuditLog::record('payroll', 'loan.requested', $loan, null, $loan->toArray(), $actor);

        return $loan;
    }

    public function approveLoan(EmployeeLoan $loan, User $actor): EmployeeLoan
    {
        if ($loan->status !== 'requested') {
            throw ValidationException::withMessages(['loan' => 'Only requested loans or advances can be approved.']);
        }
        $loan->update(['status' => 'active', 'approved_by' => $actor->id, 'approved_at' => now()]);
        AuditLog::record('payroll', 'loan.approved', $loan, null, $loan->toArray(), $actor);

        return $loan;
    }

    private function calculateEmployee(PayrollRun $run, Employee $employee, EmployeeSalaryStructure $salary, array &$warnings): PayrollItem
    {
        $period = $run->period;
        $workingDates = $this->workingDates($employee, $period->period_start, $period->period_end);
        $attendance = AttendanceRecord::where('employee_id', $employee->id)->whereBetween('attendance_date', [$period->period_start, $period->period_end])->get();
        $attendanceByDate = $attendance->keyBy(fn ($record) => $record->attendance_date->toDateString());
        $missingAttendance = collect($workingDates)->reject(fn ($date) => $attendanceByDate->has($date))->count();
        if ($missingAttendance > 0) {
            $warnings[] = "{$employee->employee_code}: {$missingAttendance} working day(s) have no attendance record.";
        }
        if (AttendanceCorrection::where('employee_id', $employee->id)->where('status', 'requested')->whereHas('attendanceRecord', fn ($query) => $query->whereBetween('attendance_date', [$period->period_start, $period->period_end]))->exists()) {
            $warnings[] = "{$employee->employee_code}: unresolved attendance correction.";
        }
        if (LeaveRequest::where('employee_id', $employee->id)->whereIn('status', ['submitted', 'manager_approved'])->whereDate('from_date', '<=', $period->period_end)->whereDate('to_date', '>=', $period->period_start)->exists()) {
            $warnings[] = "{$employee->employee_code}: leave request is pending approval.";
        }
        if (in_array($salary->payment_method, ['bank_transfer', 'wps'], true) && ! $employee->bankDetail?->iban_encrypted) {
            $warnings[] = "{$employee->employee_code}: bank/WPS details are missing.";
        }

        $absenceDays = $attendance->where('status', 'absent')->count();
        $presentDays = $attendance->whereIn('status', ['present', 'late', 'early_leave', 'half_day', 'work_from_home', 'business_visit'])->count();
        $unpaidLeaveDays = $this->unpaidLeaveDays($employee, $period->period_start, $period->period_end);
        $overtimeMinutes = (int) $attendance->sum('overtime_minutes');
        $basic = (float) $salary->basic_salary;
        $housing = (float) $salary->housing_allowance;
        $transport = (float) $salary->transport_allowance;
        $otherEarnings = (float) $salary->other_fixed_allowance + (float) $salary->variable_allowance;
        $hourlyRate = (float) ($salary->overtime_hourly_rate ?: ($basic / 30 / 8));
        $overtimeAmount = $salary->overtime_eligible ? round(($overtimeMinutes / 60) * $hourlyRate * 1.25, 2) : 0;
        $gross = round($basic + $housing + $transport + $otherEarnings + $overtimeAmount, 2);
        $absenceDeduction = round(($basic / 30) * $absenceDays, 2);
        $unpaidDeduction = round(($basic / 30) * $unpaidLeaveDays, 2);
        $loan = EmployeeLoan::where('employee_id', $employee->id)->where('status', 'active')->where('remaining_amount', '>', 0)->whereDate('start_month', '<=', $period->period_end)->oldest('start_month')->first();
        $loanDeduction = $loan ? min((float) $loan->installment_amount, (float) $loan->remaining_amount) : 0;
        $deductions = round($absenceDeduction + $unpaidDeduction + $loanDeduction, 2);
        $net = round(max(0, $gross - $deductions), 2);

        $item = PayrollItem::create([
            'tenant_id' => $run->tenant_id,
            'payroll_run_id' => $run->id,
            'employee_id' => $employee->id,
            'salary_structure_id' => $salary->id,
            'basic_salary' => $basic,
            'housing_allowance' => $housing,
            'transport_allowance' => $transport,
            'other_earnings' => $otherEarnings,
            'overtime_amount' => $overtimeAmount,
            'gross_salary' => $gross,
            'absence_deduction' => $absenceDeduction,
            'unpaid_leave_deduction' => $unpaidDeduction,
            'loan_deduction' => $loanDeduction,
            'total_deductions' => $deductions,
            'net_salary' => $net,
            'working_days' => count($workingDates),
            'present_days' => $presentDays,
            'unpaid_leave_days' => $unpaidLeaveDays,
            'absence_days' => $absenceDays,
            'overtime_minutes' => $overtimeMinutes,
            'calculation_snapshot' => [
                'period_start' => $period->period_start->toDateString(),
                'period_end' => $period->period_end->toDateString(),
                'daily_basic_rate' => round($basic / 30, 4),
                'overtime_hourly_rate' => round($hourlyRate, 4),
                'salary_structure' => $salary->toArray(),
                'loan_id' => $loan?->id,
            ],
        ]);

        $components = [
            ['Basic Salary', 'earning', $basic], ['Housing Allowance', 'earning', $housing], ['Transport Allowance', 'earning', $transport],
            ['Other Earnings', 'earning', $otherEarnings], ['Overtime', 'earning', $overtimeAmount], ['Absence', 'deduction', $absenceDeduction],
            ['Unpaid Leave', 'deduction', $unpaidDeduction], ['Loan / Advance', 'deduction', $loanDeduction],
        ];
        foreach ($components as [$name, $type, $amount]) {
            if ($amount > 0) {
                PayrollItemComponent::create(['tenant_id' => $run->tenant_id, 'payroll_item_id' => $item->id, 'name' => $name, 'type' => $type, 'amount' => $amount, 'source' => 'calculated']);
            }
        }
        if ($loan && $loanDeduction > 0) {
            LoanInstallment::updateOrCreate(
                ['employee_loan_id' => $loan->id, 'due_month' => $period->period_start->copy()->startOfMonth()->toDateString()],
                ['tenant_id' => $run->tenant_id, 'payroll_item_id' => $item->id, 'amount' => $loanDeduction, 'status' => 'scheduled'],
            );
        }

        return $item;
    }

    private function effectiveSalary(Employee $employee, Carbon $date, ?int $groupId): ?EmployeeSalaryStructure
    {
        return EmployeeSalaryStructure::where('employee_id', $employee->id)
            ->when($groupId, fn ($query) => $query->where('payroll_group_id', $groupId))
            ->whereDate('effective_from', '<=', $date)
            ->where(fn ($query) => $query->whereNull('effective_to')->orWhereDate('effective_to', '>=', $date))
            ->latest('effective_from')
            ->first();
    }

    private function workingDates(Employee $employee, Carbon $start, Carbon $end): array
    {
        $weeklyOff = strtolower($employee->weekly_off ?: 'sunday');
        $dates = [];
        foreach (CarbonPeriod::create($start, $end) as $date) {
            if (strtolower($date->englishDayOfWeek) !== $weeklyOff) {
                $dates[] = $date->toDateString();
            }
        }

        return $dates;
    }

    private function unpaidLeaveDays(Employee $employee, Carbon $start, Carbon $end): float
    {
        $requests = LeaveRequest::with('leaveType')->where('employee_id', $employee->id)->where('status', 'approved')
            ->whereDate('from_date', '<=', $end)->whereDate('to_date', '>=', $start)->get();
        $days = 0;
        foreach ($requests as $request) {
            if ($request->leaveType->is_paid) {
                continue;
            }
            $from = $request->from_date->greaterThan($start) ? $request->from_date : $start;
            $to = $request->to_date->lessThan($end) ? $request->to_date : $end;
            $days += $from->diffInDays($to) + 1;
        }

        return $days;
    }

    private function refreshRunTotals(PayrollRun $run): void
    {
        $totals = $run->items()->selectRaw('COALESCE(SUM(gross_salary),0) gross, COALESCE(SUM(total_deductions),0) deductions, COALESCE(SUM(net_salary),0) net')->first();
        $run->update(['gross_total' => $totals->gross, 'deduction_total' => $totals->deductions, 'net_total' => $totals->net]);
    }

    private function commitLoanDeductions(PayrollRun $run): void
    {
        LoanInstallment::whereIn('payroll_item_id', $run->items()->pluck('id'))->where('status', 'scheduled')->lockForUpdate()->get()->each(function (LoanInstallment $installment) {
            $loan = EmployeeLoan::whereKey($installment->employee_loan_id)->lockForUpdate()->firstOrFail();
            $remaining = max(0, (float) $loan->remaining_amount - (float) $installment->amount);
            $loan->update(['remaining_amount' => $remaining, 'status' => $remaining <= 0 ? 'completed' : 'active']);
            $installment->update(['status' => 'deducted', 'deducted_at' => now()]);
        });
    }

    private function postAccounting(PayrollRun $run, User $actor): void
    {
        if (JournalEntry::where('tenant_id', $run->tenant_id)->where('source_type', 'payroll')->where('reference', $run->run_number)->exists()) {
            return;
        }
        $expense = Account::firstOrCreate(['tenant_id' => $run->tenant_id, 'account_code' => '6100'], ['account_name' => 'Payroll Expense', 'account_type' => 'expense', 'is_system' => true, 'status' => 'active']);
        $payable = Account::firstOrCreate(['tenant_id' => $run->tenant_id, 'account_code' => '2100'], ['account_name' => 'Payroll Payable', 'account_type' => 'liability', 'is_system' => true, 'status' => 'active']);
        $deductionPayable = Account::firstOrCreate(['tenant_id' => $run->tenant_id, 'account_code' => '2110'], ['account_name' => 'Payroll Deductions Payable', 'account_type' => 'liability', 'is_system' => true, 'status' => 'active']);
        $entry = JournalEntry::create([
            'tenant_id' => $run->tenant_id,
            'branch_id' => $run->branch_id,
            'entry_number' => $this->financialSequences->next($run->tenant, 'journal'),
            'entry_date' => now()->toDateString(),
            'source_type' => 'payroll',
            'reference' => $run->run_number,
            'narration' => 'Approved payroll '.$run->period->period_name,
            'total_debit' => $run->gross_total,
            'total_credit' => $run->gross_total,
            'created_by' => $actor->id,
        ]);
        JournalItem::create(['journal_entry_id' => $entry->id, 'account_id' => $expense->id, 'debit' => $run->gross_total, 'credit' => 0, 'description' => 'Gross payroll cost']);
        JournalItem::create(['journal_entry_id' => $entry->id, 'account_id' => $payable->id, 'debit' => 0, 'credit' => $run->net_total, 'description' => 'Salary payable']);
        if ((float) $run->deduction_total > 0) {
            JournalItem::create(['journal_entry_id' => $entry->id, 'account_id' => $deductionPayable->id, 'debit' => 0, 'credit' => $run->deduction_total, 'description' => 'Payroll deductions payable']);
        }
    }

    private function csvLine(array $values): string
    {
        return implode(',', array_map(fn ($value) => '"'.str_replace('"', '""', (string) $value).'"', $values));
    }
}
