<?php

namespace App\Controllers;

use App\Models\BookingModel;
use App\Models\BookingServiceItemModel;
use App\Models\CorporateAccountModel;
use App\Models\FolioModel;
use App\Models\GuestPreferenceModel;
use App\Models\GuestModel;
use App\Models\HotelServiceModel;
use App\Models\PaymentAttemptModel;
use App\Models\RatePlanModel;
use App\Models\RoomModel;
use Config\Lookups;
use RuntimeException;

/**
 * Steps 3-7 of the redesigned New Booking wizard (Guest Details ->
 * Reservation Details -> Add Services -> Payment -> Confirmation) — see
 * the migration/model doc comments this session for what's genuinely new
 * (Services/Extras, adults/children, payment collected at booking time)
 * versus reused as-is (guest search, rate resolution, the existing
 * deposit-via-Hubtel-link flow, SMS confirmation).
 *
 * Steps 1-2 (Search Availability, Select Room) stay on BookingController
 * — dates/room-type filtering doesn't need to survive across steps the
 * way guest/services/payment choices do, so they're carried as plain GET
 * params exactly as before. Once a room is actually selected, this
 * controller takes over and keeps everything in session
 * (self::SESSION_KEY) until the final Payment step's submit — that's the
 * one moment a booking (and its guest, if new, and its service
 * selections) actually gets written to the database. Every step before
 * that is just building up session state; abandoning the wizard midway
 * (closing the tab, starting a different search) leaves nothing behind
 * in the database.
 */
class BookingWizardController extends BaseController
{
    private const SESSION_KEY = 'booking_wizard';

    private function wizard(): array
    {
        return session(self::SESSION_KEY) ?? [];
    }

    private function saveWizard(array $data): void
    {
        session()->set(self::SESSION_KEY, array_merge($this->wizard(), $data));
    }

    private function clearWizard(): void
    {
        session()->remove(self::SESSION_KEY);
    }

    /**
     * Step 2 -> 3. This is where the session-backed part of the wizard
     * begins — re-checks the room is still actually free (it was only
     * confirmed available at the moment Step 2 was rendered, which could
     * be minutes ago).
     */
    public function selectRoom()
    {
        $roomId   = (int) $this->request->getPost('room_id');
        $checkIn  = $this->request->getPost('check_in');
        $checkOut = $this->request->getPost('check_out');

        $room = (new RoomModel())->withType()->where('rooms.id', $roomId)->first();

        if ($room === null || ! $checkIn || ! $checkOut) {
            return redirect()->to('bookings/new')->with('error', 'Select a room and dates first.');
        }

        if ((new BookingModel())->hasOverlap($roomId, $checkIn, $checkOut)) {
            return redirect()->to('bookings/search?check_in=' . $checkIn . '&check_out=' . $checkOut)
                ->with('error', 'That room was just booked by someone else — pick another.');
        }

        $resolvedRate = (new RatePlanModel())->resolveRate($room->room_type_id, $checkIn, $room->base_rate);

        $this->clearWizard(); // a fresh room selection always starts a fresh wizard
        $this->saveWizard([
            'room_id'      => $roomId,
            'check_in'     => $checkIn,
            'check_out'    => $checkOut,
            'rate_applied' => $resolvedRate,
            'adults'       => 1,
            'children'     => 0,
        ]);

        return redirect()->to('bookings/wizard/guest');
    }

    private function requireRoom()
    {
        if (empty($this->wizard()['room_id'])) {
            return redirect()->to('bookings/new')->with('error', 'Start a new booking search first.');
        }

        return null;
    }

    private function requireGuest()
    {
        if ($redirect = $this->requireRoom()) {
            return $redirect;
        }

        $w = $this->wizard();

        if (empty($w['guest_id']) && empty($w['new_guest'])) {
            return redirect()->to('bookings/wizard/guest')->with('error', 'Add the guest first.');
        }

        return null;
    }

    private function roomForWizard(array $w): ?object
    {
        return (new RoomModel())->withType()->where('rooms.id', $w['room_id'])->first();
    }

    private function nights(array $w): int
    {
        return (int) max(1, (strtotime($w['check_out']) - strtotime($w['check_in'])) / 86400);
    }

    // ---- Step 3: Guest Details ----------------------------------------

    public function guestForm()
    {
        if ($redirect = $this->requireRoom()) {
            return $redirect;
        }

        $w = $this->wizard();

        return view('bookings/wizard_guest', [
            'pageTitle' => 'New Booking',
            'wizard'    => $w,
            'room'      => $this->roomForWizard($w),
            'nights'    => $this->nights($w),
        ]);
    }

    public function saveGuest()
    {
        if ($redirect = $this->requireRoom()) {
            return $redirect;
        }

        $guestId = (int) $this->request->getPost('guest_id');

        if ($guestId > 0) {
            if ((new GuestModel())->find($guestId) === null) {
                return redirect()->to('bookings/wizard/guest')->with('error', 'Select a valid guest.');
            }

            $this->saveWizard(['guest_id' => $guestId, 'new_guest' => null]);

            return redirect()->to('bookings/wizard/details');
        }

        $fullName = trim((string) $this->request->getPost('full_name'));
        $phone    = trim((string) $this->request->getPost('phone'));

        if ($fullName === '' || $phone === '') {
            return redirect()->to('bookings/wizard/guest')->with('error', "Enter the new guest's name and phone.");
        }

        $this->saveWizard([
            'guest_id'  => null,
            'new_guest' => [
                'full_name' => $fullName,
                'phone'     => $phone,
                'email'     => $this->request->getPost('email') ?: null,
            ],
        ]);

        return redirect()->to('bookings/wizard/details');
    }

    // ---- Step 4: Reservation Details -----------------------------------

    public function detailsForm()
    {
        if ($redirect = $this->requireGuest()) {
            return $redirect;
        }

        $w = $this->wizard();

        return view('bookings/wizard_details', [
            'pageTitle'      => 'New Booking',
            'wizard'         => $w,
            'room'           => $this->roomForWizard($w),
            'nights'         => $this->nights($w),
            'guest'          => $w['guest_id'] ? (new GuestModel())->find($w['guest_id']) : null,
            'corporateAccount' => ! empty($w['corporate_account_id']) ? (new CorporateAccountModel())->find($w['corporate_account_id']) : null,
            'guestPreferences' => $w['guest_id'] ? (new GuestPreferenceModel())->forGuest((int) $w['guest_id']) : null,
            'lookups'        => new Lookups(),
        ]);
    }

    public function saveDetails()
    {
        if ($redirect = $this->requireGuest()) {
            return $redirect;
        }

        $adults   = max(1, (int) $this->request->getPost('adults'));
        $children = max(0, (int) $this->request->getPost('children'));

        $w = $this->wizard();
        $rateApplied = $w['rate_applied'];
        $corporateAccountId = (int) $this->request->getPost('corporate_account_id') ?: null;

        // A corporate account with its own negotiated rate plan overrides
        // the standard date-matched rate resolved back in selectRoom() —
        // only if the plan is still active and actually covers this room
        // type (see RatePlanModel::resolveWithPlan()); otherwise the
        // account is still tagged for billing but the rate is unaffected.
        if ($corporateAccountId !== null) {
            $account = (new CorporateAccountModel())->find($corporateAccountId);
            $room    = $this->roomForWizard($w);

            if ($account !== null && $account->default_rate_plan_id && $room !== null) {
                $rateApplied = (new RatePlanModel())->resolveWithPlan(
                    $account->default_rate_plan_id,
                    $room->room_type_id,
                    $w['check_in'],
                    $room->base_rate,
                );
            }
        }

        $this->saveWizard([
            'adults'               => $adults,
            'children'             => $children,
            'special_requests'     => trim((string) $this->request->getPost('special_requests')) ?: null,
            'source'               => $this->request->getPost('source') ?: 'walk_in',
            'corporate_account_id' => $corporateAccountId,
            'rate_applied'         => $rateApplied,
        ]);

        return redirect()->to('bookings/wizard/services');
    }

    // ---- Step 5: Add Services ------------------------------------------

    public function servicesForm()
    {
        if ($redirect = $this->requireGuest()) {
            return $redirect;
        }

        $w = $this->wizard();

        return view('bookings/wizard_services', [
            'pageTitle'    => 'New Booking',
            'wizard'       => $w,
            'room'         => $this->roomForWizard($w),
            'nights'       => $this->nights($w),
            'services'     => (new HotelServiceModel())->active(),
            'lookups'      => new Lookups(),
        ]);
    }

    public function saveServices()
    {
        if ($redirect = $this->requireGuest()) {
            return $redirect;
        }

        $catalog = (new HotelServiceModel())->active();
        $catalogById = [];
        foreach ($catalog as $svc) {
            $catalogById[$svc->id] = $svc;
        }

        $qtyByService = (array) $this->request->getPost('qty');
        $selected     = [];

        foreach ($qtyByService as $serviceId => $qty) {
            $qty = (int) $qty;

            if ($qty <= 0 || ! isset($catalogById[(int) $serviceId])) {
                continue;
            }

            $svc = $catalogById[(int) $serviceId];
            $selected[] = [
                'service_id' => $svc->id,
                'name'       => $svc->name,
                'unit_price' => $svc->price,
                'qty'        => $qty,
                'total'      => money_multiply($svc->price, $qty),
            ];
        }

        $this->saveWizard(['services' => $selected]);

        return redirect()->to('bookings/wizard/payment');
    }

    // ---- Step 6: Payment (the actual commit happens on submit) --------

    /**
     * @return array{roomTotal: string, servicesTotal: string, subtotal: string, taxRate: string, taxAmount: string, grandTotal: string}
     */
    private function pricingSummary(array $w): array
    {
        $roomTotal = money_multiply($w['rate_applied'], $this->nights($w));

        $servicesTotal = '0.00';
        foreach ($w['services'] ?? [] as $item) {
            $servicesTotal = money_add($servicesTotal, $item['total']);
        }

        $subtotal = money_add($roomTotal, $servicesTotal);
        $taxRate  = service('settings')->get('billing.tax_rate_percent', '0') ?: '0';
        $taxAmount = (float) $taxRate > 0 ? number_format((float) $subtotal * (float) $taxRate / 100, 2, '.', '') : '0.00';
        $grandTotal = money_add($subtotal, $taxAmount);

        return [
            'roomTotal'     => $roomTotal,
            'servicesTotal' => $servicesTotal,
            'subtotal'      => $subtotal,
            'taxRate'       => $taxRate,
            'taxAmount'     => $taxAmount,
            'grandTotal'    => $grandTotal,
        ];
    }

    public function paymentForm()
    {
        if ($redirect = $this->requireGuest()) {
            return $redirect;
        }

        $w = $this->wizard();

        return view('bookings/wizard_payment', [
            'pageTitle' => 'New Booking',
            'wizard'    => $w,
            'room'      => $this->roomForWizard($w),
            'nights'    => $this->nights($w),
            'pricing'   => $this->pricingSummary($w),
            'lookups'   => new Lookups(),
        ]);
    }

    /**
     * The commit. Everything before this point only ever touched the
     * session. From here: guest (if new), booking, service-item rows,
     * and — depending on which payment path was chosen — either a
     * manual payment recorded now (applied to the folio at check-in) or
     * a Hubtel deposit link (same flow BookingController used to run).
     */
    public function confirm()
    {
        if ($redirect = $this->requireGuest()) {
            return $redirect;
        }

        $w = $this->wizard();
        $room = $this->roomForWizard($w);

        if ($room === null) {
            return redirect()->to('bookings/new')->with('error', 'That room no longer exists.');
        }

        if ((new BookingModel())->hasOverlap($w['room_id'], $w['check_in'], $w['check_out'])) {
            return redirect()->to('bookings/search?check_in=' . $w['check_in'] . '&check_out=' . $w['check_out'])
                ->with('error', 'That room was just booked by someone else — pick another.');
        }

        $guests = new GuestModel();

        if ($w['guest_id']) {
            $guestId = (int) $w['guest_id'];
        } else {
            $guestData = $w['new_guest'];

            if (! $guests->insert($guestData)) {
                return $this->redirectWithModelErrors($guests);
            }

            $guestId = $guests->getInsertID();
            $this->logActivity('guest', 'Created guest "' . $guestData['full_name'] . '" during booking.');
        }

        $paymentChoice = $this->request->getPost('payment_choice') ?: 'none';

        $bookingData = [
            'guest_id'       => $guestId,
            'room_id'        => $w['room_id'],
            'check_in_date'  => $w['check_in'],
            'check_out_date' => $w['check_out'],
            'status'         => $paymentChoice === 'link' ? 'pending_deposit' : 'confirmed',
            'source'         => $w['source'] ?? 'walk_in',
            'rate_applied'   => $w['rate_applied'],
            'notes'          => $w['special_requests'] ?? null,
            'adults'         => $w['adults'] ?? 1,
            'children'       => $w['children'] ?? 0,
            'corporate_account_id' => $w['corporate_account_id'] ?? null,
        ];

        if ($paymentChoice === 'now') {
            $amount = parse_money($this->request->getPost('payment_amount'));

            if ((float) $amount > 0) {
                $bookingData['initial_payment_amount'] = $amount;
                $bookingData['initial_payment_method']  = $this->request->getPost('payment_method') ?: 'cash';
            }
        }

        $bookings = new BookingModel();

        if (! $bookings->insert($bookingData)) {
            return $this->redirectWithModelErrors($bookings);
        }

        $bookingId = $bookings->getInsertID();

        $serviceItems = new BookingServiceItemModel();

        foreach ($w['services'] ?? [] as $item) {
            $serviceItems->insert([
                'booking_id'   => $bookingId,
                'service_id'   => $item['service_id'],
                'service_name' => $item['name'],
                'unit_price'   => $item['unit_price'],
                'quantity'     => $item['qty'],
            ]);
        }

        $this->logActivity('bookings', "Booked room {$room->room_number} for {$w['check_in']} to {$w['check_out']}.");

        $guest = $guests->find($guestId);

        if ($paymentChoice === 'link') {
            $depositAmount = parse_money($this->request->getPost('payment_amount'));
            $this->requestDeposit($bookingId, $depositAmount, $guest);
        } else {
            $this->sendBookingConfirmation($bookingId, $w['check_in'], $w['check_out'], $guest);
        }

        $this->clearWizard();

        return redirect()->to('bookings/wizard/confirmation/' . $bookingId);
    }

    private function requestDeposit(int $bookingId, string $depositAmount, ?object $guest): void
    {
        $attempts   = new PaymentAttemptModel();
        $reference  = $attempts->generateClientReference('booking_deposit');
        $tenantSlug = service('tenantContext')->getTenant()->slug;

        try {
            $checkout = service('hubtelService')->createCheckout(
                (float) $depositAmount,
                "Booking #{$bookingId} deposit",
                $reference,
                site_url("webhooks/hubtel/checkout/{$tenantSlug}"),
                site_url("bookings/wizard/confirmation/{$bookingId}"),
                site_url("bookings/wizard/confirmation/{$bookingId}"),
            );
        } catch (RuntimeException $e) {
            session()->setFlashdata('error', 'Booking held, but the deposit payment link could not be generated: ' . $e->getMessage());

            return;
        }

        $attempts->insert([
            'purpose'          => 'booking_deposit',
            'booking_id'       => $bookingId,
            'client_reference' => $reference,
            'checkout_id'      => $checkout['checkoutId'],
            'checkout_url'     => $checkout['checkoutUrl'],
            'amount'           => $depositAmount,
            'status'           => 'pending',
        ]);

        if ($guest !== null) {
            service('messagingService')->send(
                'deposit_request',
                $guest->id,
                $guest->phone,
                "Hi {$guest->full_name}, please pay your deposit of " . format_money($depositAmount) .
                    " to confirm your booking: {$checkout['checkoutUrl']}",
                [$guest->full_name, format_money($depositAmount), $checkout['checkoutUrl']],
            );
        }
    }

    private function sendBookingConfirmation(int $bookingId, string $checkIn, string $checkOut, ?object $guest): void
    {
        if ($guest === null) {
            return;
        }

        $nights = (strtotime($checkOut) - strtotime($checkIn)) / 86400;

        service('messagingService')->send(
            'booking_confirmation',
            $guest->id,
            $guest->phone,
            "Hi {$guest->full_name}, your booking is confirmed for {$checkIn} to {$checkOut} ({$nights} night(s)). We look forward to hosting you!",
            [$guest->full_name, $checkIn, $checkOut],
        );
    }

    // ---- Step 7: Confirmation (read-only view of what was just created) ----

    public function confirmation(int $bookingId)
    {
        $booking = (new BookingModel())->withDetails()->where('bookings.id', $bookingId)->first();

        if ($booking === null) {
            return redirect()->to('bookings')->with('error', 'Booking not found.');
        }

        return view('bookings/wizard_confirmation', array_merge(
            ['pageTitle' => 'Booking Confirmed', 'booking' => $booking, 'lookups' => new Lookups()],
            $this->confirmationData($booking),
        ));
    }

    /**
     * Same figures confirmation()/confirmationPdf()/sendConfirmationEmail()
     * all show — computed once here so the on-screen confirmation, the
     * PDF, and the emailed copy can never disagree with each other.
     */
    private function confirmationData(object $booking): array
    {
        $nights = (int) max(1, (strtotime($booking->check_out_date) - strtotime($booking->check_in_date)) / 86400);
        $serviceItems = (new BookingServiceItemModel())->forBooking($booking->id);
        $servicesTotal = '0.00';
        foreach ($serviceItems as $item) {
            $servicesTotal = money_add($servicesTotal, money_multiply($item->unit_price, $item->quantity));
        }

        $roomTotal = money_multiply($booking->rate_applied, $nights);
        $subtotal  = money_add($roomTotal, $servicesTotal);
        $taxRate   = service('settings')->get('billing.tax_rate_percent', '0') ?: '0';
        $taxAmount = (float) $taxRate > 0 ? number_format((float) $subtotal * (float) $taxRate / 100, 2, '.', '') : '0.00';
        $grandTotal = money_add($subtotal, $taxAmount);
        $paid       = $booking->initial_payment_amount ?? '0.00';

        return [
            'nights'        => $nights,
            'serviceItems'  => $serviceItems,
            'roomTotal'     => $roomTotal,
            'servicesTotal' => $servicesTotal,
            'subtotal'      => $subtotal,
            'taxRate'       => $taxRate,
            'taxAmount'     => $taxAmount,
            'grandTotal'    => $grandTotal,
            'paid'          => $paid,
            'balance'       => money_subtract($grandTotal, $paid),
        ];
    }

    public function confirmationPdf(int $bookingId)
    {
        $booking = (new BookingModel())->withDetails()->where('bookings.id', $bookingId)->first();

        if ($booking === null) {
            return redirect()->to('bookings')->with('error', 'Booking not found.');
        }

        $html = view('bookings/wizard_confirmation_pdf', array_merge(
            ['booking' => $booking, 'tenant' => service('tenantContext')->getTenant()],
            $this->confirmationData($booking),
        ));

        $pdf = service('pdfService')->renderHtml($html);

        return $this->response
            ->setHeader('Content-Type', 'application/pdf')
            ->setHeader('Content-Disposition', "inline; filename=\"reservation-BK-" . sprintf('%04d', $bookingId) . '.pdf"')
            ->setBody($pdf);
    }

    public function sendConfirmationEmail(int $bookingId)
    {
        $booking = (new BookingModel())->withDetails()->where('bookings.id', $bookingId)->first();

        if ($booking === null || ! $booking->guest_email) {
            return redirect()->to('bookings/wizard/confirmation/' . $bookingId)->with('error', 'No email on file for this guest.');
        }

        $html = view('bookings/wizard_confirmation_pdf', array_merge(
            ['booking' => $booking, 'tenant' => service('tenantContext')->getTenant()],
            $this->confirmationData($booking),
        ));

        $pdf = service('pdfService')->renderHtml($html);
        $tenant = service('tenantContext')->getTenant();

        $result = service('emailService')->send(
            $booking->guest_email,
            'Reservation Confirmation — ' . $tenant->hotel_name,
            '<p>Hi ' . esc($booking->guest_name) . ',</p><p>Your reservation is confirmed. See the attached PDF for details.</p><p>' . esc($tenant->hotel_name) . '</p>',
            ['filename' => 'reservation-BK-' . sprintf('%04d', $bookingId) . '.pdf', 'content' => $pdf, 'mime' => 'application/pdf'],
            (int) $booking->guest_id,
        );

        if (! $result['success']) {
            return redirect()->to('bookings/wizard/confirmation/' . $bookingId)->with('error', $result['error']);
        }

        $this->logActivity('bookings', "Emailed reservation confirmation for booking #{$bookingId} to {$booking->guest_email}.");

        return redirect()->to('bookings/wizard/confirmation/' . $bookingId)->with('success', 'Confirmation emailed to ' . $booking->guest_email . '.');
    }

    /**
     * Resend — the confirmation SMS already went out automatically at
     * confirm() time (or a deposit-request SMS did, if a payment link was
     * sent instead); this is for when the guest says they never got it.
     */
    public function sendConfirmationSms(int $bookingId)
    {
        $booking = (new BookingModel())->withDetails()->where('bookings.id', $bookingId)->first();

        if ($booking === null) {
            return redirect()->to('bookings')->with('error', 'Booking not found.');
        }

        $nights = (int) max(1, (strtotime($booking->check_out_date) - strtotime($booking->check_in_date)) / 86400);

        $result = service('messagingService')->send(
            'booking_confirmation',
            $booking->guest_id,
            $booking->guest_phone,
            "Hi {$booking->guest_name}, your booking is confirmed for {$booking->check_in_date} to {$booking->check_out_date} ({$nights} night(s)). We look forward to hosting you!",
            [$booking->guest_name, $booking->check_in_date, $booking->check_out_date],
        );

        // send() only attempts a channel if the tenant has it configured, so
        // a null on both sides means nothing was even tried (no channel
        // configured) rather than a failed attempt — worded accordingly.
        $sent = $result['whatsapp'] === true || $result['sms'] === true;

        if (! $sent) {
            $this->logActivity('bookings', "Resend confirmation SMS failed for booking #{$bookingId}.");

            $message = ($result['whatsapp'] === null && $result['sms'] === null)
                ? 'No messaging channel is configured for this hotel yet — set it up under Settings > Integrations.'
                : 'Could not send the confirmation message — check the messaging provider settings and try again.';

            return redirect()->to('bookings/wizard/confirmation/' . $bookingId)->with('error', $message);
        }

        $this->logActivity('bookings', "Resent confirmation SMS for booking #{$bookingId}.");

        return redirect()->to('bookings/wizard/confirmation/' . $bookingId)->with('success', 'Confirmation message sent.');
    }
}
