<?php

use CodeIgniter\Router\RouteCollection;

/** @var RouteCollection $routes */

// Public (tenant already resolved by the global 'tenant' filter, no login required)
// Throttled — password/code-guessing endpoints, previously completely
// unprotected against brute force. 8/min is generous enough for a real
// user fumbling a password twice, tight enough to make guessing
// impractical (a 6-digit OTP has 1,000,000 combinations; even without
// the code's own 10-minute expiry, 8 guesses/min is nowhere close).
$routes->get('login', 'AuthController::showLogin');
$routes->post('login', 'AuthController::attemptLogin', ['filter' => 'throttle:8,60']);
$routes->get('login/verify', 'AuthController::showVerify');
$routes->post('login/verify', 'AuthController::verifyOtp', ['filter' => 'throttle:8,60']);
$routes->post('login/resend', 'AuthController::resendOtp', ['filter' => 'throttle:5,60']);
$routes->get('logout', 'AuthController::logout');

// Serves images written by handle_image_upload() (logos, menu item
// photos) — see UploadController's doc comment for why this can't just
// be a static public/ path. Public: decorative branding/menu assets,
// not sensitive, and the login page may want the tenant logo.
$routes->get('uploads/(:segment)/(:segment)', 'UploadController::serve/$1/$2');

// SMS-based self-service password reset (see ForgotPasswordController for
// why SMS and not email). Public — the whole point is for someone who
// can't log in to reach it.
$routes->get('forgot-password', 'ForgotPasswordController::showRequestForm');
$routes->post('forgot-password', 'ForgotPasswordController::requestCode', ['filter' => 'throttle:5,60']);
$routes->get('forgot-password/verify', 'ForgotPasswordController::showVerifyForm');
$routes->post('forgot-password/verify', 'ForgotPasswordController::resetPassword', ['filter' => 'throttle:8,60']);
$routes->post('forgot-password/resend', 'ForgotPasswordController::resendCode', ['filter' => 'throttle:5,60']);

// Webhooks: machine-to-machine, no session/subdomain/CSRF (see the
// 'tenant'/'csrf' global filter exceptions in Filters.php). Tenant is
// resolved explicitly from the (:segment) slug in the URL itself.
$routes->post('webhooks/hubtel/checkout/(:segment)', 'PaymentWebhookController::hubtelCheckout/$1');
// Platform's own Hubtel account (subscription payments FROM hotels) —
// distinct merchant context, distinct endpoint, see PaymentWebhookController.
$routes->post('webhooks/hubtel/platform-checkout', 'PaymentWebhookController::platformHubtelCheckout');
// WhatsApp Concierge's inbound webhook — Meta Cloud API. verify() is
// Meta's GET handshake; incoming() is the real per-message POST. See
// ConciergeWebhookController for why this mirrors PaymentWebhookController's
// exact tenant-resolution/always-200 pattern.
$routes->get('webhooks/whatsapp/concierge/(:segment)', 'ConciergeWebhookController::verify/$1');
$routes->post('webhooks/whatsapp/concierge/(:segment)', 'ConciergeWebhookController::incoming/$1');

// Public booking widget — the app's first anonymous, cross-origin surface
// (embedded via <iframe> on a hotel's own external website, see
// WidgetBookingController's doc comment). Excluded from the 'csrf' global
// filter (see Filters.php) since a cross-site iframe can't reliably carry
// a SameSite=Lax CSRF cookie. Throttled: generous for browsing, tight for
// the two writes (booking + Hubtel checkout initiation).
$routes->group('widget', ['filter' => 'throttle:30,60'], static function (RouteCollection $routes) {
    $routes->get('booking', 'WidgetBookingController::search');
    $routes->get('booking/form', 'WidgetBookingController::bookForm');
    $routes->post('booking/confirm', 'WidgetBookingController::confirm', ['filter' => 'throttle:5,60']);
    $routes->get('booking/confirmation/(:num)', 'WidgetBookingController::confirmation/$1');
});

// Public event registration — reached via the link/QR staff share from an
// event's page (see EventController::show()), never the raw events.id.
$routes->group('event-register', ['filter' => 'throttle:20,60'], static function (RouteCollection $routes) {
    $routes->get('(:segment)', 'EventRegistrationController::form/$1');
    $routes->post('(:segment)', 'EventRegistrationController::register/$1', ['filter' => 'throttle:5,60']);
});

// Public event self-check-in — this page IS the kiosk (see
// EventCheckinController's doc comment). Higher budget than registration:
// one shared tablet at a busy event entrance legitimately bursts many
// requests from a single IP in a short window.
$routes->group('event-checkin', ['filter' => 'throttle:60,60'], static function (RouteCollection $routes) {
    $routes->get('(:segment)', 'EventCheckinController::show/$1');
    $routes->post('(:segment)', 'EventCheckinController::submit/$1');
});

// Event Organizer realm — a third auth boundary alongside staff and
// Super Admin, own session-key prefix (organizer*) for the same
// shared-session reason AdminAuth's admin* prefix exists — see
// App\Filters\OrganizerAuth. Tenant-scoped (unlike admin), so stays
// subject to the normal 'tenant'/'csrf' global filters.
$routes->get('organizer/login', 'EventOrganizerAuthController::showLogin');
$routes->post('organizer/login', 'EventOrganizerAuthController::attemptLogin', ['filter' => 'throttle:10,60']);
$routes->get('organizer/logout', 'EventOrganizerAuthController::logout');
$routes->get('organizer/forgot-password', 'EventOrganizerPasswordController::showRequestForm');
$routes->post('organizer/forgot-password', 'EventOrganizerPasswordController::requestCode', ['filter' => 'throttle:10,60']);
$routes->get('organizer/set-password', 'EventOrganizerPasswordController::showSetPasswordForm');
$routes->post('organizer/set-password', 'EventOrganizerPasswordController::setPassword', ['filter' => 'throttle:10,60']);
$routes->post('organizer/set-password/resend', 'EventOrganizerPasswordController::resendCode', ['filter' => 'throttle:10,60']);

$routes->group('organizer', ['filter' => 'organizerAuth'], static function (RouteCollection $routes) {
    $routes->get('dashboard', 'EventOrganizerController::dashboard');
    $routes->get('events/(:num)', 'EventOrganizerController::showEvent/$1');
    $routes->post('events/(:num)/fields', 'EventOrganizerController::addField/$1');
    $routes->post('events/(:num)/fields/(:num)/delete', 'EventOrganizerController::deleteField/$1/$2');
    $routes->post('events/(:num)/fields/(:num)/move-up', 'EventOrganizerController::moveFieldUp/$1/$2');
    $routes->post('events/(:num)/fields/(:num)/move-down', 'EventOrganizerController::moveFieldDown/$1/$2');
    $routes->get('events/(:num)/registrations/(:num)', 'EventOrganizerController::registrationAnswers/$1/$2');
});

// Super Admin / control plane — no tenant at all (see Filters.php's
// 'tenant' except list). Own auth, own session keys, own layout.
$routes->get('admin/login', 'Admin\AdminAuthController::showLogin');
$routes->post('admin/login', 'Admin\AdminAuthController::attemptLogin', ['filter' => 'throttle:8,60']);
$routes->get('admin/logout', 'Admin\AdminAuthController::logout');

$routes->group('admin', ['filter' => 'adminAuth'], static function (RouteCollection $routes) {
    $routes->get('/', 'Admin\DashboardController::index');

    // Hotels / tenants
    $routes->get('tenants', 'Admin\TenantController::index');
    $routes->get('tenants/new', 'Admin\TenantController::new');
    $routes->post('tenants', 'Admin\TenantController::create');
    $routes->get('tenants/(:num)', 'Admin\TenantController::show/$1');
    $routes->post('tenants/(:num)/status', 'Admin\TenantController::updateStatus/$1');

    // Plans
    $routes->get('plans', 'Admin\PlanController::index');
    $routes->get('plans/new', 'Admin\PlanController::new');
    $routes->post('plans', 'Admin\PlanController::create');
    $routes->get('plans/(:num)/edit', 'Admin\PlanController::edit/$1');
    $routes->post('plans/(:num)', 'Admin\PlanController::update/$1');
    $routes->post('plans/(:num)/toggle', 'Admin\PlanController::toggleActive/$1');

    // Subscriptions
    $routes->post('tenants/(:num)/subscription', 'Admin\SubscriptionController::assign/$1');
    $routes->post('subscriptions/(:num)/status', 'Admin\SubscriptionController::updateStatus/$1');

    // Billing & Invoices
    $routes->get('invoices', 'Admin\InvoiceController::index');
    $routes->get('tenants/(:num)/invoices/new', 'Admin\InvoiceController::new/$1');
    $routes->post('tenants/(:num)/invoices', 'Admin\InvoiceController::create/$1');
    $routes->get('invoices/(:num)', 'Admin\InvoiceController::show/$1');
    $routes->get('invoices/(:num)/pdf', 'Admin\InvoiceController::pdf/$1');
    $routes->post('invoices/(:num)/mark-paid', 'Admin\InvoiceController::markPaid/$1');
    $routes->post('invoices/(:num)/void', 'Admin\InvoiceController::void/$1');
    $routes->post('invoices/(:num)/hubtel-payment', 'Admin\InvoiceController::requestHubtelPayment/$1');

    // Transactions ledger
    $routes->get('transactions', 'Admin\TransactionController::index');

    // Admin users
    $routes->get('admins', 'Admin\AdminUserController::index');
    $routes->get('admins/new', 'Admin\AdminUserController::new');
    $routes->post('admins', 'Admin\AdminUserController::create');
    $routes->post('admins/(:num)/toggle-active', 'Admin\AdminUserController::toggleActive/$1');

    // Activity log
    $routes->get('activity-log', 'Admin\ActivityLogController::index');

    // Platform settings (Hubtel credentials for collecting from hotels)
    $routes->get('settings', 'Admin\SettingsController::index');
    $routes->post('settings', 'Admin\SettingsController::update');

    // Reports / system health
    $routes->get('reports', 'Admin\ReportController::index');
    $routes->get('system-health', 'Admin\SystemHealthController::index');
});

// Every route below requires a logged-in staff user for the current tenant.
$routes->group('', ['filter' => 'auth'], static function (RouteCollection $routes) {
    $routes->get('/', 'DashboardController::index');
    $routes->post('dashboard/generate-briefing', 'DashboardController::generateBriefing');
    $routes->get('notifications', 'NotificationController::index');
    $routes->post('notifications/mark-read', 'NotificationController::markRead');
    $routes->post('notifications/(:num)/mark-read', 'NotificationController::markOneRead/$1');

    // Shift schedule — every role reaches this (see
    // Config\Lookups::$permissionModules['shifts']); index() itself
    // branches manager (full week grid + approvals) vs everyone else
    // ("My Shifts" + swap/time-off requests). Manager-only actions
    // (assign, cancel, approve/deny) sit in the manager-only group below.
    $routes->get('shifts', 'ShiftController::index');
    $routes->post('shifts/(:num)/log-hours', 'ShiftController::logHours/$1');
    $routes->post('shifts/(:num)/request-swap', 'ShiftController::requestSwap/$1');
    $routes->post('time-off', 'ShiftController::requestTimeOff');

    // My Profile — self-service, any logged-in role, own account only
    // (see ProfileController's class doc comment). Distinct from
    // UserController's manager-only 'users/*' routes below, which
    // administer OTHER staff's accounts.
    $routes->get('profile', 'ProfileController::index');
    $routes->post('profile', 'ProfileController::update');
    $routes->post('profile/photo', 'ProfileController::uploadPhoto');
    $routes->post('profile/change-password', 'ProfileController::changePassword');

    // Front-of-house (manager + front_desk only). fnb has no access here
    // per the Phase 2 brief ("no access to room reservations, guest CRM
    // editing, or user management") — this group is what enforces that;
    // it was left as plain 'auth' in Phase 1 because front_desk/manager
    // were the only two roles that existed yet.
    $routes->group('', ['filter' => 'role:manager,front_desk'], static function (RouteCollection $routes) {
        // Staff Booking Assistant — read-only chat over real availability/rate/booking data (see Phase 5 AI brief).
        $routes->get('ai/assistant', 'AiAssistantController::index');
        $routes->post('ai/assistant/ask', 'AiAssistantController::ask');
        // WhatsApp Concierge — staff inbox (see ConciergeService/ConciergeWebhookController for how a conversation gets here).
        $routes->get('concierge', 'ConciergeController::index');
        $routes->get('concierge/(:num)', 'ConciergeController::show/$1');
        $routes->post('concierge/(:num)/messages/(:num)/send', 'ConciergeController::sendReply/$1/$2');
        $routes->post('concierge/(:num)/messages/(:num)/dismiss', 'ConciergeController::dismissReply/$1/$2');
        $routes->post('concierge/(:num)/resolve', 'ConciergeController::resolve/$1');
        // Room types
        $routes->get('room-types', 'RoomTypeController::index');
        $routes->get('room-types/new', 'RoomTypeController::new');
        $routes->post('room-types', 'RoomTypeController::create');
        $routes->get('room-types/(:num)/edit', 'RoomTypeController::edit/$1');
        $routes->post('room-types/(:num)', 'RoomTypeController::update/$1');
        $routes->post('room-types/(:num)/delete', 'RoomTypeController::delete/$1');
        $routes->post('room-types/(:num)/toggle-active', 'RoomTypeController::toggleActive/$1');

        // Rooms — full management (add/edit/delete). Viewing the status
        // board and updating a room's status live in the Housekeeping-
        // accessible group below, since housekeeping staff need those two
        // without needing to add/edit/delete rooms.
        $routes->get('rooms', 'RoomController::index');
        $routes->get('rooms/export', 'RoomController::exportCsv');
        $routes->get('rooms/new', 'RoomController::new');
        $routes->post('rooms', 'RoomController::create');
        $routes->get('rooms/(:num)/edit', 'RoomController::edit/$1');
        $routes->post('rooms/(:num)', 'RoomController::update/$1');
        $routes->post('rooms/(:num)/delete', 'RoomController::delete/$1');

        // Booking calendar + create-booking flow
        $routes->get('bookings', 'BookingController::index');
        $routes->get('bookings/calendar', 'BookingController::calendar');
        $routes->get('bookings/new', 'BookingController::searchForm');
        $routes->get('bookings/search', 'BookingController::searchResults');
        $routes->post('bookings/wizard/select-room', 'BookingWizardController::selectRoom');
        $routes->get('bookings/wizard/guest', 'BookingWizardController::guestForm');
        $routes->post('bookings/wizard/guest', 'BookingWizardController::saveGuest');
        $routes->get('bookings/wizard/details', 'BookingWizardController::detailsForm');
        $routes->post('bookings/wizard/details', 'BookingWizardController::saveDetails');
        $routes->get('bookings/wizard/services', 'BookingWizardController::servicesForm');
        $routes->post('bookings/wizard/services', 'BookingWizardController::saveServices');
        $routes->get('bookings/wizard/payment', 'BookingWizardController::paymentForm');
        $routes->post('bookings/wizard/payment', 'BookingWizardController::confirm');
        $routes->get('bookings/wizard/confirmation/(:num)', 'BookingWizardController::confirmation/$1');
        $routes->get('bookings/wizard/confirmation/(:num)/pdf', 'BookingWizardController::confirmationPdf/$1');
        $routes->post('bookings/wizard/confirmation/(:num)/email', 'BookingWizardController::sendConfirmationEmail/$1');
        $routes->post('bookings/wizard/confirmation/(:num)/sms', 'BookingWizardController::sendConfirmationSms/$1');

        // Group/bulk bookings — several rooms reserved together for one
        // organization or party (see BookingGroupController). Own route
        // prefix, not nested under bookings/wizard/*, since it's a
        // completely separate (stateless) two-step flow.
        $routes->get('group-bookings', 'BookingGroupController::index');
        $routes->get('group-bookings/new', 'BookingGroupController::new');
        $routes->post('group-bookings/rooms', 'BookingGroupController::findRooms');
        $routes->post('group-bookings', 'BookingGroupController::create');
        $routes->get('group-bookings/(:num)', 'BookingGroupController::show/$1');
        $routes->post('group-bookings/(:num)/cancel', 'BookingGroupController::cancel/$1');
        $routes->post('group-bookings/(:num)/rooms/(:num)/cancel', 'BookingGroupController::cancelRoom/$1/$2');

        $routes->get('bookings/(:num)', 'BookingController::show/$1');
        $routes->post('bookings/(:num)/cancel', 'BookingController::cancel/$1');
        $routes->post('bookings/(:num)/no-show', 'BookingController::markNoShow/$1');
        $routes->get('bookings/(:num)/change-room', 'BookingController::changeRoomForm/$1');
        $routes->post('bookings/(:num)/change-room', 'BookingController::changeRoom/$1');

        // Check-in / check-out
        $routes->get('checkin', 'CheckInController::index');
        $routes->get('checkin/(:num)', 'CheckInController::form/$1');
        $routes->post('checkin/(:num)', 'CheckInController::process/$1');

        $routes->get('checkout', 'CheckOutController::index');
        $routes->get('checkout/(:num)', 'CheckOutController::form/$1');
        $routes->post('checkout/(:num)/payment', 'CheckOutController::recordPayment/$1');
        $routes->post('checkout/(:num)', 'CheckOutController::process/$1');

        // Folios — operational actions only (adding charges/payments,
        // corporate tagging, loyalty redemption). Viewing a folio and
        // invoicing moved to the Accounts-accessible group below, since
        // that's the read/invoice-only slice the new Accounts role gets.
        $routes->post('folios/(:num)/charges', 'FolioController::addCharge/$1');
        $routes->post('folios/(:num)/payments', 'FolioController::addPayment/$1');
        $routes->post('folios/(:num)/refund', 'FolioController::refundPayment/$1');
        $routes->post('folios/(:num)/allocations', 'FolioController::addAllocation/$1');
        $routes->post('folios/(:num)/hubtel-payment', 'FolioController::requestHubtelPayment/$1');
        $routes->post('folios/(:num)/corporate-account', 'FolioController::setCorporateAccount/$1');
        $routes->post('folios/(:num)/redeem-points', 'FolioController::redeemPoints/$1');

        // Guests (CRM)
        $routes->get('guests', 'GuestController::index');
        $routes->get('guests/new', 'GuestController::new');
        $routes->post('guests', 'GuestController::create');
        $routes->get('guests/lookup', 'GuestController::lookup');
        $routes->get('guests/search-ajax', 'GuestController::searchAjax');
        $routes->post('guests/smart-search', 'GuestController::smartSearch');
        $routes->get('guests/(:num)', 'GuestController::show/$1');
        $routes->get('guests/(:num)/edit', 'GuestController::edit/$1');
        $routes->post('guests/(:num)', 'GuestController::update/$1');
        $routes->post('guests/(:num)/log-communication', 'GuestController::logCommunication/$1');
        $routes->post('guests/(:num)/join-dining', 'GuestController::joinDining/$1');
        $routes->post('guests/(:num)/renew-dining', 'GuestController::renewDining/$1');
        $routes->post('guests/(:num)/cancel-dining', 'GuestController::cancelDining/$1');
        $routes->post('guests/(:num)/log-benefit-usage', 'GuestController::logBenefitUsage/$1');

        // Corporate accounts, folio viewing/invoicing, and Housekeeping +
        // Maintenance used to live in this group too — see the
        // Accounts-accessible and Housekeeping-accessible groups just
        // below, split out so those two new (Phase 4) roles get exactly
        // this slice each without also getting bookings/check-in-out/
        // guest-CRM-editing/user-management (least-privilege defaults).
    });

    // Corporate accounts + folio viewing/invoicing — the new Accounts
    // role's bookkeeping/invoicing slice (least-privilege default from
    // the build plan); front desk keeps using this group too, unchanged
    // from before the split. Operational folio actions (adding a charge/
    // payment, redeeming loyalty points, etc.) stay in the front-desk-
    // only group above — Accounts is read/invoice-only.
    $routes->group('', ['filter' => 'role:manager,front_desk,accounts'], static function (RouteCollection $routes) {
        $routes->get('corporate-accounts', 'CorporateAccountController::index');
        $routes->get('corporate-accounts/new', 'CorporateAccountController::new');
        $routes->post('corporate-accounts', 'CorporateAccountController::create');
        $routes->get('corporate-accounts/lookup', 'CorporateAccountController::lookup');
        $routes->get('corporate-accounts/(:num)', 'CorporateAccountController::show/$1');
        $routes->get('corporate-accounts/(:num)/edit', 'CorporateAccountController::edit/$1');
        $routes->post('corporate-accounts/(:num)', 'CorporateAccountController::update/$1');
        $routes->get('corporate-accounts/(:num)/statement', 'CorporateAccountController::statement/$1');

        $routes->get('folios', 'FolioController::index');
        $routes->get('folios/(:num)', 'FolioController::show/$1');
        $routes->get('folios/(:num)/invoice', 'InvoiceController::folioInvoice/$1');
        $routes->post('folios/(:num)/email-invoice', 'FolioController::emailInvoice/$1');
        $routes->get('payments', 'PaymentController::index');
    });

    // Room status board + Housekeeping + Maintenance — the new
    // Housekeeping role's whole world (least-privilege default from the
    // build plan): no rooms CRUD, no bookings/check-in-out/guest CRM.
    // Front desk keeps using this group too, unchanged from before.
    $routes->group('', ['filter' => 'role:manager,front_desk,housekeeping'], static function (RouteCollection $routes) {
        $routes->get('rooms/board', 'RoomController::board');
        $routes->get('rooms/(:num)', 'RoomController::show/$1');
        $routes->post('rooms/(:num)/status', 'RoomController::updateStatus/$1');

        $routes->get('housekeeping', 'HousekeepingController::index');
        $routes->get('housekeeping/new', 'HousekeepingController::new');
        $routes->post('housekeeping', 'HousekeepingController::create');
        $routes->post('housekeeping/(:num)/start', 'HousekeepingController::start/$1');
        $routes->post('housekeeping/(:num)/complete', 'HousekeepingController::complete/$1');
        $routes->post('housekeeping/(:num)/inspect', 'HousekeepingController::inspect/$1');
        $routes->get('housekeeping/(:num)/edit', 'HousekeepingController::edit/$1');
        $routes->post('housekeeping/(:num)', 'HousekeepingController::update/$1');
        $routes->get('housekeeping/(:num)', 'HousekeepingController::show/$1');

        $routes->get('maintenance', 'MaintenanceController::index');
        $routes->get('maintenance/new', 'MaintenanceController::new');
        $routes->post('maintenance', 'MaintenanceController::create');
        $routes->post('maintenance/(:num)/start', 'MaintenanceController::start/$1');
        $routes->post('maintenance/(:num)/complete', 'MaintenanceController::complete/$1');
        $routes->get('maintenance/(:num)/edit', 'MaintenanceController::edit/$1');
        $routes->post('maintenance/(:num)', 'MaintenanceController::update/$1');
        $routes->get('maintenance/(:num)', 'MaintenanceController::show/$1');
    });

    // F&B (manager + fnb roles only — table reservations, events; POS/
    // kitchen display join this group once built). front_desk has no
    // access here per the Phase 2 brief, same way fnb has no access to
    // room reservations/guest CRM/user management.
    $routes->group('', ['filter' => 'role:manager,fnb'], static function (RouteCollection $routes) {
        $routes->post('ai/draft-description', 'AiWriterController::draftDescription');
        // Unified Restaurant hub — a single landing page tying together
        // the F&B screens below (orders, reservations, menu) with stats
        // and a sales-today chart, matching a user-supplied mockup.
        $routes->get('restaurant', 'RestaurantController::index');

        $routes->get('dining/areas', 'DiningController::areas');
        $routes->post('dining/areas', 'DiningController::createArea');
        $routes->post('dining/areas/(:num)/delete', 'DiningController::deleteArea/$1');
        $routes->get('dining/tables', 'DiningController::tables');
        $routes->post('dining/tables', 'DiningController::createTable');
        $routes->post('dining/tables/(:num)/delete', 'DiningController::deleteTable/$1');

        $routes->get('dining/reservations', 'RestaurantReservationController::index');
        $routes->get('dining/reservations/new', 'RestaurantReservationController::new');
        $routes->post('dining/reservations', 'RestaurantReservationController::create');
        $routes->get('dining/reservations/lookup-guest', 'RestaurantReservationController::lookupGuest');
        $routes->post('dining/reservations/(:num)/seat', 'RestaurantReservationController::seat/$1');
        $routes->post('dining/reservations/(:num)/complete', 'RestaurantReservationController::complete/$1');
        $routes->post('dining/reservations/(:num)/cancel', 'RestaurantReservationController::cancel/$1');
        $routes->post('dining/reservations/(:num)/no-show', 'RestaurantReservationController::noShow/$1');

        $routes->get('events', 'EventController::index');
        $routes->get('events/new', 'EventController::new');
        $routes->post('events', 'EventController::create');
        $routes->get('events/(:num)', 'EventController::show/$1');
        $routes->get('events/(:num)/edit', 'EventController::edit/$1');
        $routes->post('events/(:num)', 'EventController::update/$1');
        $routes->post('events/(:num)/fields', 'EventController::addField/$1');
        $routes->post('events/(:num)/fields/(:num)/delete', 'EventController::deleteField/$1/$2');
        $routes->post('events/(:num)/fields/(:num)/move-up', 'EventController::moveFieldUp/$1/$2');
        $routes->post('events/(:num)/fields/(:num)/move-down', 'EventController::moveFieldDown/$1/$2');
        $routes->get('events/(:num)/registrations/(:num)', 'EventController::registrationAnswers/$1/$2');
        $routes->post('events/(:num)/invite-organizer', 'EventController::inviteOrganizer/$1');
        $routes->post('events/(:num)/resend-organizer-invite', 'EventController::resendOrganizerInvite/$1');

        $routes->get('event-clients', 'EventClientController::index');
        $routes->get('event-clients/new', 'EventClientController::new');
        $routes->post('event-clients', 'EventClientController::create');
        $routes->get('event-clients/lookup', 'EventClientController::lookup');
        $routes->get('event-clients/(:num)', 'EventClientController::show/$1');
        $routes->get('event-clients/(:num)/edit', 'EventClientController::edit/$1');
        $routes->post('event-clients/(:num)', 'EventClientController::update/$1');

        // POS: menu, order entry, kitchen display
        $routes->get('pos/menu', 'MenuController::index');
        $routes->post('pos/menu', 'MenuController::create');
        $routes->post('pos/menu/(:num)/toggle', 'MenuController::toggleAvailable/$1');
        $routes->post('pos/menu/(:num)/delete', 'MenuController::delete/$1');
        $routes->get('pos/menu/(:num)/edit', 'MenuController::edit/$1');
        $routes->post('pos/menu/(:num)', 'MenuController::update/$1');

        $routes->get('pos/orders', 'PosOrderController::index');
        $routes->get('pos/orders/new', 'PosOrderController::new');
        $routes->post('pos/orders', 'PosOrderController::create');
        $routes->get('pos/orders/lookup-guest', 'PosOrderController::lookupGuest');
        $routes->get('pos/orders/lookup-corporate', 'PosOrderController::lookupCorporateAccount');
        $routes->get('pos/orders/(:num)', 'PosOrderController::show/$1');
        $routes->post('pos/orders/(:num)/items', 'PosOrderController::addItem/$1');
        $routes->post('pos/orders/(:num)/send', 'PosOrderController::sendToKitchen/$1');
        $routes->post('pos/orders/(:num)/served', 'PosOrderController::markServed/$1');
        $routes->post('pos/orders/(:num)/settle-direct', 'PosOrderController::settleDirect/$1');
        $routes->post('pos/orders/(:num)/settle-room', 'PosOrderController::settleToRoom/$1');

        $routes->get('pos/kitchen', 'KitchenDisplayController::index');
        $routes->post('pos/kitchen/items/(:num)/ready', 'KitchenDisplayController::markItemReady/$1');
    });

    // Complaint & service recovery — the one module every department can
    // reach (a complaint can come from any of them), so it gets its own
    // group rather than sitting inside either front-of-house group above.
    $routes->group('', ['filter' => 'role:manager,front_desk,fnb'], static function (RouteCollection $routes) {
        $routes->get('complaints', 'ComplaintController::index');
        $routes->get('complaints/new', 'ComplaintController::new');
        $routes->post('complaints', 'ComplaintController::create');
        $routes->get('complaints/lookup-guest', 'ComplaintController::lookupGuest');
        $routes->get('complaints/(:num)', 'ComplaintController::show/$1');
        $routes->post('complaints/(:num)/assign', 'ComplaintController::assign/$1');
        $routes->post('complaints/(:num)/resolve', 'ComplaintController::resolve/$1');
        $routes->post('complaints/(:num)/follow-up', 'ComplaintController::followUp/$1');
    });

    // Manager-only: staff user management + activity log
    $routes->group('', ['filter' => 'role:manager'], static function (RouteCollection $routes) {
        $routes->get('users', 'UserController::index');
        $routes->get('users/new', 'UserController::new');
        $routes->post('users', 'UserController::create');
        $routes->get('users/(:num)', 'UserController::show/$1');
        $routes->post('users/(:num)/toggle-active', 'UserController::toggleActive/$1');
        $routes->post('users/(:num)/reset-password', 'UserController::resetPassword/$1');
        $routes->post('users/(:num)/photo', 'UserController::uploadPhoto/$1');
        $routes->post('users/(:num)/documents', 'UserController::uploadDocument/$1');
        $routes->get('users/(:num)/documents/(:num)/download', 'UserController::downloadDocument/$1/$2');
        $routes->post('users/(:num)/documents/(:num)/delete', 'UserController::deleteDocument/$1/$2');
        $routes->post('users/(:num)/salary', 'UserController::updateSalary/$1');
        $routes->post('users/(:num)/identification', 'UserController::updateIdentification/$1');

        $routes->get('activity-log', 'ActivityLogController::index');
        $routes->get('activity-log/export.csv', 'ActivityLogController::exportCsv');

        $routes->get('communication', 'CommunicationController::index');
        $routes->get('guest-message-drafts', 'GuestMessageDraftController::index');
        $routes->post('guest-message-drafts/(:num)/send', 'GuestMessageDraftController::send/$1');
        $routes->post('guest-message-drafts/(:num)/dismiss', 'GuestMessageDraftController::dismiss/$1');
        $routes->post('communication/send', 'CommunicationController::send');

        // Email marketing campaigns
        $routes->get('campaigns', 'CampaignController::index');
        $routes->get('campaigns/new', 'CampaignController::new');
        $routes->post('campaigns', 'CampaignController::create');
        $routes->get('campaigns/preview-audience', 'CampaignController::previewAudience');
        $routes->get('campaigns/(:num)', 'CampaignController::show/$1');
        $routes->post('campaigns/(:num)/send', 'CampaignController::send/$1');

        $routes->get('settings', 'SettingsController::index');
        $routes->post('settings', 'SettingsController::update');
        $routes->post('settings/hotel-info', 'SettingsController::updateHotelInfo');
        $routes->post('settings/test-sms', 'SettingsController::testSms', ['filter' => 'throttle:5,60']);
        $routes->post('settings/test-email', 'SettingsController::testEmail', ['filter' => 'throttle:5,60']);
        $routes->post('settings/test-whatsapp', 'SettingsController::testWhatsapp', ['filter' => 'throttle:5,60']);

        $routes->get('role-management', 'RoleManagementController::index');
        $routes->post('role-management/(:segment)', 'RoleManagementController::update/$1');

        $routes->get('backup', 'BackupController::index');
        $routes->post('backup/settings', 'BackupController::updateSettings');
        $routes->post('backup/run-now', 'BackupController::runNow');
        $routes->get('backup/export', 'BackupController::export');
        $routes->post('backup/restore', 'BackupController::restore');

        // Shift schedule — manager-only actions (assign/cancel a shift,
        // approve/deny a swap or time-off request). Viewing + self-service
        // requests are open to every role, in the 'auth' group above.
        $routes->post('shifts', 'ShiftController::assign');
        $routes->post('shifts/(:num)/cancel', 'ShiftController::cancelShift/$1');
        $routes->post('shift-swaps/(:num)/approve', 'ShiftController::approveSwap/$1');
        $routes->post('shift-swaps/(:num)/deny', 'ShiftController::denySwap/$1');
        $routes->post('time-off/(:num)/approve', 'ShiftController::approveTimeOff/$1');
        $routes->post('time-off/(:num)/deny', 'ShiftController::denyTimeOff/$1');
        $routes->post('expenses/(:num)/approve', 'ExpenseController::approve/$1');
        $routes->post('expenses/(:num)/deny', 'ExpenseController::deny/$1');

        $routes->get('services', 'ServiceCatalogController::index');
        $routes->get('services/new', 'ServiceCatalogController::new');
        $routes->post('services', 'ServiceCatalogController::create');
        $routes->get('services/(:num)/edit', 'ServiceCatalogController::edit/$1');
        $routes->post('services/(:num)', 'ServiceCatalogController::update/$1');
        $routes->post('services/(:num)/toggle-active', 'ServiceCatalogController::toggleActive/$1');
        $routes->post('services/(:num)/delete', 'ServiceCatalogController::delete/$1');

        $routes->get('inventory', 'InventoryController::index');
        $routes->get('inventory/new', 'InventoryController::new');
        $routes->post('inventory', 'InventoryController::create');
        $routes->get('inventory/(:num)/edit', 'InventoryController::edit/$1');
        $routes->post('inventory/(:num)', 'InventoryController::update/$1');
        $routes->post('inventory/(:num)/delete', 'InventoryController::delete/$1');
        $routes->post('inventory/(:num)/receive', 'InventoryController::receiveStock/$1');
        $routes->post('inventory/(:num)/consume', 'InventoryController::consumeStock/$1');
        $routes->post('inventory/(:num)/adjust', 'InventoryController::adjustStock/$1');
        $routes->get('inventory/(:num)', 'InventoryController::show/$1');

        $routes->get('ai-modules', 'AiInsightsController::index');
        $routes->get('ai-modules/settings', 'AiInsightsController::settings');
        $routes->post('ai-modules/settings', 'AiInsightsController::updateSettings');

        // Phase 5 AI features — real Claude API calls, distinct from the
        // rule-based ai-modules above (see ClaudeService's class doc
        // comment). ai-call-log is the shared audit trail every feature
        // below writes to.
        $routes->get('ai-call-log', 'AiCallLogController::index');
        $routes->get('ai/alerts', 'AiAlertController::index');
        $routes->post('ai/alerts/(:num)/dismiss', 'AiAlertController::dismiss/$1');
        $routes->post('reports/narrate-kpis', 'ReportController::narrateKpis');
        $routes->post('maintenance/suggest-triage', 'MaintenanceController::suggestTriage');

        // Staff-only data (who worked how much/how well) — kept out of
        // the Accounts role's Reports access below; this is HR-adjacent,
        // not billing.
        $routes->get('reports/staff-performance', 'StaffPerformanceController::index');

        // Payroll & salaries — manager-only (see Lookups::$permissionModules['payroll']).
        $routes->get('payroll', 'PayrollController::index');
        $routes->get('payroll/new', 'PayrollController::new');
        $routes->post('payroll', 'PayrollController::create');
        $routes->get('payroll/(:num)', 'PayrollController::show/$1');
        $routes->post('payroll/(:num)/finalize', 'PayrollController::finalize/$1');
        $routes->post('payroll/(:num)/mark-paid', 'PayrollController::markPaid/$1');
        $routes->post('payroll/(:num)/payslips/(:num)', 'PayrollController::updatePayslip/$1/$2');
        $routes->post('payroll/(:num)/payslips/(:num)/line-items', 'PayrollController::addLineItem/$1/$2');
        $routes->post('payroll/(:num)/payslips/(:num)/line-items/(:num)/delete', 'PayrollController::deleteLineItem/$1/$2/$3');
        $routes->get('payroll/(:num)/payslips/(:num)/pdf', 'PayrollController::payslipPdf/$1/$2');
    });

    // Billing & Finance + Reports + Rate Plans — the new Accounts role's
    // remaining slice (least-privilege default from the build plan);
    // manager keeps using this group too, unchanged from before the
    // split. None of the operational or staff-management routes above.
    $routes->group('', ['filter' => 'role:manager,accounts'], static function (RouteCollection $routes) {
        $routes->get('billing-finance', 'BillingFinanceController::index');

        $routes->get('reports', 'ReportController::index');
        $routes->get('reports/export.csv', 'ReportController::exportCsv');
        $routes->get('reports/export.pdf', 'ReportController::exportPdf');
        $routes->get('reports/profit-loss', 'ProfitLossController::index');

        $routes->get('expenses', 'ExpenseController::index');
        $routes->get('expenses/new', 'ExpenseController::new');
        $routes->post('expenses', 'ExpenseController::create');
        $routes->get('expenses/(:num)', 'ExpenseController::show/$1');

        $routes->get('rate-plans', 'RatePlanController::index');
        $routes->get('rate-plans/new', 'RatePlanController::new');
        $routes->post('rate-plans', 'RatePlanController::create');
        $routes->get('rate-plans/(:num)/edit', 'RatePlanController::edit/$1');
        $routes->post('rate-plans/(:num)', 'RatePlanController::update/$1');
        $routes->post('rate-plans/(:num)/delete', 'RatePlanController::delete/$1');
    });
});
