<?php

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

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Payments\Enums\PaymentMethod;
use App\Domain\Refunds\Enums\RefundStatus;
use App\Domain\Refunds\Models\Refund;
use App\Domain\Refunds\Services\RefundService;
use App\Http\Controllers\Controller;
use App\Support\ApiResponse;
use App\Support\Money\Money;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

/** Refunds (§31). */
class RefundController extends Controller
{
    public function __construct(private readonly RefundService $refunds) {}

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

        $refunds = Refund::query()
            ->with(['applicant:id,uuid,full_name,reference_no', 'requestedBy:id,name'])
            ->where('company_id', $request->user()->company_id)
            ->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
            ->when($request->boolean('awaiting_approval'), fn ($q) => $q->awaitingApproval())
            ->when($request->boolean('awaiting_payout'), fn ($q) => $q->awaitingPayout())
            ->when($request->filled('search'), function ($q) use ($request) {
                $search = '%'.$request->string('search').'%';
                $q->where(fn ($inner) => $inner
                    ->where('refund_no', 'like', $search)
                    ->orWhereHas('applicant', fn ($a) => $a
                        ->where('full_name', 'like', $search)
                        ->orWhere('reference_no', 'like', $search)));
            })
            ->latest('requested_at')
            ->latest('id')
            ->paginate($request->integer('per_page', 25));

        return ApiResponse::success(
            $refunds->through(fn (Refund $refund) => $this->present($refund)),
            meta: [
                'statuses' => array_map(
                    fn (RefundStatus $status) => ['value' => $status->value, 'label' => $status->label()],
                    RefundStatus::cases(),
                ),
                'reason_codes' => config('mams.refunds.reason_codes'),
                'approval_limit' => (int) config('mams.refunds.approval_limit'),
            ],
        );
    }

    public function forApplicant(Request $request, Applicant $applicant): JsonResponse
    {
        $this->authorize('view', $applicant);
        abort_unless($request->user()->can('refund.view'), 403);

        $refunds = Refund::where('applicant_id', $applicant->getKey())
            ->with('requestedBy:id,name')
            ->latest('id')
            ->get();

        return ApiResponse::success($refunds->map(fn (Refund $refund) => $this->present($refund)));
    }

    public function store(Request $request, Applicant $applicant): JsonResponse
    {
        $this->authorize('view', $applicant);
        abort_unless($request->user()->can('refund.request'), 403);

        $validated = $request->validate([
            'requested_amount' => ['required', 'integer', 'min:1'],
            'currency' => ['required', 'string', 'size:3'],
            'reason' => ['required', 'string', 'max:2000'],
            'reason_code' => ['nullable', Rule::in(array_keys(config('mams.refunds.reason_codes', [])))],
            'payment_id' => ['nullable', 'uuid'],
            'invoice_id' => ['nullable', 'uuid'],
            'beneficiary_name' => ['nullable', 'string', 'max:255'],
            'beneficiary_account' => ['nullable', 'string', 'max:120'],
            'notes' => ['nullable', 'string', 'max:1000'],
        ]);

        $refund = $this->refunds->request(
            $applicant,
            Money::of($validated['requested_amount'], $validated['currency']),
            $validated['reason'],
            $request->user(),
            $validated,
        );

        return ApiResponse::success(
            $this->present($refund->fresh(['applicant'])),
            'Refund requested.',
            status: 201,
        );
    }

    public function review(Request $request, Refund $refund): JsonResponse
    {
        abort_unless($request->user()->can('refund.review'), 403);
        $this->assertSameCompany($request, $refund);

        $validated = $request->validate([
            'deduction_amount' => ['nullable', 'integer', 'min:0'],
            'note' => ['nullable', 'string', 'max:1000'],
        ]);

        $reviewed = $this->refunds->review(
            $refund,
            $request->user(),
            $validated['deduction_amount'] ?? null,
            $validated['note'] ?? null,
        );

        return ApiResponse::success($this->present($reviewed->fresh(['applicant'])), 'Review recorded.');
    }

    public function approve(Request $request, Refund $refund): JsonResponse
    {
        abort_unless($request->user()->can('refund.approve') || $request->user()->can('refund.review'), 403);
        $this->assertSameCompany($request, $refund);

        $validated = $request->validate([
            'approved_amount' => ['nullable', 'integer', 'min:1'],
            'deduction_amount' => ['nullable', 'integer', 'min:0'],
            'deduction_reason' => ['nullable', 'string', 'max:1000'],
        ]);

        $approved = $this->refunds->approve($refund, $request->user(), $validated);

        return ApiResponse::success($this->present($approved->fresh(['applicant'])), 'Refund approved.');
    }

    public function reject(Request $request, Refund $refund): JsonResponse
    {
        abort_unless($request->user()->can('refund.approve') || $request->user()->can('refund.review'), 403);
        $this->assertSameCompany($request, $refund);

        $validated = $request->validate(['reason' => ['required', 'string', 'max:1000']]);

        $rejected = $this->refunds->reject($refund, $validated['reason'], $request->user());

        return ApiResponse::success($this->present($rejected->fresh(['applicant'])), 'Refund rejected.');
    }

    public function cancel(Request $request, Refund $refund): JsonResponse
    {
        $this->assertSameCompany($request, $refund);

        // Whoever raised it may withdraw it; anyone else needs the permission.
        abort_unless(
            $request->user()->can('refund.cancel') || $refund->requested_by === $request->user()->getKey(),
            403,
        );

        $validated = $request->validate(['reason' => ['required', 'string', 'max:1000']]);

        $cancelled = $this->refunds->cancel($refund, $validated['reason'], $request->user());

        return ApiResponse::success($this->present($cancelled->fresh(['applicant'])), 'Refund cancelled.');
    }

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

        $validated = $request->validate([
            'payout_method' => ['required', Rule::in(PaymentMethod::values())],
            'beneficiary_name' => ['nullable', 'string', 'max:255'],
            'beneficiary_account' => ['nullable', 'string', 'max:120'],
            'transaction_reference' => ['nullable', 'string', 'max:100'],
            'paid_on' => ['nullable', 'date'],
        ]);

        $paid = $this->refunds->pay($refund, $request->user(), $validated);

        return ApiResponse::success(
            $this->present($paid->fresh(['applicant'])),
            "Refund {$paid->refund_no} paid out.",
        );
    }

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

    /** @return array<string, mixed> */
    private function present(Refund $refund): array
    {
        return [
            'id' => $refund->uuid,
            'refund_no' => $refund->refund_no,
            'status' => $refund->status->value,
            'status_label' => $refund->status->label(),
            'is_closed' => $refund->status->isClosed(),
            'is_editable' => $refund->status->isEditable(),
            'currency' => $refund->currency,
            'requested_amount' => (int) $refund->requested_amount,
            'approved_amount' => (int) $refund->approved_amount,
            'deduction_amount' => (int) $refund->deduction_amount,
            'payable_amount' => $refund->payable()->minorUnits,
            'paid_amount' => (int) $refund->paid_amount,
            'deduction_reason' => $refund->deduction_reason,
            'reason_code' => $refund->reason_code,
            'reason' => $refund->reason,
            'review_note' => $refund->review_note,
            'rejection_reason' => $refund->rejection_reason,
            'payout_method' => $refund->payout_method,
            'beneficiary_name' => $refund->beneficiary_name,
            'transaction_reference' => $refund->transaction_reference,
            'requested_at' => $refund->requested_at?->toIso8601String(),
            'requested_by' => $refund->relationLoaded('requestedBy') ? $refund->requestedBy?->name : null,
            'approved_at' => $refund->approved_at?->toIso8601String(),
            'paid_on' => $refund->paid_on?->toDateString(),
            'applicant' => $refund->relationLoaded('applicant') && $refund->applicant ? [
                'id' => $refund->applicant->uuid,
                'name' => $refund->applicant->full_name,
                'reference' => $refund->applicant->reference_no,
            ] : null,
        ];
    }
}
