<?php

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

use App\Domain\Expenses\Enums\ExpenseStatus;
use App\Domain\Expenses\Models\Expense;
use App\Domain\Expenses\Models\ExpenseCategory;
use App\Domain\Expenses\Services\ApprovalMatrix;
use App\Domain\Expenses\Services\ExpenseService;
use App\Http\Controllers\Controller;
use App\Support\ApiResponse;
use App\Support\Money\Money;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

/** Agency spending (§39, §40). */
class ExpenseController extends Controller
{
    public function __construct(
        private readonly ExpenseService $expenses,
        private readonly ApprovalMatrix $matrix,
    ) {}

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

        $expenses = Expense::query()
            ->with(['category:id,uuid,name', 'vendor:id,uuid,name', '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_payment'), fn ($q) => $q->awaitingPayment())
            ->when($request->filled('search'), fn ($q) => $q->where(
                fn ($inner) => $inner
                    ->where('expense_no', 'like', '%'.$request->string('search').'%')
                    ->orWhere('purpose', 'like', '%'.$request->string('search').'%'),
            ))
            ->latest('expense_date')
            ->latest('id')
            ->paginate($request->integer('per_page', 25));

        return ApiResponse::success(
            $expenses->through(fn (Expense $expense) => $this->present($expense)),
            meta: [
                'statuses' => array_map(
                    fn (ExpenseStatus $status) => ['value' => $status->value, 'label' => $status->label()],
                    ExpenseStatus::cases(),
                ),
                'approval_matrix' => $this->matrix->forCompany($request->user()->company_id),
            ],
        );
    }

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

        $categories = ExpenseCategory::where('company_id', $request->user()->company_id)
            ->active()
            ->with('ledgerAccount:id,code,name')
            ->orderBy('name')
            ->get();

        return ApiResponse::success(
            $categories->map(fn (ExpenseCategory $category) => [
                'id' => $category->uuid,
                'code' => $category->code,
                'name' => $category->name,
                'requires_receipt' => $category->requires_receipt,
                'account_code' => $category->ledgerAccount?->code,
                'account_name' => $category->ledgerAccount?->name,
            ]),
        );
    }

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

        $validated = $request->validate([
            'category_id' => ['required', 'uuid'],
            'amount' => ['required', 'integer', 'min:1'],
            'tax_amount' => ['nullable', 'integer', 'min:0'],
            'currency' => ['required', 'string', 'size:3'],
            'purpose' => ['required', 'string', 'max:1000'],
            'expense_date' => ['nullable', 'date'],
            'vendor_id' => ['nullable', 'integer', 'exists:vendors,id'],
            'applicant_id' => ['nullable', 'integer', 'exists:applicants,id'],
            'cost_center' => ['nullable', 'string', 'max:60'],
            'notes' => ['nullable', 'string', 'max:1000'],
        ]);

        $category = ExpenseCategory::where('uuid', $validated['category_id'])
            ->where('company_id', $request->user()->company_id)
            ->firstOrFail();

        $expense = $this->expenses->request(
            $category,
            Money::of($validated['amount'], $validated['currency']),
            $request->user(),
            $validated,
        );

        return ApiResponse::success(
            $this->present($expense->fresh(['category'])),
            'Expense submitted for approval.',
            status: 201,
        );
    }

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

        $approved = $this->expenses->approve($expense, $request->user());

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

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

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

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

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

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

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

        $paid = $this->expenses->pay($expense, $validated, $request->user());

        return ApiResponse::success($this->present($paid->fresh(['category'])), 'Expense paid and posted.');
    }

    public function cancel(Request $request, Expense $expense): JsonResponse
    {
        abort_unless($request->user()->can('expense.create'), 403);
        $this->assertSameCompany($request, $expense);

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

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

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

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

    /** @return array<string, mixed> */
    private function present(Expense $expense): array
    {
        return [
            'id' => $expense->uuid,
            'expense_no' => $expense->expense_no,
            'status' => $expense->status->value,
            'status_label' => $expense->status->label(),
            'is_editable' => $expense->status->isEditable(),
            'currency' => $expense->currency,
            'amount' => (int) $expense->amount,
            'tax_amount' => (int) $expense->tax_amount,
            'total_amount' => (int) $expense->total_amount,
            'expense_date' => $expense->expense_date?->toDateString(),
            'purpose' => $expense->purpose,
            'cost_center' => $expense->cost_center,
            'approver_role' => $expense->approver_role,
            'rejection_reason' => $expense->rejection_reason,
            'payment_method' => $expense->payment_method,
            'payment_reference' => $expense->payment_reference,
            'paid_on' => $expense->paid_on?->toDateString(),
            'requested_by' => $expense->relationLoaded('requestedBy') ? $expense->requestedBy?->name : null,
            'category' => $expense->relationLoaded('category') && $expense->category ? [
                'id' => $expense->category->uuid,
                'name' => $expense->category->name,
            ] : null,
            'vendor' => $expense->relationLoaded('vendor') && $expense->vendor ? [
                'id' => $expense->vendor->uuid,
                'name' => $expense->vendor->name,
            ] : null,
        ];
    }
}
