<?php

namespace App\Controllers;

use App\Models\BookingModel;
use App\Models\FolioLineItemModel;
use App\Models\FolioModel;
use App\Models\FolioPaymentModel;
use App\Models\GuestModel;
use App\Models\PaymentAttemptModel;
use App\Models\RoomModel;
use App\Models\RoomTypeModel;
use Config\Lookups;

class BookingController extends BaseController
{
    /**
     * Searchable/filterable reservations list — redesigned to match a
     * user-supplied mockup. That mockup implied a few things that don't
     * exist here and were deliberately NOT faked: OTA channel integrations
     * (Booking.com/Expedia/Agoda — this app only has the 4 real sources in
     * Config\Lookups::$bookingSources), a CSV import, and per-booking
     * adult/child occupancy counts. "Confirmation #" is a real formatted
     * booking id (BK-0001, ...), not a fabricated 5-digit sequence implying
     * more volume than actually exists. "Total" prefers the booking's real
     * folio total (once checked in) and falls back to rate × nights for a
     * not-yet-arrived booking (no folio exists until check-in — see
     * CheckInController).
     */
    public function index()
    {
        $bookings = new BookingModel();

        $filters = [
            'q'            => $this->request->getGet('q'),
            'date_from'    => $this->request->getGet('date_from'),
            'date_to'      => $this->request->getGet('date_to'),
            'room_type_id' => $this->request->getGet('room_type_id'),
            'source'       => $this->request->getGet('source'),
            'status'       => $this->request->getGet('status'),
            'tab'          => $this->request->getGet('tab') ?: 'all',
        ];

        $results = $bookings->search($filters)->paginate(10);
        $pager   = $bookings->pager;

        $folios = new FolioModel();

        foreach ($results as $b) {
            $folio  = $folios->findForBooking($b->id);
            $nights = max(1, (strtotime($b->check_out_date) - strtotime($b->check_in_date)) / 86400);

            $b->total          = $folio !== null ? $folios->totalCharges($folio->id) : money_multiply($b->rate_applied, (int) $nights);
            $b->payment_status = $folio === null ? null : $this->paymentStatus($folios->totalCharges($folio->id), $folios->totalPaid($folio->id));
            $b->nights         = (int) $nights;
        }

        $today     = date('Y-m-d');
        $yesterday = date('Y-m-d', strtotime('-1 day'));
        $total     = (new BookingModel())->countAllResults();

        return view('bookings/index', [
            'pageTitle'    => 'Reservations',
            'pageSubtitle' => 'Manage and track all hotel reservations.',
            'bookings'    => $results,
            'pager'       => $pager,
            'filters'     => $filters,
            'roomTypes'   => (new RoomTypeModel())->orderBy('name')->findAll(),
            'lookups'     => new Lookups(),
            'stats'       => [
                'total'            => $total,
                'createdTodayPct'  => $this->percentChange(
                    (new BookingModel())->where('DATE(created_at)', $yesterday)->countAllResults(),
                    (new BookingModel())->where('DATE(created_at)', $today)->countAllResults()
                ),
                'confirmed'        => (new BookingModel())->where('status', 'confirmed')->countAllResults(),
                'checkInToday'     => (new BookingModel())->arrivalCountOn($today),
                'checkInYesterday' => (new BookingModel())->arrivalCountOn($yesterday),
                'inHouse'          => (new BookingModel())->where('status', 'checked_in')->countAllResults(),
                'cancelled'        => (new BookingModel())->where('status', 'cancelled')->countAllResults(),
                'totalAll'         => max(1, $total),
            ],
        ]);
    }

    private function paymentStatus(string $totalCharges, string $totalPaid): string
    {
        if ((float) $totalPaid <= 0) {
            return 'unpaid';
        }

        return (float) $totalPaid >= (float) $totalCharges ? 'paid' : 'partial';
    }

    private function percentChange(int $previous, int $current): float
    {
        if ($previous === 0) {
            return $current > 0 ? 100.0 : 0.0;
        }

        return round((($current - $previous) / $previous) * 100, 1);
    }

    /**
     * Rooms down the side (grouped by room type, matching the mockup),
     * dates across the top — the main front-desk screen. Defaults to a
     * 7-day week starting today, or a caller-supplied start date;
     * Prev/Next/Today shift by 7 days to match. Redesigned to match a
     * user-supplied mockup — its legend included "Due In"/"Due Out"/
     * "Blocked"/"Maintenance" states that aren't real booking statuses
     * here (Due In/Out are a same-day view of confirmed/checked_in,
     * already covered by the Reservations list's tabs; Blocked/
     * Maintenance would need the not-yet-built Maintenance module) — the
     * legend below shows the 4 real statuses BookingModel::inRange()
     * actually returns (cancelled/no_show bookings never appear here at
     * all, so showing them in the legend would be misleading).
     */
    public function calendar()
    {
        $start = $this->request->getGet('start') ?: date('Y-m-d');
        $days  = 7;
        $end   = date('Y-m-d', strtotime($start . " +{$days} days"));

        $roomBuilder = (new RoomModel())->withType();

        if ($roomTypeId = $this->request->getGet('room_type_id')) {
            $roomBuilder->where('rooms.room_type_id', (int) $roomTypeId);
        }

        $rooms    = $roomBuilder->orderBy('room_types.name')->orderBy('rooms.room_number')->findAll();
        $bookings = (new BookingModel())->inRange($start, $end);

        // Index bookings per room for O(1) lookup while rendering the grid.
        $bookingsByRoom = [];
        foreach ($bookings as $booking) {
            $bookingsByRoom[$booking->room_id][] = $booking;
        }

        $dateColumns = [];
        for ($i = 0; $i < $days; $i++) {
            $dateColumns[] = date('Y-m-d', strtotime($start . " +{$i} days"));
        }

        // Collapse each room's per-day cells into colspan'd segments (empty
        // vs. one specific booking) so a multi-night stay renders as a
        // single clearly-labeled block instead of one cell per night.
        $roomSegments = [];
        $roomsByType  = [];
        foreach ($rooms as $room) {
            $roomSegments[$room->id] = $this->buildRoomSegments($dateColumns, $bookingsByRoom[$room->id] ?? []);
            $roomsByType[$room->room_type_name][] = $room;
        }

        return view('bookings/calendar', [
            'pageTitle'    => 'Booking Calendar',
            'pageSubtitle' => 'Rooms down the side, dates across the top.',
            'roomsByType'  => $roomsByType,
            'dateColumns'  => $dateColumns,
            'roomSegments' => $roomSegments,
            'start'        => $start,
            'roomTypes'    => (new RoomTypeModel())->orderBy('name')->findAll(),
            'roomTypeId'   => $roomTypeId ?? null,
            'lookups'      => new Lookups(),
        ]);
    }

    /**
     * @param list<string> $dateColumns
     * @param list<object> $roomBookings
     *
     * @return list<array{type: string, colspan: int, booking: ?object}>
     */
    private function buildRoomSegments(array $dateColumns, array $roomBookings): array
    {
        $bookingForDate = static function (string $date) use ($roomBookings): ?object {
            foreach ($roomBookings as $booking) {
                if ($date >= $booking->check_in_date && $date < $booking->check_out_date) {
                    return $booking;
                }
            }

            return null;
        };

        $segments = [];

        foreach ($dateColumns as $date) {
            $booking = $bookingForDate($date);
            $last    = $segments[count($segments) - 1] ?? null;
            $sameAsLast = $last !== null && (
                ($booking === null && $last['booking'] === null)
                || ($booking !== null && $last['booking'] !== null && $booking->id === $last['booking']->id)
            );

            if ($sameAsLast) {
                $segments[count($segments) - 1]['colspan']++;
            } else {
                $segments[] = ['type' => $booking ? 'booking' : 'empty', 'colspan' => 1, 'booking' => $booking];
            }
        }

        return $segments;
    }

    /**
     * Step 1 of "create booking": pick dates (and optionally a room type),
     * see which rooms are free.
     */
    public function searchForm()
    {
        return view('bookings/search', [
            'pageTitle'    => 'New Booking',
            'pageSubtitle' => 'Step 1 of 7 — pick dates to see which rooms are free.',
            'roomTypes'    => (new RoomTypeModel())->orderBy('name')->findAll(),
            'checkIn'      => $this->request->getGet('check_in') ?: date('Y-m-d'),
            'checkOut'     => $this->request->getGet('check_out') ?: date('Y-m-d', strtotime('+1 day')),
        ]);
    }

    public function searchResults()
    {
        $checkIn    = $this->request->getGet('check_in');
        $checkOut   = $this->request->getGet('check_out');
        $roomTypeId = $this->request->getGet('room_type_id') ?: null;

        if (! $checkIn || ! $checkOut || strtotime($checkOut) <= strtotime($checkIn)) {
            return redirect()->to('bookings/new')->with('error', 'Check-out must be after check-in.');
        }

        $availableRooms = (new RoomModel())->findAvailable($checkIn, $checkOut, $roomTypeId ? (int) $roomTypeId : null);

        return view('bookings/search_results', [
            'pageTitle'      => 'Available Rooms',
            'pageSubtitle'   => 'Step 2 of 7 — choose a room to book.',
            'availableRooms' => $availableRooms,
            'checkIn'        => $checkIn,
            'checkOut'       => $checkOut,
        ]);
    }

    /**
     * Redesigned (tabs: Reservation/Guest/Payment/Notes) to match the
     * mockup batch — previously a single plain card. Payment Info tab
     * degrades gracefully when there's no folio yet (a not-yet-checked-in
     * booking never has one — see CheckInController) rather than showing
     * an empty/broken table.
     */
    public function show(int $id)
    {
        $booking = (new BookingModel())->withDetails()->where('bookings.id', $id)->first();

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

        $guest = (new GuestModel())->find($booking->guest_id);
        $folioModel = new FolioModel();
        $folio   = $folioModel->findForBooking($id);
        $charges = $folio ? (new FolioLineItemModel())->forFolio($folio->id) : [];
        $payments = $folio ? (new FolioPaymentModel())->forFolio($folio->id) : [];

        $pendingDeposit = $booking->status === 'pending_deposit'
            ? (new PaymentAttemptModel())->where('booking_id', $id)->where('purpose', 'booking_deposit')->where('status', 'pending')->orderBy('id', 'desc')->first()
            : null;

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

        return view('bookings/show', [
            'pageTitle'      => 'Booking #' . $id,
            'booking'        => $booking,
            'guest'          => $guest,
            'nights'         => (int) $nights,
            'folio'          => $folio,
            'charges'        => $charges,
            'payments'       => $payments,
            'folioTotal'     => $folio ? $folioModel->totalCharges($folio->id) : '0.00',
            'folioPaid'      => $folio ? $folioModel->totalPaid($folio->id) : '0.00',
            'folioBalance'   => $folio ? $folioModel->balanceDue($folio->id) : '0.00',
            'pendingDeposit' => $pendingDeposit,
            'lookups'        => new Lookups(),
        ]);
    }

    public function cancel(int $id)
    {
        $bookings = new BookingModel();
        $booking  = $bookings->find($id);

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

        if (in_array($booking->status, ['checked_in', 'checked_out'], true)) {
            return redirect()->back()->with('error', 'Cannot cancel a booking that has already checked in.');
        }

        $bookings->update($id, ['status' => 'cancelled']);
        $this->logActivity('bookings', "Cancelled booking #{$id}.");

        return redirect()->to('bookings/calendar')->with('success', 'Booking cancelled.');
    }

    /**
     * `no_show` is a real status in Config\Lookups::$bookingStatuses that
     * nothing ever actually set — front desk had no way to mark a guest
     * who never arrived. Only meaningful for a booking that was confirmed
     * (or held for deposit) and never checked in; once check-in happens
     * the guest plainly wasn't a no-show, and there's nothing to flip back
     * from cancelled/checked_out either.
     */
    public function markNoShow(int $id)
    {
        $bookings = new BookingModel();
        $booking  = $bookings->find($id);

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

        if (! in_array($booking->status, ['confirmed', 'pending_deposit'], true)) {
            return redirect()->back()->with('error', 'Only a confirmed booking that never arrived can be marked no-show.');
        }

        $bookings->update($id, ['status' => 'no_show']);
        $this->logActivity('bookings', "Marked booking #{$id} as no-show.");

        return redirect()->to('bookings/' . $id)->with('success', 'Booking marked as no-show.');
    }

    /**
     * Reassigns a booking to a different room for the same dates — real
     * gap, appeared in the mockups (Room Management/Check-In) as a plain
     * front-desk action. Works for any active (not yet arrived or
     * currently in-house) booking; a cancelled/checked_out/no_show
     * booking has nothing left to reassign. Available rooms use the same
     * overlap check as new-booking search (RoomModel::findAvailable),
     * excluding this booking's own reservation so its current room never
     * appears as an "other" option.
     */
    public function changeRoomForm(int $id)
    {
        $booking = (new BookingModel())->withDetails()->where('bookings.id', $id)->first();

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

        if (! in_array($booking->status, ['confirmed', 'pending_deposit', 'checked_in'], true)) {
            return redirect()->to('bookings/' . $id)->with('error', 'This booking can no longer be reassigned to a different room.');
        }

        $availableRooms = (new RoomModel())->findAvailableExcludingBooking(
            $booking->check_in_date,
            $booking->check_out_date,
            $id,
            (int) $booking->room_id,
        );

        return view('bookings/change_room', [
            'pageTitle'      => 'Change Room — Booking #' . $id,
            'pageSubtitle'   => 'Currently in ' . $booking->room_number . ' (' . $booking->room_type_name . ').',
            'booking'        => $booking,
            'availableRooms' => $availableRooms,
            'lookups'        => new Lookups(),
        ]);
    }

    public function changeRoom(int $id)
    {
        $bookings = new BookingModel();
        $booking  = $bookings->find($id);

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

        if (! in_array($booking->status, ['confirmed', 'pending_deposit', 'checked_in'], true)) {
            return redirect()->to('bookings/' . $id)->with('error', 'This booking can no longer be reassigned to a different room.');
        }

        $newRoomId = (int) $this->request->getPost('room_id');
        $rooms     = new RoomModel();
        $newRoom   = $rooms->withType()->where('rooms.id', $newRoomId)->first();

        if ($newRoom === null) {
            return redirect()->back()->with('error', 'Select a room.');
        }

        if ($newRoomId === (int) $booking->room_id) {
            return redirect()->to('bookings/' . $id)->with('error', 'That is already this booking\'s room.');
        }

        if ($bookings->hasOverlap($newRoomId, $booking->check_in_date, $booking->check_out_date, $id)) {
            return redirect()->back()->with('error', 'That room is no longer available for these dates.');
        }

        $oldRoomId     = (int) $booking->room_id;
        $oldRoomNumber = $rooms->find($oldRoomId)->room_number ?? "#{$oldRoomId}";

        $bookings->update($id, ['room_id' => $newRoomId]);

        // Same room-state handoff as check-out: an in-house guest's old
        // room needs housekeeping before it can be sold again, and the
        // new room is now physically occupied. A not-yet-arrived booking
        // never touched room status in the first place (see
        // CheckInController), so there's nothing to flip either side.
        if ($booking->status === 'checked_in') {
            $rooms->setStatus($oldRoomId, 'dirty');
            $rooms->setStatus($newRoomId, 'occupied');
        }

        $this->logActivity('bookings', "Moved booking #{$id} from room {$oldRoomNumber} to {$newRoom->room_number}.");

        return redirect()->to('bookings/' . $id)->with('success', 'Room changed to ' . $newRoom->room_number . '.');
    }
}
