<?php

namespace App\Services;

use App\Enums\BookingStatus;
use App\Enums\ChargeBasis;
use App\Enums\InvoiceStatus;
use App\Enums\ReservationStatus;
use App\Exceptions\DocumentException;
use App\Models\Booking;
use App\Models\BookingItem;
use App\Models\BookingSpace;
use App\Models\Invoice;
use App\Models\Space;
use App\Support\PropertyDate;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

class BookingService
{
    public function __construct(
        private readonly DocumentNumberService $documentNumbers,
        private readonly AvailabilityService $availability,
        private readonly BookingStatusService $statusService,
        private readonly PricingService $pricing,
        private readonly SettingsService $settings,
        private readonly AuditService $audit,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function create(array $data): Booking
    {
        return DB::transaction(function () use ($data): Booking {
            $spaces = $data['spaces'] ?? [];
            $items = $data['items'] ?? [];
            unset($data['spaces'], $data['items']);

            $data['booking_number'] = $this->documentNumbers->next('booking');
            $data['status'] = $data['status'] ?? BookingStatus::Enquiry;

            $booking = Booking::query()->create($data);

            $this->syncSpaces($booking, $spaces);
            $this->syncItems($booking, $items);
            $this->recalculateTotals($booking);

            $booking->load(['customer', 'spaces.space', 'items']);

            $this->audit->log('booking.created', $booking, null, $booking->toArray());

            return $booking;
        });
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function update(Booking $booking, array $data): Booking
    {
        return DB::transaction(function () use ($booking, $data): Booking {
            $oldValues = $booking->toArray();
            $spaces = $data['spaces'] ?? null;
            $items = $data['items'] ?? null;
            unset($data['spaces'], $data['items']);

            $booking->update($data);

            if ($spaces !== null) {
                $booking->spaces()->delete();
                $this->syncSpaces($booking, $spaces);
            }

            if ($items !== null) {
                $booking->items()->delete();
                $this->syncItems($booking, $items);
            }

            $this->recalculateTotals($booking);
            $booking->refresh()->load(['customer', 'spaces.space', 'items']);

            $this->audit->log('booking.updated', $booking, $oldValues, $booking->toArray());

            return $booking;
        });
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function reschedule(Booking $booking, array $data): Booking
    {
        return DB::transaction(function () use ($booking, $data): Booking {
            $spaces = $data['spaces'] ?? [];
            unset($data['spaces']);

            $booking->update($data);

            foreach ($spaces as $spaceData) {
                $bookingSpace = $booking->spaces()->whereKey($spaceData['id'] ?? 0)->first();

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

                $setupStart = Carbon::parse($spaceData['setup_start']);
                $breakdownEnd = Carbon::parse($spaceData['breakdown_end']);

                $this->availability->assertAvailable(
                    (int) $bookingSpace->space_id,
                    $setupStart,
                    $breakdownEnd,
                    $bookingSpace->id,
                );

                $eventStart = Carbon::parse($spaceData['event_start']);
                $eventEnd = Carbon::parse($spaceData['event_end']);

                $billableDays = $this->pricing->calculateBillableDays($eventStart, $eventEnd);
                $billableHours = max(1, (int) ceil($eventStart->diffInMinutes($eventEnd) / 60));

                $bookingSpace->update([
                    'setup_start' => $setupStart,
                    'event_start' => $eventStart,
                    'event_end' => $eventEnd,
                    'breakdown_end' => $breakdownEnd,
                    'billable_days' => $billableDays,
                    'billable_hours' => $billableHours,
                ]);
            }

            $this->recalculateTotals($booking);
            $booking->refresh()->load(['customer', 'spaces.space', 'items']);

            $this->audit->log('booking.rescheduled', $booking, null, $booking->toArray());

            return $booking;
        });
    }

    public function confirm(Booking $booking, ?string $reason = null, string $auditAction = 'booking.confirmed'): Booking
    {
        return DB::transaction(function () use ($booking, $reason, $auditAction): Booking {
            $this->availability->expireHolds();

            $bookingSpaces = $booking->spaces()->lockForUpdate()->get();

            foreach ($bookingSpaces as $bookingSpace) {
                $this->availability->assertAvailable(
                    (int) $bookingSpace->space_id,
                    $bookingSpace->setup_start,
                    $bookingSpace->breakdown_end,
                    $bookingSpace->id,
                );

                $bookingSpace->update([
                    'reservation_status' => ReservationStatus::Confirmed,
                    'hold_expires_at' => null,
                ]);
            }

            $this->statusService->transition($booking, BookingStatus::Confirmed, $reason);

            $booking->refresh()->load(['customer', 'spaces.space', 'items', 'invoices.payments']);

            app(\App\Services\InvoiceService::class)->refreshStatusesForBooking($booking);

            $this->audit->log($auditAction, $booking, null, $booking->toArray(), $reason);

            return $booking;
        });
    }

    public function placeHold(Booking $booking): Booking
    {
        return DB::transaction(function () use ($booking): Booking {
            $holdHours = (int) $this->settings->get('default_hold_duration_hours', 48);
            $expiresAt = now()->addHours($holdHours);

            foreach ($booking->spaces as $bookingSpace) {
                $this->availability->assertAvailable(
                    (int) $bookingSpace->space_id,
                    $bookingSpace->setup_start,
                    $bookingSpace->breakdown_end,
                    $bookingSpace->id,
                );

                $bookingSpace->update([
                    'reservation_status' => ReservationStatus::Provisional,
                    'hold_expires_at' => $expiresAt,
                ]);
            }

            if ($booking->status === BookingStatus::Enquiry) {
                $this->statusService->transition($booking, BookingStatus::Provisional);
            } else {
                $booking->update(['status' => BookingStatus::Provisional]);
            }

            $booking->refresh()->load(['customer', 'spaces.space', 'items']);

            $this->audit->log('booking.hold_placed', $booking, null, [
                'hold_expires_at' => $expiresAt->toIso8601String(),
            ]);

            return $booking;
        });
    }

    public function cancel(Booking $booking, ?string $reason = null): Booking
    {
        return DB::transaction(function () use ($booking, $reason): Booking {
            $oldValues = $booking->toArray();

            foreach ($booking->spaces as $bookingSpace) {
                $bookingSpace->update(['reservation_status' => ReservationStatus::Cancelled]);
            }

            $this->statusService->transition($booking, BookingStatus::Cancelled, $reason);

            $booking->refresh()->load(['customer', 'spaces.space', 'items']);

            $this->audit->log('booking.cancelled', $booking, $oldValues, $booking->toArray(), $reason);

            return $booking;
        });
    }

    public function duplicate(Booking $booking): Booking
    {
        $booking->load(['spaces', 'items']);

        $data = $booking->only([
            'customer_id',
            'contact_name',
            'contact_phone',
            'contact_email',
            'event_name',
            'event_type_id',
            'description',
            'event_start',
            'event_end',
            'guest_count',
            'seating_arrangement',
            'coordinator_id',
            'customer_notes',
            'internal_notes',
            'catering_requirements',
            'av_requirements',
            'decorations_requirements',
            'signage_requirements',
            'security_requirements',
            'other_requirements',
        ]);

        $data['spaces'] = $booking->spaces->map(fn (BookingSpace $space) => [
            'space_id' => $space->space_id,
            'setup_start' => $space->setup_start->toDateTimeString(),
            'event_start' => $space->event_start->toDateTimeString(),
            'event_end' => $space->event_end->toDateTimeString(),
            'breakdown_end' => $space->breakdown_end->toDateTimeString(),
            'seating_layout' => $space->seating_layout,
            'guest_count' => $space->guest_count,
            'unit_price' => $space->unit_price,
            'charge_basis' => $space->charge_basis->value,
            'notes' => $space->notes,
        ])->all();

        $data['items'] = $booking->items->map(fn (BookingItem $item) => [
            'package_id' => $item->package_id,
            'product_id' => $item->product_id,
            'booking_space_id' => $item->booking_space_id,
            'description' => $item->description,
            'quantity' => $item->quantity,
            'unit_price' => $item->unit_price,
            'charge_basis' => $item->charge_basis->value,
            'guest_count' => $item->guest_count,
            'discount_amount' => $item->discount_amount,
            'tax_rate' => $item->tax_rate,
            'sort_order' => $item->sort_order,
            'is_complimentary' => $item->is_complimentary,
        ])->all();

        $duplicate = $this->create($data);

        $this->audit->log('booking.duplicated', $duplicate, null, [
            'source_booking_id' => $booking->id,
            'source_booking_number' => $booking->booking_number,
        ]);

        return $duplicate;
    }

    /**
     * Create a provisional booking from invoice/quotation form payload (multi-space).
     *
     * @param  array<string, mixed>  $data
     */
    public function createFromDocumentPayload(array $data): Booking
    {
        $spaces = array_values(array_filter(
            $data['spaces'] ?? [],
            fn ($space) => ! empty($space['space_id']) && ! empty($space['event_start']) && ! empty($space['event_end']),
        ));

        if ($spaces === []) {
            throw new DocumentException('At least one space with event start and end is required.');
        }

        $starts = collect($spaces)->pluck('event_start')->filter()->sort()->values();
        $ends = collect($spaces)->pluck('event_end')->filter()->sort()->values();

        $guestCount = (int) ($data['guest_count'] ?? 0);
        if ($guestCount <= 0) {
            $guestCount = (int) collect($spaces)->max(fn ($space) => (int) ($space['guest_count'] ?? 0));
        }

        return $this->create([
            'customer_id' => $data['customer_id'],
            'contact_name' => $data['contact_name'] ?? null,
            'contact_phone' => $data['contact_phone'] ?? null,
            'contact_email' => $data['contact_email'] ?? null,
            'event_name' => $data['event_name'] ?? 'Event Reservation',
            'event_type_id' => $data['event_type_id'] ?? null,
            'description' => $data['description'] ?? null,
            'event_start' => $data['event_start'] ?? $starts->first(),
            'event_end' => $data['event_end'] ?? $ends->last(),
            'guest_count' => $guestCount,
            'seating_arrangement' => $data['seating_arrangement'] ?? null,
            'coordinator_id' => $data['coordinator_id'] ?? null,
            'customer_notes' => $data['customer_notes'] ?? null,
            'internal_notes' => $data['internal_notes'] ?? null,
            'status' => BookingStatus::Provisional,
            'spaces' => $spaces,
        ]);
    }

    /**
     * Confirm booking/reservations when an invoice reaches Partially Paid or Paid.
     */
    public function confirmFromInvoicePayment(Booking $booking): Booking
    {
        if ($booking->status === BookingStatus::Confirmed) {
            return $booking;
        }

        if (in_array($booking->status, [BookingStatus::Cancelled, BookingStatus::Completed, BookingStatus::NoShow], true)) {
            return $booking;
        }

        return $this->confirm(
            $booking,
            'Auto-confirmed after invoice deposit/payment.',
            'booking.auto_confirmed_from_payment',
        );
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function createProvisionalWithSpace(array $data): Booking
    {
        return $this->create([
            'customer_id' => $data['customer_id'],
            'event_name' => $data['event_name'] ?? 'Event Reservation',
            'event_start' => $data['event_start'],
            'event_end' => $data['event_end'],
            'guest_count' => $data['guest_count'] ?? 0,
            'status' => BookingStatus::Provisional,
            'spaces' => [[
                'space_id' => $data['space_id'],
                'event_start' => $data['event_start'],
                'event_end' => $data['event_end'],
                'setup_start' => $data['setup_start'] ?? null,
                'breakdown_end' => $data['breakdown_end'] ?? null,
                'guest_count' => $data['guest_count'] ?? 0,
            ]],
        ]);
    }

    public function recalculateTotals(Booking $booking): void
    {
        $booking->load(['spaces', 'items']);

        $lines = [];

        foreach ($booking->spaces as $space) {
            $lines[] = [
                'unit_price' => $space->unit_price,
                'quantity' => 1,
                'charge_basis' => $space->charge_basis,
                'guest_count' => $space->guest_count ?: $booking->guest_count,
                'billable_days' => $space->billable_days,
                'billable_hours' => $space->billable_hours,
                'tax_rate' => $this->settings->get('default_tax', 0),
            ];

            $amounts = $this->pricing->calculateLineAmount($lines[array_key_last($lines)]);
            $space->update(['line_total' => $amounts['total']]);
        }

        foreach ($booking->items as $item) {
            $chargeBasis = $item->is_complimentary
                ? ChargeBasis::Complimentary
                : $item->charge_basis;

            $line = [
                'unit_price' => $item->unit_price,
                'quantity' => $item->quantity,
                'charge_basis' => $chargeBasis,
                'guest_count' => $item->guest_count ?: $booking->guest_count,
                'billable_days' => $item->billable_days ?: $booking->billable_days,
                'billable_hours' => $item->billable_hours ?: $booking->billable_hours,
                'discount_amount' => $item->discount_amount,
                'tax_rate' => $item->tax_rate,
            ];

            $amounts = $this->pricing->calculateLineAmount($line);
            $item->update([
                'tax_amount' => $amounts['tax'],
                'line_total' => $amounts['total'],
            ]);

            $lines[] = $line;
        }

        $totals = $this->pricing->calculateDocumentTotals($lines, [
            'overall_discount_amount' => $booking->discount_amount,
            'service_charge_rate' => $this->settings->get('default_service_charge', 0),
            'amount_paid' => $booking->amount_paid,
        ]);

        $billableDays = $this->pricing->calculateBillableDays(
            $booking->event_start,
            $booking->event_end,
            PropertyDate::timezone(),
        );

        $billableHours = max(1, (int) ceil($booking->event_start->diffInMinutes($booking->event_end) / 60));

        $booking->update([
            'billable_days' => $billableDays,
            'billable_hours' => $billableHours,
            'subtotal' => $totals['subtotal'],
            'discount_amount' => $totals['discount'],
            'service_charge_amount' => $totals['service_charge'],
            'tax_amount' => $totals['tax'],
            'grand_total' => $totals['grand_total'],
            'outstanding_balance' => $totals['outstanding'],
        ]);

        // Prefer linked invoice as financial source of truth when present.
        $this->syncFinancialsFromPrimaryInvoice($booking->fresh());
    }

    /**
     * Copy financial totals from the primary (latest non-void) linked invoice onto the booking
     * so reservation Financial Summary matches the invoice.
     */
    public function syncFinancialsFromInvoice(Invoice $invoice): void
    {
        if (! $invoice->booking_id || $invoice->status === InvoiceStatus::Void) {
            return;
        }

        $booking = Booking::query()->find($invoice->booking_id);
        if ($booking === null) {
            return;
        }

        $this->syncFinancialsFromPrimaryInvoice($booking);
    }

    public function syncFinancialsFromPrimaryInvoice(Booking $booking): void
    {
        $invoice = $booking->invoices()
            ->where('status', '!=', InvoiceStatus::Void->value)
            ->orderByDesc('id')
            ->first();

        if ($invoice === null) {
            return;
        }

        $old = $booking->only([
            'subtotal',
            'discount_amount',
            'service_charge_amount',
            'tax_amount',
            'grand_total',
            'amount_paid',
            'outstanding_balance',
        ]);

        $new = [
            'subtotal' => $invoice->subtotal,
            'discount_amount' => $invoice->discount_amount,
            'service_charge_amount' => $invoice->service_charge_amount,
            'tax_amount' => $invoice->tax_amount,
            'grand_total' => $invoice->grand_total,
            'amount_paid' => $invoice->amount_paid,
            'outstanding_balance' => $invoice->outstanding_balance,
        ];

        $changed = false;
        foreach ($new as $key => $value) {
            if (round((float) ($old[$key] ?? 0), 2) !== round((float) $value, 2)) {
                $changed = true;
                break;
            }
        }

        if (! $changed) {
            return;
        }

        $booking->update($new);

        $this->audit->log(
            'booking.financials_synced',
            $booking,
            $old,
            array_merge($new, [
                'invoice_id' => $invoice->id,
                'invoice_number' => $invoice->invoice_number,
            ]),
            'Booking financial summary synced from invoice '.$invoice->invoice_number,
        );
    }

    /**
     * @param  list<array<string, mixed>>  $spaces
     */
    private function syncSpaces(Booking $booking, array $spaces): void
    {
        foreach ($spaces as $spaceData) {
            $space = Space::query()->findOrFail($spaceData['space_id']);

            $eventStart = Carbon::parse($spaceData['event_start'] ?? $booking->event_start);
            $eventEnd = Carbon::parse($spaceData['event_end'] ?? $booking->event_end);

            $setupStart = isset($spaceData['setup_start'])
                ? Carbon::parse($spaceData['setup_start'])
                : $eventStart->copy()->subMinutes($space->setup_duration_minutes);

            $breakdownEnd = isset($spaceData['breakdown_end'])
                ? Carbon::parse($spaceData['breakdown_end'])
                : $eventEnd->copy()->addMinutes($space->breakdown_duration_minutes);

            $billableDays = $this->pricing->calculateBillableDays($eventStart, $eventEnd);
            $billableHours = max(1, (int) ceil($eventStart->diffInMinutes($eventEnd) / 60));

            $unitPrice = $spaceData['unit_price'] ?? $space->full_day_rate ?? $space->standard_rate ?? 0;
            $chargeBasis = $spaceData['charge_basis'] ?? ChargeBasis::PerReservationDay->value;

            $lineAmounts = $this->pricing->calculateLineAmount([
                'unit_price' => $unitPrice,
                'quantity' => 1,
                'charge_basis' => $chargeBasis,
                'guest_count' => $spaceData['guest_count'] ?? $booking->guest_count,
                'billable_days' => $billableDays,
                'billable_hours' => $billableHours,
                'tax_rate' => $this->settings->get('default_tax', 0),
            ]);

            BookingSpace::query()->create([
                'booking_id' => $booking->id,
                'space_id' => $space->id,
                'setup_start' => $setupStart,
                'event_start' => $eventStart,
                'event_end' => $eventEnd,
                'breakdown_end' => $breakdownEnd,
                'seating_layout' => $spaceData['seating_layout'] ?? null,
                'guest_count' => $spaceData['guest_count'] ?? $booking->guest_count,
                'unit_price' => $unitPrice,
                'charge_basis' => $chargeBasis,
                'billable_days' => $billableDays,
                'billable_hours' => $billableHours,
                'line_total' => $lineAmounts['total'],
                'reservation_status' => ReservationStatus::Provisional,
                'notes' => $spaceData['notes'] ?? null,
            ]);
        }
    }

    /**
     * @param  list<array<string, mixed>>  $items
     */
    private function syncItems(Booking $booking, array $items): void
    {
        foreach ($items as $index => $itemData) {
            $chargeBasis = ($itemData['is_complimentary'] ?? false)
                ? ChargeBasis::Complimentary
                : ($itemData['charge_basis'] ?? ChargeBasis::PerUnit->value);

            $lineAmounts = $this->pricing->calculateLineAmount([
                'unit_price' => $itemData['unit_price'] ?? 0,
                'quantity' => $itemData['quantity'] ?? 1,
                'charge_basis' => $chargeBasis,
                'guest_count' => $itemData['guest_count'] ?? $booking->guest_count,
                'billable_days' => $itemData['billable_days'] ?? $booking->billable_days,
                'billable_hours' => $itemData['billable_hours'] ?? $booking->billable_hours,
                'discount_amount' => $itemData['discount_amount'] ?? 0,
                'tax_rate' => $itemData['tax_rate'] ?? $this->settings->get('default_tax', 0),
            ]);

            BookingItem::query()->create([
                'booking_id' => $booking->id,
                'package_id' => $itemData['package_id'] ?? null,
                'product_id' => $itemData['product_id'] ?? null,
                'booking_space_id' => $itemData['booking_space_id'] ?? null,
                'description' => $itemData['description'],
                'quantity' => $itemData['quantity'] ?? 1,
                'unit_price' => $itemData['unit_price'] ?? 0,
                'charge_basis' => $itemData['charge_basis'] ?? ChargeBasis::PerUnit->value,
                'guest_count' => $itemData['guest_count'] ?? $booking->guest_count,
                'billable_days' => $itemData['billable_days'] ?? $booking->billable_days,
                'billable_hours' => $itemData['billable_hours'] ?? $booking->billable_hours,
                'discount_amount' => $itemData['discount_amount'] ?? 0,
                'tax_rate' => $itemData['tax_rate'] ?? $this->settings->get('default_tax', 0),
                'tax_amount' => $lineAmounts['tax'],
                'line_total' => $lineAmounts['total'],
                'sort_order' => $itemData['sort_order'] ?? $index,
                'is_complimentary' => $itemData['is_complimentary'] ?? false,
            ]);
        }
    }
}
