<?php

namespace App\Models;

use Config\Lookups;

/**
 * `status` and `source` are validated against App\Config\Lookups rather
 * than a hard-coded list — see that class for why (Phase 3 adds
 * `pending_deposit` without touching this column).
 */
class BookingModel extends TenantModel
{
    protected $table            = 'bookings';
    protected $primaryKey       = 'id';
    protected $useAutoIncrement = true;
    protected $returnType       = 'object';
    protected $useTimestamps    = true;

    protected $allowedFields = [
        'guest_id',
        'room_id',
        'check_in_date',
        'check_out_date',
        'status',
        'source',
        'rate_applied',
        'notes',
        'reminder_sent_at',
        'adults',
        'children',
        'initial_payment_amount',
        'initial_payment_method',
        'initial_payment_applied',
        'corporate_account_id',
        'booking_group_id',
    ];

    public function __construct()
    {
        parent::__construct();

        $lookups = new Lookups();

        $this->validationRules = [
            'guest_id'       => 'required|integer',
            'room_id'        => 'required|integer',
            'check_in_date'  => 'required|valid_date[Y-m-d]',
            'check_out_date' => 'required|valid_date[Y-m-d]|checkOutAfterCheckIn[check_in_date]',
            'status'         => 'permit_empty|in_list[' . implode(',', $lookups->bookingStatusKeys()) . ']',
            'source'         => 'permit_empty|in_list[' . implode(',', array_keys($lookups->bookingSources)) . ']',
        ];
    }

    /**
     * Bookings with guest name / room number / room type joined in — the
     * calendar, arrivals, and departures views all want this shape.
     */
    public function withDetails()
    {
        return $this->select('bookings.*, guests.full_name as guest_name, guests.phone as guest_phone, guests.email as guest_email,
                rooms.room_number as room_number, rooms.room_type_id as room_type_id, room_types.name as room_type_name')
            ->join('guests', 'guests.id = bookings.guest_id')
            ->join('rooms', 'rooms.id = bookings.room_id')
            ->join('room_types', 'room_types.id = rooms.room_type_id');
    }

    /** Every room booked as part of one group booking — see BookingGroupController. */
    public function forGroup(int $groupId): array
    {
        return $this->withDetails()->where('bookings.booking_group_id', $groupId)->orderBy('rooms.room_number', 'asc')->findAll();
    }

    /**
     * The Reservations list (app/Views/bookings/index.php) — every filter
     * is optional and AND-combined. Returns the model itself (still
     * chainable) rather than findAll(), so the controller can add
     * ->paginate() the same way ActivityLogController does.
     *
     * @param array{q?: string, date_from?: string, date_to?: string, room_type_id?: int, source?: string, status?: string, tab?: string} $filters
     */
    public function search(array $filters): static
    {
        $builder = $this->withDetails();

        if (! empty($filters['q'])) {
            $term = $filters['q'];
            $builder->groupStart()
                ->like('guests.full_name', $term)
                ->orLike('guests.phone', $term)
                ->orLike('guests.email', $term)
                ->orLike('bookings.id', $term)
                ->groupEnd();
        }

        if (! empty($filters['date_from'])) {
            $builder->where('bookings.check_in_date >=', $filters['date_from']);
        }

        if (! empty($filters['date_to'])) {
            $builder->where('bookings.check_in_date <=', $filters['date_to']);
        }

        if (! empty($filters['room_type_id'])) {
            $builder->where('rooms.room_type_id', $filters['room_type_id']);
        }

        if (! empty($filters['source'])) {
            $builder->where('bookings.source', $filters['source']);
        }

        if (! empty($filters['status'])) {
            $builder->where('bookings.status', $filters['status']);
        }

        // Tabs are a second, coarser status/date filter layered on top of
        // (not instead of) the "All Status" dropdown above — matches the
        // mockup having both a tab strip and a status filter at once.
        switch ($filters['tab'] ?? 'all') {
            case 'confirmed':
                $builder->where('bookings.status', 'confirmed');
                break;
            case 'in_house':
                $builder->where('bookings.status', 'checked_in');
                break;
            case 'due_in':
                $builder->where('bookings.status', 'confirmed')->where('bookings.check_in_date', date('Y-m-d'));
                break;
            case 'due_out':
                $builder->where('bookings.status', 'checked_in')->where('bookings.check_out_date', date('Y-m-d'));
                break;
            case 'cancelled':
                $builder->where('bookings.status', 'cancelled');
                break;
            case 'no_show':
                $builder->where('bookings.status', 'no_show');
                break;
            case 'all':
            default:
                break;
        }

        return $builder->orderBy('bookings.check_in_date', 'desc')->orderBy('bookings.id', 'desc');
    }

    /**
     * Bookings due to arrive on $date regardless of current status —
     * broader than arrivalsOn() (which only shows still-confirmed,
     * not-yet-checked-in arrivals, the actual front-desk to-do list).
     * This is for the Reservations list's "vs yesterday" stat, where
     * "how many were due in that day" is the meaningful comparison, not
     * "how many are still pending" (which is always 0 for a past day).
     */
    public function arrivalCountOn(string $date): int
    {
        return $this->where('check_in_date', $date)->where('status !=', 'cancelled')->countAllResults();
    }

    public function arrivalsOn(string $date): array
    {
        return $this->withDetails()
            ->where('bookings.check_in_date', $date)
            ->whereIn('bookings.status', ['confirmed'])
            ->orderBy('rooms.room_number')
            ->findAll();
    }

    public function departuresOn(string $date): array
    {
        return $this->withDetails()
            ->where('bookings.check_out_date', $date)
            ->whereIn('bookings.status', ['checked_in'])
            ->orderBy('rooms.room_number')
            ->findAll();
    }

    /**
     * Every booking that overlaps [rangeStart, rangeEnd) — used by the
     * calendar grid. Cancelled/no-show bookings are excluded so they don't
     * visually block the room for new bookings.
     */
    public function inRange(string $rangeStart, string $rangeEnd): array
    {
        return $this->withDetails()
            ->whereIn('bookings.status', ['pending_deposit', 'confirmed', 'checked_in', 'checked_out'])
            ->where('bookings.check_in_date <', $rangeEnd)
            ->where('bookings.check_out_date >', $rangeStart)
            ->findAll();
    }

    /**
     * `pending_deposit` counts as holding the room — Phase 3's whole point
     * of that status is that the room stays reserved while the guest pays,
     * not just once payment clears. See also RoomModel::findAvailable(),
     * which must stay in sync with this same status list.
     */
    /**
     * Distinct rooms actually occupied on $date — a stay (checked_in or
     * checked_out, so a past day still counts even though the booking has
     * since moved to checked_out) whose date range covers $date. Used for
     * the dashboard's occupancy trend on days before today; today itself
     * uses RoomModel's live status instead (see DashboardController) —
     * a merely `confirmed` booking checking in later today shouldn't
     * count as occupying the room right now.
     */
    public function occupiedRoomCountOn(string $date): int
    {
        $rows = $this->builder()
            ->select('room_id')
            ->distinct()
            ->whereIn('status', ['checked_in', 'checked_out'])
            ->where('check_in_date <=', $date)
            ->where('check_out_date >', $date)
            ->get()
            ->getResultArray();

        return count($rows);
    }

    /**
     * Reservation counts for the dashboard's "Reservations Overview" —
     * counted by check-in date falling in [startDate, endDate], grouped
     * by status. `pending_deposit` bookings are folded into the
     * "confirmed" bucket the same way the front desk treats them
     * everywhere else (see the class-level note on hasOverlap()) — from
     * a reservations-overview point of view a deposit-pending booking is
     * still a live reservation, not a distinct fourth outcome.
     *
     * @return array{total: int, confirmed: int, cancelled: int, no_show: int}
     */
    public function reservationsOverview(string $startDate, string $endDate): array
    {
        $counts = [
            'confirmed' => $this->where('check_in_date >=', $startDate)->where('check_in_date <=', $endDate)
                ->whereIn('status', ['pending_deposit', 'confirmed', 'checked_in', 'checked_out'])->countAllResults(),
            'cancelled' => $this->where('check_in_date >=', $startDate)->where('check_in_date <=', $endDate)
                ->where('status', 'cancelled')->countAllResults(),
            'no_show' => $this->where('check_in_date >=', $startDate)->where('check_in_date <=', $endDate)
                ->where('status', 'no_show')->countAllResults(),
        ];

        $counts['total'] = $counts['confirmed'] + $counts['cancelled'] + $counts['no_show'];

        return $counts;
    }

    public function hasOverlap(int $roomId, string $checkIn, string $checkOut, ?int $excludeBookingId = null): bool
    {
        $builder = $this->where('room_id', $roomId)
            ->whereIn('status', ['pending_deposit', 'confirmed', 'checked_in'])
            ->where('check_in_date <', $checkOut)
            ->where('check_out_date >', $checkIn);

        if ($excludeBookingId !== null) {
            $builder->where('id !=', $excludeBookingId);
        }

        return $builder->countAllResults() > 0;
    }

    /**
     * "Active or upcoming" for the WhatsApp Concierge's grounding check
     * (see the Phase 5 AI brief's Tier 2 guardrails) — checked_in takes
     * priority (the guest messaging us is most likely in-house right
     * now), otherwise the soonest not-yet-arrived confirmed booking.
     * Returns null if neither exists, which is exactly the signal the
     * concierge uses to withhold an AI reply and flag for staff instead.
     */
    public function activeOrUpcomingForGuest(int $guestId): ?object
    {
        $checkedIn = $this->withDetails()
            ->where('bookings.guest_id', $guestId)
            ->where('bookings.status', 'checked_in')
            ->orderBy('bookings.check_out_date', 'asc')
            ->first();

        if ($checkedIn !== null) {
            return $checkedIn;
        }

        return $this->withDetails()
            ->where('bookings.guest_id', $guestId)
            ->where('bookings.status', 'confirmed')
            ->where('bookings.check_in_date >=', date('Y-m-d'))
            ->orderBy('bookings.check_in_date', 'asc')
            ->first();
    }
}
