<?php

namespace App\Http\Controllers\Api\V1\Payroll;

use App\Domain\HR\Models\Employee;
use App\Domain\Payroll\Enums\CalculationMode;
use App\Domain\Payroll\Enums\ComponentType;
use App\Domain\Payroll\Enums\PayrollStatus;
use App\Domain\Payroll\Models\EmployeeSalaryLine;
use App\Domain\Payroll\Models\EmployeeSalaryStructure;
use App\Domain\Payroll\Models\PayrollRun;
use App\Domain\Payroll\Models\Payslip;
use App\Domain\Payroll\Models\SalaryComponent;
use App\Domain\Payroll\Services\PayrollService;
use App\Http\Controllers\Controller;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

/** Payroll runs, structures and payslips (§51–§55). */
class PayrollController extends Controller
{
    public function __construct(private readonly PayrollService $payroll) {}

    public function index(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('payroll.view'), 403);

        $runs = PayrollRun::where('company_id', $request->user()->company_id)
            ->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
            ->latest('period_start')
            ->latest('id')
            ->paginate($request->integer('per_page', 25));

        return ApiResponse::success(
            $runs->through(fn (PayrollRun $run) => $this->presentRun($run)),
            meta: [
                'statuses' => array_map(
                    fn (PayrollStatus $status) => ['value' => $status->value, 'label' => $status->label()],
                    PayrollStatus::cases(),
                ),
            ],
        );
    }

    public function show(Request $request, PayrollRun $run): JsonResponse
    {
        abort_unless($request->user()->can('payroll.view'), 403);
        $this->assertSameCompany($request, $run);

        $run->load(['payslips.employee:id,uuid,full_name,employee_code', 'payslips.lines']);

        return ApiResponse::success(array_merge($this->presentRun($run), [
            'payslips' => $run->payslips->map(fn (Payslip $payslip) => $this->presentPayslip($payslip)),
        ]));
    }

    public function store(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('payroll.calculate'), 403);

        $validated = $request->validate([
            'period_start' => ['required', 'date'],
            'period_end' => ['required', 'date', 'after_or_equal:period_start'],
            'branch_id' => ['nullable', 'integer', 'exists:branches,id'],
        ]);

        $run = $this->payroll->open(
            $request->user()->company_id,
            $validated['branch_id'] ?? null,
            Carbon::parse($validated['period_start']),
            Carbon::parse($validated['period_end']),
            $request->user(),
        );

        return ApiResponse::success($this->presentRun($run), 'Payroll opened.', status: 201);
    }

    public function calculate(Request $request, PayrollRun $run): JsonResponse
    {
        abort_unless($request->user()->can('payroll.calculate'), 403);
        $this->assertSameCompany($request, $run);

        $validated = $request->validate([
            'attendance' => ['nullable', 'array'],
            'attendance.*.days_absent' => ['nullable', 'integer', 'min:0', 'max:31'],
            'attendance.*.overtime_hours' => ['nullable', 'integer', 'min:0', 'max:400'],
        ]);

        $calculated = $this->payroll->calculate($run, $request->user(), $validated['attendance'] ?? []);

        return ApiResponse::success(
            $this->presentRun($calculated),
            "{$calculated->employee_count} payslips calculated.",
        );
    }

    public function approve(Request $request, PayrollRun $run): JsonResponse
    {
        abort_unless($request->user()->can('payroll.approve'), 403);
        $this->assertSameCompany($request, $run);

        return ApiResponse::success(
            $this->presentRun($this->payroll->approve($run, $request->user())),
            'Payroll approved.',
        );
    }

    public function post(Request $request, PayrollRun $run): JsonResponse
    {
        abort_unless($request->user()->can('payroll.post'), 403);
        $this->assertSameCompany($request, $run);

        return ApiResponse::success(
            $this->presentRun($this->payroll->post($run, $request->user())),
            'Payroll posted to accounts.',
        );
    }

    public function pay(Request $request, PayrollRun $run): JsonResponse
    {
        abort_unless($request->user()->can('payroll.pay'), 403);
        $this->assertSameCompany($request, $run);

        $validated = $request->validate([
            'paid_from_code' => ['nullable', 'string', 'max:20'],
            'payment_method' => ['nullable', 'string', 'max:30'],
            'payment_reference' => ['nullable', 'string', 'max:100'],
            'pay_date' => ['nullable', 'date'],
        ]);

        return ApiResponse::success(
            $this->presentRun($this->payroll->pay($run, $validated, $request->user())),
            'Salaries paid.',
        );
    }

    public function components(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('payroll.view'), 403);

        $components = SalaryComponent::where('company_id', $request->user()->company_id)
            ->active()
            ->get();

        return ApiResponse::success(
            $components->map(fn (SalaryComponent $component) => [
                'id' => $component->uuid,
                'code' => $component->code,
                'name' => $component->name,
                'component_type' => $component->component_type->value,
                'type_label' => $component->component_type->label(),
                'calculation_mode' => $component->calculation_mode->value,
                'mode_label' => $component->calculation_mode->label(),
                'rate_basis_points' => (int) $component->rate_basis_points,
                'rate_label' => $component->calculation_mode->needsRate() ? $component->rateLabel() : null,
                'is_taxable' => $component->is_taxable,
            ]),
            meta: [
                'types' => array_map(
                    fn (ComponentType $type) => ['value' => $type->value, 'label' => $type->label()],
                    ComponentType::cases(),
                ),
                'modes' => array_map(
                    fn (CalculationMode $mode) => [
                        'value' => $mode->value,
                        'label' => $mode->label(),
                        'needs_rate' => $mode->needsRate(),
                    ],
                    CalculationMode::cases(),
                ),
            ],
        );
    }

    public function structure(Request $request, Employee $employee): JsonResponse
    {
        abort_unless($request->user()->can('payroll.view'), 403);
        abort_unless($employee->company_id === $request->user()->company_id, 403);

        $structures = EmployeeSalaryStructure::where('employee_id', $employee->getKey())
            ->with('lines.component')
            ->orderByDesc('effective_from')
            ->get();

        return ApiResponse::success(
            $structures->map(fn (EmployeeSalaryStructure $structure) => [
                'id' => $structure->uuid,
                'currency' => $structure->currency,
                'basic_salary' => (int) $structure->basic_salary,
                'effective_from' => $structure->effective_from->toDateString(),
                'effective_to' => $structure->effective_to?->toDateString(),
                'is_current' => $structure->effective_to === null,
                'lines' => $structure->lines->map(fn (EmployeeSalaryLine $line) => [
                    'component' => $line->component?->name,
                    'component_code' => $line->component?->code,
                    'type' => $line->component?->component_type->value,
                    'mode' => $line->component?->calculation_mode->value,
                    'amount' => (int) $line->amount,
                    'rate_basis_points' => (int) $line->rate_basis_points,
                ]),
            ]),
        );
    }

    public function setStructure(Request $request, Employee $employee): JsonResponse
    {
        abort_unless($request->user()->can('payroll.structure_manage'), 403);
        abort_unless($employee->company_id === $request->user()->company_id, 403);

        $validated = $request->validate([
            'basic_salary' => ['required', 'integer', 'min:1'],
            'currency' => ['required', 'string', 'size:3'],
            'effective_from' => ['required', 'date'],
            'lines' => ['nullable', 'array', 'max:40'],
            'lines.*.component_code' => ['required', 'string', 'max:40'],
            'lines.*.amount' => ['nullable', 'integer', 'min:0'],
            'lines.*.rate_basis_points' => ['nullable', 'integer', 'min:0', 'max:10000'],
        ]);

        $structure = DB::transaction(function () use ($employee, $validated, $request) {
            $effectiveFrom = Carbon::parse($validated['effective_from']);

            // The previous structure is closed off rather than edited, so old
            // payslips keep the numbers they were calculated from.
            EmployeeSalaryStructure::where('employee_id', $employee->getKey())
                ->whereNull('effective_to')
                ->update(['effective_to' => $effectiveFrom->copy()->subDay()->toDateString()]);

            $structure = EmployeeSalaryStructure::create([
                'company_id' => $employee->company_id,
                'employee_id' => $employee->getKey(),
                'currency' => $validated['currency'],
                'basic_salary' => $validated['basic_salary'],
                'effective_from' => $effectiveFrom->toDateString(),
                'created_by' => $request->user()->getKey(),
            ]);

            foreach ($validated['lines'] ?? [] as $line) {
                $component = SalaryComponent::where('company_id', $employee->company_id)
                    ->where('code', $line['component_code'])
                    ->first();

                if ($component === null) {
                    continue;
                }

                EmployeeSalaryLine::create([
                    'employee_salary_structure_id' => $structure->getKey(),
                    'salary_component_id' => $component->getKey(),
                    'amount' => $line['amount'] ?? 0,
                    'rate_basis_points' => $line['rate_basis_points'] ?? 0,
                ]);
            }

            return $structure;
        });

        return ApiResponse::success(
            ['id' => $structure->uuid, 'effective_from' => $structure->effective_from->toDateString()],
            'Salary structure saved.',
            status: 201,
        );
    }

    /** An employee may always read their own payslips (§54). */
    public function myPayslips(Request $request): JsonResponse
    {
        $employee = Employee::where('user_id', $request->user()->getKey())->first();

        abort_if($employee === null, 403, 'No employee record is linked to this account.');

        $payslips = Payslip::where('employee_id', $employee->getKey())
            ->with(['lines', 'run:id,period_label'])
            ->latest('id')
            ->limit(24)
            ->get();

        return ApiResponse::success(
            $payslips->map(fn (Payslip $payslip) => $this->presentPayslip($payslip)),
        );
    }

    private function assertSameCompany(Request $request, PayrollRun $run): void
    {
        abort_unless($run->company_id === $request->user()->company_id, 403);
    }

    /** @return array<string, mixed> */
    private function presentRun(PayrollRun $run): array
    {
        return [
            'id' => $run->uuid,
            'run_no' => $run->run_no,
            'period_label' => $run->period_label,
            'period_start' => $run->period_start?->toDateString(),
            'period_end' => $run->period_end?->toDateString(),
            'pay_date' => $run->pay_date?->toDateString(),
            'status' => $run->status->value,
            'status_label' => $run->status->label(),
            'is_recalculable' => $run->status->isRecalculable(),
            'currency' => $run->currency,
            'gross_earnings' => (int) $run->gross_earnings,
            'total_deductions' => (int) $run->total_deductions,
            'employer_contributions' => (int) $run->employer_contributions,
            'net_payable' => (int) $run->net_payable,
            'employee_count' => (int) $run->employee_count,
            'calculated_at' => $run->calculated_at?->toIso8601String(),
            'approved_at' => $run->approved_at?->toIso8601String(),
            'posted_at' => $run->posted_at?->toIso8601String(),
            'paid_at' => $run->paid_at?->toIso8601String(),
        ];
    }

    /** @return array<string, mixed> */
    private function presentPayslip(Payslip $payslip): array
    {
        return [
            'id' => $payslip->uuid,
            'payslip_no' => $payslip->payslip_no,
            'period' => $payslip->relationLoaded('run') ? $payslip->run?->period_label : null,
            'currency' => $payslip->currency,
            'basic_salary' => (int) $payslip->basic_salary,
            'gross_earnings' => (int) $payslip->gross_earnings,
            'total_deductions' => (int) $payslip->total_deductions,
            'employer_contributions' => (int) $payslip->employer_contributions,
            'net_pay' => (int) $payslip->net_pay,
            'days_absent' => (int) $payslip->days_absent,
            'overtime_hours' => (int) $payslip->overtime_hours,
            'payment_method' => $payslip->payment_method,
            'payment_reference' => $payslip->payment_reference,
            'paid_on' => $payslip->paid_on?->toDateString(),
            'employee' => $payslip->relationLoaded('employee') && $payslip->employee ? [
                'id' => $payslip->employee->uuid,
                'name' => $payslip->employee->full_name,
                'code' => $payslip->employee->employee_code,
            ] : null,
            'lines' => $payslip->relationLoaded('lines')
                ? $payslip->lines->map(fn ($line) => [
                    'name' => $line->component_name,
                    'type' => $line->component_type,
                    'amount' => (int) $line->amount,
                    'basis' => $line->basis,
                ])
                : [],
        ];
    }
}
