<?php

namespace App\Services;

use App\Enums\AgentStatus;
use App\Enums\BookingStatus;
use App\Enums\CustomerStatus;
use App\Enums\PaymentMethod;
use App\Enums\PaymentType;
use App\Models\Agent;
use App\Models\Booking;
use App\Models\Customer;
use App\Models\Floor;
use App\Models\Unit;
use App\Models\UnitSubtype;
use App\Models\User;
use App\Support\BookingNumber;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class BookingService
{
    public function __construct(
        private BookingNumber $numbers,
        private BookingUnitLookup $lookup,
        private BookingDocumentService $documents,
    ) {}

    /**
     * @param  array{
     *     customer_id: int,
     *     agent_id: int|null,
     *     unit_id: int,
     *     unit_subtype_id: int,
     *     floor_id: int,
     *     unit_number: string,
     *     payment_type: PaymentType|null,
     *     down_payment: string|null,
     *     received_amount: string|null,
     *     payment_method: PaymentMethod|null,
     *     payment_reference: string|null,
     *     account_name: string|null,
     *     acknowledge_cross_type: bool
     * }  $data
     * @param  list<UploadedFile>  $proofs
     */
    public function create(User $actor, array $data, array $proofs = []): Booking
    {
        return DB::transaction(function () use ($actor, $data, $proofs): Booking {
            $unit = Unit::query()->findOrFail($data['unit_id']);
            $subtype = UnitSubtype::query()->findOrFail($data['unit_subtype_id']);
            $floor = Floor::query()->findOrFail($data['floor_id']);

            if ($subtype->unit_id !== $unit->id) {
                throw ValidationException::withMessages([
                    'unit_subtype_id' => 'This variant does not belong to the selected unit type.',
                ]);
            }

            $crediting = $data['payment_method'] instanceof PaymentMethod;
            $this->assertUnitNumberAvailable(
                $unit,
                $data['unit_number'],
                $data['acknowledge_cross_type'],
            );

            $customer = Customer::query()->findOrFail($data['customer_id']);

            if (! $customer->hasCompleteProfile()) {
                throw ValidationException::withMessages([
                    'customer_id' => 'Complete the customer profile before booking.',
                ]);
            }

            $agent = $this->resolveAgent($data['agent_id']);
            $status = $crediting ? BookingStatus::Confirmed : BookingStatus::Pending;

            $booking = Booking::query()->create([
                'number' => $this->numbers->unique(),
                'customer_id' => $customer->id,
                'agent_id' => $agent?->id,
                'unit_id' => $unit->id,
                'unit_subtype_id' => $subtype->id,
                'floor_id' => $floor->id,
                'unit_number' => trim($data['unit_number']),
                'payment_type' => $data['payment_type'],
                'down_payment' => $data['down_payment'],
                'received_amount' => $data['received_amount'],
                'payment_method' => $data['payment_method'],
                'payment_reference' => $data['payment_reference'],
                'account_name' => $data['account_name'],
                'status' => $status,
                'created_by' => $actor->id,
            ]);

            if ($crediting) {
                $this->promoteCustomer($booking, $agent);
                $this->cancelCompetingPending($booking);

                foreach ($proofs as $proof) {
                    $this->documents->store($actor, $booking, $proof);
                }
            }

            return $booking;
        });
    }

    /**
     * @param  array{
     *     payment_type: PaymentType,
     *     down_payment: string|null,
     *     received_amount: string,
     *     payment_method: PaymentMethod,
     *     payment_reference: string|null,
     *     account_name: string|null,
     *     acknowledge_cross_type: bool
     * }  $data
     * @param  list<UploadedFile>  $proofs
     */
    public function creditPayment(User $actor, Booking $booking, array $data, array $proofs = []): Booking
    {
        return DB::transaction(function () use ($actor, $booking, $data, $proofs): Booking {
            /** @var Booking $booking */
            $booking = Booking::query()->lockForUpdate()->findOrFail($booking->id);
            $booking->loadMissing(['unit', 'customer', 'agent']);

            if (! $booking->status->isPending()) {
                throw ValidationException::withMessages([
                    'booking' => $booking->status->isConfirmed()
                        ? 'This booking is already confirmed.'
                        : 'Cancelled bookings cannot receive payment.',
                ]);
            }

            $this->assertUnitNumberAvailable(
                $booking->unit,
                $booking->unit_number,
                true,
                $booking->id,
                false,
            );

            $booking->update([
                'payment_type' => $data['payment_type'],
                'down_payment' => $data['down_payment'],
                'received_amount' => $data['received_amount'],
                'payment_method' => $data['payment_method'],
                'payment_reference' => $data['payment_reference'],
                'account_name' => $data['account_name'],
                'status' => BookingStatus::Confirmed,
            ]);

            $this->promoteCustomer($booking, $booking->agent);
            $this->cancelCompetingPending($booking);

            foreach ($proofs as $proof) {
                $this->documents->store($actor, $booking, $proof);
            }

            return $booking->refresh();
        });
    }

    /**
     * @param  array{
     *     agent_id?: int|null,
     *     down_payment?: string|null,
     *     received_amount?: string|null,
     *     payment_method?: PaymentMethod|null,
     *     payment_reference?: string|null,
     *     account_name?: string|null
     * }  $data
     */
    public function update(Booking $booking, array $data): Booking
    {
        if ($booking->status->isCancelled()) {
            throw ValidationException::withMessages([
                'booking' => 'Cancelled bookings cannot be updated.',
            ]);
        }

        if ($booking->status->isPending()) {
            throw ValidationException::withMessages([
                'booking' => 'Credit payment on a pending booking instead of updating it.',
            ]);
        }

        if (array_key_exists('agent_id', $data)) {
            $data['agent_id'] = $this->resolveAgent($data['agent_id'])?->id;
        }

        $booking->update($data);

        return $booking->refresh();
    }

    public function cancel(Booking $booking): void
    {
        if ($booking->status->isCancelled()) {
            throw ValidationException::withMessages([
                'booking' => 'This booking is already cancelled.',
            ]);
        }

        $booking->update(['status' => BookingStatus::Cancelled]);
    }

    /**
     * @return array{
     *     unitNumber: string,
     *     sameType: array{bookingNumber: string, customerName: string, unitName: string, message: string}|null,
     *     otherTypes: list<array{bookingNumber: string, customerName: string, unitName: string, message: string}>,
     *     pendingSameType: list<array{bookingNumber: string, customerName: string, unitName: string, message: string}>,
     *     pendingMessage: string|null
     * }
     */
    public function lookup(int $unitId, string $unitNumber): array
    {
        $unit = Unit::query()->findOrFail($unitId);
        $conflicts = $this->lookup->find($unit, $unitNumber);
        $trimmed = trim($unitNumber);
        $pendingCount = $conflicts['pendingSameType']->count();

        return [
            'unitNumber' => $trimmed,
            'sameType' => $conflicts['sameType'] instanceof Booking
                ? $this->conflictPayload(
                    $conflicts['sameType'],
                    $this->lookup->sameTypeMessage($conflicts['sameType'], $trimmed),
                )
                : null,
            'otherTypes' => $conflicts['otherTypes']
                ->map(fn (Booking $booking): array => $this->conflictPayload(
                    $booking,
                    $this->lookup->otherTypeMessage($booking, $unit, $trimmed),
                ))
                ->all(),
            'pendingSameType' => $conflicts['pendingSameType']
                ->map(fn (Booking $booking): array => $this->conflictPayload(
                    $booking,
                    sprintf(
                        '%s is pending for %s (%s).',
                        $trimmed,
                        $this->customerName($booking),
                        $booking->number,
                    ),
                ))
                ->all(),
            'pendingMessage' => $pendingCount > 0
                ? $this->lookup->pendingMessage($unit, $trimmed, $pendingCount)
                : null,
        ];
    }

    private function assertUnitNumberAvailable(
        Unit $unit,
        string $unitNumber,
        bool $acknowledgeCrossType,
        ?int $ignoreBookingId = null,
        bool $requireCrossTypeAck = true,
    ): void {
        $conflicts = $this->lookup->find($unit, $unitNumber, $ignoreBookingId);
        $trimmed = trim($unitNumber);

        if ($conflicts['sameType'] instanceof Booking) {
            throw ValidationException::withMessages([
                'unit_number' => $this->lookup->sameTypeMessage($conflicts['sameType'], $trimmed),
            ]);
        }

        $other = $conflicts['otherTypes']->first();

        if ($requireCrossTypeAck && $other instanceof Booking && ! $acknowledgeCrossType) {
            throw ValidationException::withMessages([
                'acknowledge_cross_type' => $this->lookup->otherTypeMessage($other, $unit, $trimmed),
            ]);
        }
    }

    private function cancelCompetingPending(Booking $winner): void
    {
        Booking::query()
            ->where('status', BookingStatus::Pending)
            ->where('unit_id', $winner->unit_id)
            ->whereRaw('LOWER(unit_number) = ?', [$this->lookup->normalize($winner->unit_number)])
            ->whereKeyNot($winner->id)
            ->lockForUpdate()
            ->get()
            ->each(function (Booking $booking): void {
                $booking->update(['status' => BookingStatus::Cancelled]);
            });
    }

    private function promoteCustomer(Booking $booking, ?Agent $agent): void
    {
        $customer = $booking->customer()->firstOrFail();

        $customer->update([
            'status' => CustomerStatus::Customer,
            'agent_id' => $customer->agent_id ?? $agent?->id ?? $booking->agent_id,
        ]);
    }

    private function resolveAgent(?int $agentId): ?Agent
    {
        if ($agentId === null) {
            return null;
        }

        $agent = Agent::query()->findOrFail($agentId);

        if ($agent->status !== AgentStatus::Active) {
            throw ValidationException::withMessages([
                'agent_id' => 'This agent is not active.',
            ]);
        }

        return $agent;
    }

    /**
     * @return array{bookingNumber: string, customerName: string, unitName: string, message: string}
     */
    private function conflictPayload(Booking $booking, string $message): array
    {
        return [
            'bookingNumber' => $booking->number,
            'customerName' => $this->customerName($booking),
            'unitName' => $booking->unit->name,
            'message' => $message,
        ];
    }

    private function customerName(Booking $booking): string
    {
        return trim($booking->customer->first_name.' '.$booking->customer->last_name);
    }
}
