<?php
namespace Bookaro\Controller\Admin;

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct call to a custom plugin table ($wpdb->prefix); the query is prepared and booking state must be read fresh (caching would serve stale admin data).

use Bookaro\Repository\BookingRepository;
use Bookaro\Service\BookingService;
use Bookaro\Utils\TemplateLoader;
use function add_menu_page;
use function admin_url;
use function absint;
use function current_user_can;
use function esc_html__;
use function esc_html_e;
use function esc_url;
use function get_option;
use function bookaro_get_option;
use function sanitize_key;
use function sanitize_text_field;
use function sanitize_textarea_field;
use function wp_unslash;
use function wp_die;
use function wp_safe_redirect;
use function wp_verify_nonce;
use function wp_timezone;
use function _n;

/**
 * Handles the WP-Admin menu and bookings list screen.
 */
class BookingAdminController
{
    private BookingRepository $db;
    private BookingService $service;

    /**
     * @param BookingRepository $db Database service for fetching bookings.
     * @param BookingService    $service Booking business logic service.
     */
    public function __construct( BookingRepository $db, BookingService $service )
    {
        $this->db      = $db;
        $this->service = $service;
    }

    /**
     * Register our menu page.
     */
    public function register_admin_menu(): void
    {
        $capability = function_exists( 'bookaro_get_admin_menu_capability' )
            ? bookaro_get_admin_menu_capability()
            : 'manage_options';
        $hook = add_menu_page(
            __( 'MyBookaro', 'mybookaro' ),
            __( 'MyBookaro', 'mybookaro' ),
            $capability,
            'bookaro-bookings',
            [ $this, 'render_bookings_page' ],
            self::get_admin_menu_icon(),
            58.6
        );

        // Per-row status/delete actions need to run BEFORE WP emits the admin
        // header, otherwise wp_safe_redirect()'s Location header can't be
        // set and the user lands on a blank page (the redirect's exit kills
        // the rest of the page render). load-{hook} fires early enough.
        if ( $hook ) {
            add_action( 'load-' . $hook, [ $this, 'handle_booking_row_actions' ] );
        }

        // Rename the default first submenu label to "Bookings".
        global $submenu;
        if ( isset( $submenu['bookaro-bookings'][0][0] ) ) {
            $submenu['bookaro-bookings'][0][0] = __( 'Bookings', 'mybookaro' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentionally relabeling our own admin submenu item; WP exposes no API for this.
        }
    }

    /**
     * Process per-row and bulk Approve / Reject / Cancel / Complete /
     * No-show / Delete actions from the bookings list before any HTML
     * has been emitted.
     *
     * Bulk submissions carry a `booking[]` checkbox array, which is how
     * we distinguish them from per-row clicks (which only carry `id`).
     */
    public function handle_booking_row_actions(): void
    {
        $can_manage_admin = function_exists( 'bookaro_current_user_can_manage_admin' )
            ? bookaro_current_user_can_manage_admin()
            : current_user_can( 'manage_options' );
        if ( ! $can_manage_admin ) {
            return;
        }

        if ( isset( $_REQUEST['booking'] ) ) {
            // Bulk-action handler lives on the table so it can reuse the
            // table's own filter-preserving redirect URL builder. Calling
            // it from load-{hook} (not render) is what fixes the
            // headers-already-sent blank-page bug.
            $table = new Bookings_List_Table( $this->db );
            $table->process_bulk_action( $this->service );
            return;
        }

        $action = sanitize_key( wp_unslash( $_GET['action'] ?? '' ) );
        if ( '' === $action ) {
            return;
        }

        $id    = absint( wp_unslash( $_GET['id'] ?? 0 ) );
        $nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ?? '' ) );
        if ( $id < 1 ) {
            return;
        }

        if ( 'delete' === $action ) {
            if ( wp_verify_nonce( $nonce, 'delete_booking_' . $id ) ) {
                $this->db->delete_booking( $id );
                self::set_admin_notice_transient( 'deleted' );
                wp_safe_redirect( admin_url( 'admin.php?page=bookaro-bookings' ) );
                exit;
            }
            return;
        }

        if ( 0 === strpos( $action, 'status_' ) ) {
            $status  = sanitize_key( substr( $action, strlen( 'status_' ) ) );
            $allowed = [ 'approved', 'rejected', 'cancelled', 'completed', 'no_show' ];
            if (
                in_array( $status, $allowed, true )
                && wp_verify_nonce( $nonce, 'bookaro_status_' . $id . '_' . $status )
            ) {
                // Bug 44: respect the Bug 26 state machine on the admin per-row
                // action handler too. REST update_status / bulk_update_status
                // both gate through validateStatusChange and reject terminal
                // ->non-terminal transitions; this per-row "Approve / Reject /
                // Cancel" link path was bypassing that check, letting admin
                // silently flip a completed booking back to pending via a
                // single click. The bookaro_validate_booking_status_change
                // filter still runs last for sites that need to relax the
                // rule, so admin override remains possible — just no longer
                // silent.
                $validation = $this->service->validateStatusChange( $id, $status );
                if ( $validation instanceof \WP_Error ) {
                    self::set_admin_notice_transient( 'status_locked' );
                    wp_safe_redirect( admin_url( 'admin.php?page=bookaro-bookings' ) );
                    exit;
                }
                $this->service->updateBookingStatus( $id, $status );
                self::set_admin_notice_transient( 'status_' . $status );
                wp_safe_redirect( admin_url( 'admin.php?page=bookaro-bookings' ) );
                exit;
            }
        }
    }

    /**
     * Stash a one-shot admin notice for the next page load.
     *
     * Static + public so the list table's bulk-action handler can call
     * it after applying a bulk operation.
     *
     * @param string $key   Notice slug — used to look up the display string.
     * @param int    $count How many bookings the action affected (1 for per-row).
     */
    public static function set_admin_notice_transient( string $key, int $count = 1 ): void
    {
        if ( function_exists( 'set_transient' ) && function_exists( 'get_current_user_id' ) ) {
            set_transient(
                'bookaro_admin_action_notice_' . get_current_user_id(),
                [ 'key' => $key, 'count' => max( 1, $count ) ],
                30
            );
        }
    }

    /**
     * Inline monochrome SVG mark used as the WP admin menu icon.
     *
     * Returned as a base64 data-URI so the icon ships without an extra
     * HTTP request and WP applies its color-scheme filter to recolor it.
     * The mark mirrors the brand ribbon used elsewhere — bookmark form
     * with three booking-state lanes.
     *
     * @return string data:image/svg+xml;base64,... URI
     */
    private static function get_admin_menu_icon(): string
    {
        $svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72">'
            . '<path d="M 20 18 A 6 6 0 0 1 26 12 L 46 12 A 6 6 0 0 1 52 18 L 52 62 L 38.5 46 Q 36 43 33.5 46 L 20 62 Z" fill="black"/>'
            . '<rect x="26" y="20" width="20" height="4" rx="2" fill="black" opacity="0.45"/>'
            . '<rect x="26" y="28" width="20" height="4" rx="2" fill="white"/>'
            . '<rect x="26" y="36" width="14" height="4" rx="2" fill="black" opacity="0.45"/>'
            . '</svg>';

        return 'data:image/svg+xml;base64,' . base64_encode( $svg );
    }

    /**
     * Reorder submenus under MyBookaro based on purpose.
     */
    public function reorder_submenus(): void
    {
        global $submenu;
        $parent = 'bookaro-bookings';
        if ( empty( $submenu[ $parent ] ) || ! is_array( $submenu[ $parent ] ) ) {
            return;
        }

        // Ensure the first submenu label reads "Bookings" (WP defaults to the parent label).
        foreach ( $submenu[ $parent ] as $index => $item ) {
            if ( ( $item[2] ?? '' ) === 'bookaro-bookings' ) {
                $submenu[ $parent ][ $index ][0] = __( 'Bookings', 'mybookaro' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentionally relabeling our own admin submenu item; WP exposes no API for this.
                break;
            }
        }

        $desired = [
            'bookaro-bookings',        // Bookings (main)
            'bookaro-getting-started', // Onboarding (MyListing)
            'bookaro-setup-wizard',    // Onboarding (Generic)
            'bookaro-booking-settings',
            'bookaro-custom-forms',
            'bookaro-profiles',
            'bookaro-overview',        // Pro reports overview (if enabled)
            'bookaro-payouts',         // Pro payouts (if enabled)
            'bookaro-logs',
            'bookaro-system-status',
            'bookaro-pro-license',
            'bookaro-upgrade',
        ];
        if ( function_exists( 'bookaro_feature_enabled' ) && bookaro_feature_enabled( 'exports' ) ) {
            $desired[] = 'bookaro-export-log';
        }
        if ( function_exists( 'bookaro_is_pro_active' ) && bookaro_is_pro_active() ) {
            $desired = array_values( array_diff( $desired, [ 'bookaro-upgrade' ] ) );
        }

        $ordered = [];
        $used = [];
        foreach ( $desired as $slug ) {
            foreach ( $submenu[ $parent ] as $index => $item ) {
                if ( ( $item[2] ?? '' ) === $slug ) {
                    $ordered[] = $item;
                    $used[ $index ] = true;
                }
            }
        }

        foreach ( $submenu[ $parent ] as $index => $item ) {
            if ( ! isset( $used[ $index ] ) ) {
                $ordered[] = $item;
            }
        }

        $submenu[ $parent ] = $ordered; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentionally reordering our own admin submenu; WP exposes no API for this.
    }

    /**
     * Output the bookings list table.
     *
     * Per-row Approve / Reject / Cancel / Complete / No-show / Delete actions
     * are handled by `handle_booking_row_actions()` on the `load-{hook}` action
     * (registered in `register_admin_menu()`) so the post-action redirect can
     * set a Location header before any admin HTML is emitted.
     */
    public function render_bookings_page(): void
    {
        $can_manage_admin = function_exists( 'bookaro_current_user_can_manage_admin' )
            ? bookaro_current_user_can_manage_admin()
            : current_user_can( 'manage_options' );
        if ( ! $can_manage_admin ) {
            wp_die(esc_html__('You do not have permission to access this page.', 'mybookaro'));
        }

        $action = sanitize_key( wp_unslash( $_GET['action'] ?? '' ) );

        // Surface a one-shot success notice if the load-{hook} handler just
        // processed a per-row or bulk action and redirected here.
        $notice = function_exists( 'get_transient' ) && function_exists( 'get_current_user_id' )
            ? get_transient( 'bookaro_admin_action_notice_' . get_current_user_id() )
            : null;
        // Legacy string format from older releases — coerce to the new array shape.
        if ( is_string( $notice ) && '' !== $notice ) {
            $notice = [ 'key' => $notice, 'count' => 1 ];
        }
        if ( is_array( $notice ) && ! empty( $notice['key'] ) ) {
            delete_transient( 'bookaro_admin_action_notice_' . get_current_user_id() );
            $notice_key = (string) $notice['key'];
            $count      = max( 1, (int) ( $notice['count'] ?? 1 ) );
            $messages   = [
                /* translators: %d: number of bookings deleted. */
                'deleted'           => _n( '%d booking deleted.', '%d bookings deleted.', $count, 'mybookaro' ),
                /* translators: %d: number of bookings marked as approved. */
                'status_approved'   => _n( '%d booking marked as approved.', '%d bookings marked as approved.', $count, 'mybookaro' ),
                /* translators: %d: number of bookings marked as rejected. */
                'status_rejected'   => _n( '%d booking marked as rejected.', '%d bookings marked as rejected.', $count, 'mybookaro' ),
                /* translators: %d: number of bookings marked as cancelled. */
                'status_cancelled'  => _n( '%d booking marked as cancelled.', '%d bookings marked as cancelled.', $count, 'mybookaro' ),
                /* translators: %d: number of bookings marked as completed. */
                'status_completed'  => _n( '%d booking marked as completed.', '%d bookings marked as completed.', $count, 'mybookaro' ),
                /* translators: %d: number of bookings marked as no-show. */
                'status_no_show'    => _n( '%d booking marked as no-show.', '%d bookings marked as no-show.', $count, 'mybookaro' ),
                // Bug 44: surfaced when admin status changes are refused by
                // validateStatusChange (e.g. trying to flip a completed
                // booking back to pending). The renderer needs the
                // error-notice CSS class so admins notice the rejection.
                /* translators: %d: number of status changes that were refused. */
                'status_locked'     => _n(
                    '%d status change was refused because the booking is in a terminal state.',
                    '%d status changes were refused because those bookings are in terminal states.',
                    $count,
                    'mybookaro'
                ),
            ];
            if ( isset( $messages[ $notice_key ] ) ) {
                // Both singular and plural forms now carry the %d placeholder,
                // so always sprintf the count in (singular renders e.g. "1 booking deleted.").
                $message = sprintf( $messages[ $notice_key ], $count );
                $css_class = ( 'status_locked' === $notice_key ) ? 'notice-error' : 'notice-success';
                printf(
                    '<div class="notice %s is-dismissible"><p>%s</p></div>',
                    esc_attr( $css_class ),
                    esc_html( $message )
                );
            }
        }

        if ( 'edit' === $action ) {
            $settings = get_option( 'bookaro_settings', [] );
            if ( empty( $settings['allow_admin_edit_bookings'] ) ) {
                echo '<div class="notice notice-error"><p>' . esc_html__('Editing is disabled.', 'mybookaro') . '</p></div>';
                return;
            }
            $id = absint( wp_unslash( $_GET['id'] ?? 0 ) );
            $nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ?? '' ) );
            if ( ! $id || ! wp_verify_nonce( $nonce, 'edit_booking_' . $id ) ) {
                wp_die( esc_html__( 'Invalid booking edit request.', 'mybookaro' ) );
            }
            $this->render_edit_form( $id );
            return;
        }

        $table = new Bookings_List_Table( $this->db );
        $table->prepare_items();
        ?>
        <div class="wrap bookaro-admin bookaro-admin-bookings">
            <div class="bookaro-card bookaro-hero bookaro-bookings-hero">
                <div class="bookaro-hero__content">
                    <div class="bookaro-hero__brand">
                        <img src="<?php echo esc_url( BOOKARO_PLUGIN_URL . 'assets/img/mb-logo.svg' ); ?>" alt="<?php esc_attr_e( 'MyBookaro', 'mybookaro' ); ?>" width="120" height="36" />
                    </div>
                    <h1><?php esc_html_e( 'Bookings', 'mybookaro' ); ?></h1>
                    <p class="description"><?php esc_html_e( 'Review recent activity, filter bookings, and take action.', 'mybookaro' ); ?></p>
                </div>
                <div class="bookaro-hero__actions">
                    <a class="button button-secondary" href="<?php echo esc_url( admin_url( 'admin.php?page=bookaro-booking-settings&tab=general' ) ); ?>">
                        <?php esc_html_e( 'Open settings', 'mybookaro' ); ?>
                    </a>
                </div>
            </div>
            <div class="bookaro-card bookaro-bookings-table">
                <form method="get">
                    <input type="hidden" name="page" value="bookaro-bookings" />
                    <?php
                    $table->search_box( esc_html__('Search Bookings', 'mybookaro'), 'booking' );
                    $table->display();
                    ?>
                </form>
            </div>
            <?php
            // Inline overview cards removed from the Bookings screen for a cleaner list-focused view.
            ?>
            <?php do_action( 'bookaro_admin_bookings_export_card' ); ?>
        </div>
        <?php
    }

    /**
     * Exports are a Pro-only feature delivered by the separate MyBookaro Pro
     * plugin, which hooks `bookaro_admin_bookings_export_card` to render the
     * real export card. Lite intentionally renders nothing here (no locked or
     * placeholder UI).
     */
    public function render_exports_placeholder(): void
    {
    }

    /**
     * Render the edit form & process updates.
     */
    private function render_edit_form( int $id ): void
    {
        $booking = $this->db->get_booking( $id );
        if ( ! $booking ) {
            echo '<div class="notice notice-error"><p>' . esc_html__( 'Booking not found.', 'mybookaro' ) . '</p></div>';
            return;
        }

        $row     = $booking->toArray();
        $notice  = '';
        $notice_type = '';
        $notification_logs = $this->db->get_notification_logs( $id );

        $request_method = isset( $_SERVER['REQUEST_METHOD'] )
            ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) )
            : '';
        if ( 'POST' === $request_method ) {
            $nonce = sanitize_text_field( wp_unslash( $_POST['edit_booking_nonce'] ?? '' ) );
            if ( wp_verify_nonce( $nonce, 'edit_booking_' . $id ) ) {
                $new_status = sanitize_text_field( wp_unslash( $_POST['status'] ?? '' ) );
                $new_date_input = sanitize_text_field( wp_unslash( $_POST['booking_date'] ?? '' ) );
                $tz = wp_timezone();
                $parsed_dt = \DateTime::createFromFormat( 'Y-m-d H:i:s', $new_date_input, $tz )
                    ?: \DateTime::createFromFormat( 'Y-m-d H:i', $new_date_input, $tz );
                $booking_date_value = $row['booking_date'];
                // Bug 72: parse the stored naive date in the site timezone so it
                // compares apples-to-apples against $parsed_dt (also site-tz).
                // Previously strtotime() parsed it as UTC, so an unchanged date
                // appeared to differ by the GMT offset and falsely tripped the
                // reschedule-window gate below.
                try {
                    $original_dt = new \DateTimeImmutable( $row['booking_date'], $tz );
                    $original_ts = $original_dt->getTimestamp();
                } catch ( \Exception $e ) {
                    $original_ts = strtotime( $row['booking_date'] );
                }
                $new_ts = $original_ts;
                if ( $parsed_dt ) {
                    $booking_date_value = $parsed_dt->format( 'Y-m-d H:i:s' );
                    $new_ts = $parsed_dt->getTimestamp();
                } else {
                    $notice = esc_html__( 'Invalid date format. Please use YYYY-MM-DD HH:MM:SS.', 'mybookaro' );
                    $notice_type = 'notice-error';
                }
                $reschedule_window_hours = absint( bookaro_get_option( 'booking_reschedule_window_hours', 0 ) );
                $reschedule_blocked = false;

                if ( $reschedule_window_hours > 0 && $new_ts && $original_ts && $new_ts !== $original_ts ) {
                    $start_dt = new \DateTime( $row['booking_date'], $tz );
                    $cutoff = ( clone $start_dt )->modify( '-' . $reschedule_window_hours . ' hours' );
                    $now = new \DateTime( 'now', $tz );
                    if ( $now > $cutoff ) {
                        $booking_date_value = $row['booking_date'];
                        $reschedule_blocked = true;
                    }
                }
                $data       = [
                    'service_label' => sanitize_text_field( wp_unslash( $_POST['service_label'] ?? '' ) ),
                    'booking_date'  => $booking_date_value,
                    'price'         => floatval( sanitize_text_field( wp_unslash( $_POST['price'] ?? 0 ) ) ),
                    // Bug 35: cap before write so the admin booking-edit form
                    // matches the service-layer policy. Without this, an
                    // admin pasting an oversized note got platform-dependent
                    // truncation (MySQL TEXT cliff in non-strict mode, a
                    // silent UPDATE failure in strict mode).
                    'notes'         => $this->service->truncate_user_text(
                        sanitize_textarea_field( wp_unslash( $_POST['notes'] ?? '' ) )
                    ),
                ];

                // Bug 43: refuse to silently double-book. The REST reschedule
                // path re-checks slot availability inside its GET_LOCK
                // critical section; this admin form was calling
                // update_booking directly with no conflict check. An admin
                // typing the same time as an existing booking on the same
                // provider got "Booking updated" while the system quietly
                // wrote two bookings on one slot.
                //
                // Only check when the date actually changed (otherwise every
                // save of an existing approved booking would "conflict with
                // itself"), and skip when the date validation already
                // rejected the input. Sites that intentionally overbook
                // can return true from `bookaro_allow_admin_double_book`.
                $conflict_blocked = false;
                if (
                    ! $reschedule_blocked
                    && $parsed_dt
                    && $booking_date_value !== $row['booking_date']
                    && ! (bool) apply_filters( 'bookaro_allow_admin_double_book', false, $id, $booking_date_value, $row )
                ) {
                    global $wpdb;
                    $existing_id = (int) $wpdb->get_var(
                        $wpdb->prepare(
                            "SELECT id FROM {$wpdb->prefix}bookaro_bookings
                             WHERE provider_id = %d
                               AND booking_date = %s
                               AND id <> %d
                               AND status IN ( 'pending', 'approved' )
                             LIMIT 1",
                            (int) $row['provider_id'],
                            $booking_date_value,
                            (int) $id
                        )
                    );
                    if ( $existing_id > 0 ) {
                        $data['booking_date'] = $row['booking_date'];
                        $conflict_blocked = true;
                        $notice = sprintf(
                            /* translators: %d: id of the conflicting booking */
                            __( 'Booking #%d is already on that slot. Pick a different time or cancel the other booking first.', 'mybookaro' ),
                            $existing_id
                        );
                        $notice_type = 'notice-error';
                    }
                }

                $this->db->update_booking( $id, $data );

                if ( $new_status !== $row['status'] ) {
                    // Bug 44: gate through the Bug 26 state machine so admin
                    // can't silently flip a completed booking back to pending
                    // via the edit form. Filter-based admin overrides are
                    // still possible via bookaro_validate_booking_status_change.
                    $status_validation = $this->service->validateStatusChange( $id, $new_status );
                    if ( $status_validation instanceof \WP_Error ) {
                        $notice = $status_validation->get_error_message();
                        $notice_type = 'notice-error';
                    } else {
                        $this->service->updateBookingStatus( $id, $new_status );
                        $row['status'] = $new_status;
                    }
                } else {
                    // Bug 36: when the admin edited the date / notes / price
                    // but NOT the status, the status-change branch's
                    // flush_provider_slot_cache (Bug 30) didn't fire and the
                    // bookaro_booking_admin_updated action has no listeners.
                    // An admin moving a booking from 14:00 to 15:00 would
                    // leave the slot transient cache showing the old slot
                    // as still occupied. Mirror the flush that updateBookingStatus
                    // performs so both code paths leave the cache consistent.
                    $provider_id = isset( $row['provider_id'] ) ? (int) $row['provider_id'] : 0;
                    if ( $provider_id > 0 ) {
                        $this->db->flush_provider_slot_cache( $provider_id );
                    }
                    do_action( 'bookaro_booking_admin_updated', $id );
                }

                $row    = array_merge( $row, $data );
                if ( $reschedule_blocked ) {
                    $notice = sprintf(
                        /* translators: %d: hours before booking start. */
                        __( 'Bookings can only be rescheduled up to %d hours before start.', 'mybookaro' ),
                        $reschedule_window_hours
                    );
                    $notice_type = 'notice-error';
                } else {
                    if ( empty( $notice ) ) {
                        $notice = esc_html__( 'Booking updated.', 'mybookaro' );
                        $notice_type = 'notice-success';
                    }
                }
            }
        }

        TemplateLoader::render('admin/booking-edit.php', [
            'id'     => $id,
            'row'    => $row,
            'notice' => $notice,
            'notice_type' => $notice_type,
            'notification_logs' => $notification_logs,
        ]);
    }
}
