<?php

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

use App\Domain\Expenses\Enums\BillStatus;
use App\Domain\Expenses\Enums\VendorType;
use App\Domain\Expenses\Models\Vendor;
use App\Domain\Expenses\Models\VendorBill;
use App\Domain\Expenses\Services\PayableService;
use App\Http\Controllers\Controller;
use App\Support\ApiResponse;
use App\Support\Money\Money;
use App\Support\Sequences\SequenceGenerator;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

/** Vendors and accounts payable (§43, §44). */
class VendorController extends Controller
{
    public function __construct(
        private readonly PayableService $payables,
        private readonly SequenceGenerator $sequences,
    ) {}

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

        $vendors = Vendor::where('company_id', $request->user()->company_id)
            ->when($request->filled('type'), fn ($q) => $q->where('vendor_type', $request->string('type')))
            ->when($request->boolean('active_only'), fn ($q) => $q->active())
            ->when($request->filled('search'), fn ($q) => $q->where(
                fn ($inner) => $inner
                    ->where('name', 'like', '%'.$request->string('search').'%')
                    ->orWhere('vendor_code', 'like', '%'.$request->string('search').'%'),
            ))
            ->orderBy('name')
            ->paginate($request->integer('per_page', 25));

        return ApiResponse::success(
            $vendors->through(fn (Vendor $vendor) => $this->presentVendor($vendor)),
            meta: [
                'types' => array_map(
                    fn (VendorType $type) => ['value' => $type->value, 'label' => $type->label()],
                    VendorType::cases(),
                ),
            ],
        );
    }

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

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'vendor_type' => ['required', Rule::in(VendorType::values())],
            'contact_person' => ['nullable', 'string', 'max:255'],
            'phone' => ['nullable', 'string', 'max:32'],
            'email' => ['nullable', 'email', 'max:255'],
            'address' => ['nullable', 'string', 'max:1000'],
            'tax_identifier' => ['nullable', 'string', 'max:60'],
            'bank_name' => ['nullable', 'string', 'max:255'],
            'bank_account_name' => ['nullable', 'string', 'max:255'],
            'bank_account_number' => ['nullable', 'string', 'max:64'],
            'payment_terms_days' => ['nullable', 'integer', 'min:0', 'max:365'],
            'currency' => ['required', 'string', 'size:3'],
            'notes' => ['nullable', 'string', 'max:2000'],
        ]);

        $vendor = Vendor::create(array_merge($validated, [
            'vendor_code' => $this->nextCode($request->user()->company_id),
            'company_id' => $request->user()->company_id,
            'branch_id' => $request->user()->branch_id,
            'created_by' => $request->user()->getKey(),
        ]));

        return ApiResponse::success($this->presentVendor($vendor), 'Vendor added.', status: 201);
    }

    public function update(Request $request, Vendor $vendor): JsonResponse
    {
        abort_unless($request->user()->can('vendor.update'), 403);
        abort_unless($vendor->company_id === $request->user()->company_id, 403);

        $validated = $request->validate([
            'name' => ['sometimes', 'string', 'max:255'],
            'vendor_type' => ['sometimes', Rule::in(VendorType::values())],
            'status' => ['sometimes', Rule::in(['active', 'inactive', 'blacklisted'])],
            'contact_person' => ['nullable', 'string', 'max:255'],
            'phone' => ['nullable', 'string', 'max:32'],
            'email' => ['nullable', 'email', 'max:255'],
            'payment_terms_days' => ['nullable', 'integer', 'min:0', 'max:365'],
            'notes' => ['nullable', 'string', 'max:2000'],
        ]);

        $vendor->update($validated);

        return ApiResponse::success($this->presentVendor($vendor->fresh()), 'Vendor updated.');
    }

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

        $bills = VendorBill::query()
            ->with('vendor:id,uuid,name,vendor_code')
            ->where('company_id', $request->user()->company_id)
            ->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
            ->when($request->boolean('outstanding'), fn ($q) => $q->outstanding())
            ->when($request->filled('vendor'), fn ($q) => $q->whereHas(
                'vendor',
                fn ($v) => $v->where('uuid', $request->string('vendor')),
            ))
            ->latest('bill_date')
            ->latest('id')
            ->paginate($request->integer('per_page', 25));

        return ApiResponse::success(
            $bills->through(fn (VendorBill $bill) => $this->presentBill($bill)),
            meta: [
                'statuses' => array_map(
                    fn (BillStatus $status) => ['value' => $status->value, 'label' => $status->label()],
                    BillStatus::cases(),
                ),
            ],
        );
    }

    public function storeBill(Request $request, Vendor $vendor): JsonResponse
    {
        abort_unless($request->user()->can('vendor.bill_manage'), 403);
        abort_unless($vendor->company_id === $request->user()->company_id, 403);

        $validated = $request->validate([
            'vendor_invoice_no' => ['nullable', 'string', 'max:100'],
            'bill_date' => ['nullable', 'date'],
            'due_date' => ['nullable', 'date'],
            'currency' => ['nullable', 'string', 'size:3'],
            'applicant_id' => ['nullable', 'integer', 'exists:applicants,id'],
            'notes' => ['nullable', 'string', 'max:2000'],
            'lines' => ['required', 'array', 'min:1', 'max:50'],
            'lines.*.account_code' => ['required', 'string', 'max:20'],
            'lines.*.description' => ['required', 'string', 'max:255'],
            'lines.*.quantity' => ['nullable', 'integer', 'min:1', 'max:1000'],
            'lines.*.unit_amount' => ['required', 'integer', 'min:0'],
            'lines.*.tax_amount' => ['nullable', 'integer', 'min:0'],
        ]);

        $bill = $this->payables->createBill($vendor, $validated['lines'], $request->user(), $validated);

        return ApiResponse::success(
            $this->presentBill($bill->fresh(['vendor', 'lines'])),
            'Bill recorded.',
            status: 201,
        );
    }

    public function approveBill(Request $request, VendorBill $bill): JsonResponse
    {
        abort_unless($request->user()->can('vendor.bill_manage'), 403);
        abort_unless($bill->company_id === $request->user()->company_id, 403);

        $approved = $this->payables->approve($bill, $request->user());

        return ApiResponse::success(
            $this->presentBill($approved->fresh(['vendor', 'lines'])),
            'Bill approved and posted.',
        );
    }

    public function payBill(Request $request, VendorBill $bill): JsonResponse
    {
        abort_unless($request->user()->can('vendor.pay'), 403);
        abort_unless($bill->company_id === $request->user()->company_id, 403);

        $validated = $request->validate([
            'amount' => ['required', 'integer', 'min:1'],
            'currency' => ['required', 'string', 'size:3'],
            'method' => ['nullable', 'string', 'max:30'],
            'paid_from_code' => ['nullable', 'string', 'max:20'],
            'transaction_reference' => ['nullable', 'string', 'max:100'],
            'paid_on' => ['nullable', 'date'],
            'notes' => ['nullable', 'string', 'max:1000'],
        ]);

        $this->payables->payBill(
            $bill,
            Money::of($validated['amount'], $validated['currency']),
            $validated,
            $request->user(),
        );

        return ApiResponse::success(
            $this->presentBill($bill->fresh(['vendor', 'lines'])),
            'Vendor paid.',
        );
    }

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

        return ApiResponse::success($this->payables->aging($request->user()->company_id));
    }

    private function nextCode(int $companyId): string
    {
        return $this->sequences->nextUnique(
            'vendor',
            'VEN',
            [],
            5,
            fn (string $code) => Vendor::where('company_id', $companyId)
                ->where('vendor_code', $code)
                ->exists(),
        );
    }

    /** @return array<string, mixed> */
    private function presentVendor(Vendor $vendor): array
    {
        return [
            'id' => $vendor->uuid,
            'vendor_code' => $vendor->vendor_code,
            'name' => $vendor->name,
            'vendor_type' => $vendor->vendor_type->value,
            'type_label' => $vendor->vendor_type->label(),
            'status' => $vendor->status,
            'contact_person' => $vendor->contact_person,
            'phone' => $vendor->phone,
            'email' => $vendor->email,
            'payment_terms_days' => (int) $vendor->payment_terms_days,
            'currency' => $vendor->currency,
            'outstanding_amount' => $vendor->outstanding()->minorUnits,
            'notes' => $vendor->notes,
        ];
    }

    /** @return array<string, mixed> */
    private function presentBill(VendorBill $bill): array
    {
        return [
            'id' => $bill->uuid,
            'bill_no' => $bill->bill_no,
            'vendor_invoice_no' => $bill->vendor_invoice_no,
            'status' => $bill->status->value,
            'status_label' => $bill->status->label(),
            'is_editable' => $bill->status->isEditable(),
            'accepts_payment' => $bill->status->acceptsPayment(),
            'currency' => $bill->currency,
            'subtotal_amount' => (int) $bill->subtotal_amount,
            'tax_amount' => (int) $bill->tax_amount,
            'total_amount' => (int) $bill->total_amount,
            'paid_amount' => (int) $bill->paid_amount,
            'outstanding_amount' => $bill->outstanding()->minorUnits,
            'bill_date' => $bill->bill_date?->toDateString(),
            'due_date' => $bill->due_date?->toDateString(),
            'is_overdue' => $bill->isOverdue(),
            'days_overdue' => $bill->daysOverdue(),
            'notes' => $bill->notes,
            'vendor' => $bill->relationLoaded('vendor') && $bill->vendor ? [
                'id' => $bill->vendor->uuid,
                'name' => $bill->vendor->name,
                'code' => $bill->vendor->vendor_code,
            ] : null,
            'lines' => $bill->relationLoaded('lines')
                ? $bill->lines->map(fn ($line) => [
                    'description' => $line->description,
                    'quantity' => (int) $line->quantity,
                    'unit_amount' => (int) $line->unit_amount,
                    'tax_amount' => (int) $line->tax_amount,
                    'line_total' => (int) $line->line_total,
                ])
                : [],
        ];
    }
}
