<?php
namespace Bookaro\Controller\Admin;

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Admin list table; all request reads are read-only filter/sort/pagination params (sanitized) with no state mutation, so a nonce is not required.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- The interpolated $table/$where_sql/$orderby_sql/$order_sql fragments are built from $wpdb->prefix and validated identifiers/whitelisted directions; all data values are bound via $wpdb->prepare() %d/%s placeholders.

use WP_List_Table;
use Bookaro\Repository\BookingRepository;
use Bookaro\Service\BookingService;
use function add_query_arg;
use function admin_url;
use function absint;
use function date_i18n;
use function esc_attr;
use function esc_html;
use function esc_html__;
use function esc_url;
use function bookaro_get_option;
use function get_userdata;
use function number_format_i18n;
use function current_user_can;
use function remove_query_arg;
use function sanitize_key;
use function sanitize_text_field;
use function selected;
use function wp_unslash;
use function wp_nonce_url;
use function wp_safe_redirect;
use function wp_die;
use function _n;

if ( ! class_exists( '\WP_List_Table' ) && defined( 'ABSPATH' ) ) {
    require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * Renders the Bookings list table in wp-admin.
 */
class Bookings_List_Table extends WP_List_Table
{
    private BookingRepository $db;
    private array $status_counts = [];

    public function __construct( BookingRepository $db )
    {
        parent::__construct([
            'singular' => 'booking',
            'plural'   => 'bookings',
            'ajax'     => false,
        ]);
        $this->db = $db;
    }

    /**
     * Instance method, not static.
     */
    public function get_columns(): array
    {
        $resource_id_label = $this->get_resource_id_label();

        return [
            'cb'            => '<input type="checkbox" />',
            'id'            => 'ID',
            'listing_id'    => esc_html( $resource_id_label ),
            'service_label' => esc_html__('Service', 'mybookaro'),
            'owner'         => esc_html__('Customer', 'mybookaro'),
            'provider'      => esc_html__('Provider', 'mybookaro'),
            'booking_date'  => esc_html__('Date', 'mybookaro'),
            'price'         => esc_html__('Price', 'mybookaro'),
            'status'        => esc_html__('Status', 'mybookaro'),
        ];
    }

    protected function get_sortable_columns(): array
    {
        return [
            'id'           => ['id', false],
            'booking_date' => ['booking_date', false],
            'status'       => ['status', false],
            'price'        => ['price', false],
        ];
    }

    protected function get_bulk_actions(): array
    {
        return [
            'delete' => esc_html__('Delete', 'mybookaro'),
            'status_approved'  => esc_html__( 'Mark Approved', 'mybookaro' ),
            'status_rejected'  => esc_html__( 'Mark Rejected', 'mybookaro' ),
            'status_cancelled' => esc_html__( 'Mark Cancelled', 'mybookaro' ),
            'status_completed' => esc_html__( 'Mark Completed', 'mybookaro' ),
            'status_no_show'   => esc_html__( 'Mark No-show', 'mybookaro' ),
        ];
    }

    protected function column_cb( $item ): string
    {
        return sprintf(
            '<input type="checkbox" name="booking[]" value="%d" />',
            absint( $item['id'] )
        );
    }

    protected function column_id( $item ): string
    {
        return esc_html( (string) absint( $item['id'] ?? 0 ) );
    }

    protected function column_service_label( $item ): string
    {
        $label = '';
        if ( ! empty( $item['service_id'] ) ) {
            $service = get_post( (int) $item['service_id'] );
            if ( $service && $service->post_type === BOOKARO_SERVICE_POST_TYPE ) {
                $label = $service->post_title;
            }
        }
        if ( $label === '' ) {
            $label = (string) ( $item['service_label'] ?? '' );
        }
        $label = esc_html( $label );

        $actions = $this->get_row_actions( $item );
        if ( empty( $actions ) ) {
            return $label;
        }

        return $label . $this->row_actions( $actions );
    }

    protected function column_default( $item, $column_name )
    {
        switch ( $column_name ) {
            case 'listing_id':
                $listing_id = absint( $item['listing_id'] ?? 0 );
                if ( $listing_id > 0 ) {
                    $edit_link = get_edit_post_link( $listing_id, '' );
                    if ( $edit_link ) {
                        return sprintf(
                            '<a href="%s">#%d</a>',
                            esc_url( $edit_link ),
                            $listing_id
                        );
                    }
                }
                if ( $listing_id > 0 ) {
                    return esc_html( (string) $listing_id );
                }
                return esc_html( $this->get_provider_context_label( is_array( $item ) ? $item : [] ) );
            case 'service_label':
                return $this->column_service_label( $item );
            case 'price':
                if ( function_exists( 'bookaro_format_price' ) ) {
                    return esc_html( bookaro_format_price( $item['price'] ) );
                }
                return esc_html( number_format_i18n( $item['price'], 2 ) );
            case 'owner':
                $owner = get_userdata( $item['customer_id'] );
                return $owner ? esc_html( $owner->display_name ) : '';
            case 'provider':
                $provider = get_userdata( $item['provider_id'] );
                return $provider ? esc_html( $provider->display_name ) : '';
            case 'booking_date':
                // Bug 72: format via presenter so the displayed time is not
                // shifted by the GMT offset on non-UTC sites.
                return esc_html( \Bookaro\Utils\BookingPolicyPresenter::format_datetime_label( (string) $item['booking_date'] ) );
            case 'status':
                return $this->format_status_badge( (string) ( $item['status'] ?? '' ) );
            default:
                return '';
        }
    }

    protected function get_row_actions( array $item ): array
    {
        $actions  = [];
        $booking_id = absint( $item['id'] ?? 0 );
        if ( $booking_id < 1 ) {
            return $actions;
        }

        if ( bookaro_get_option( 'allow_admin_edit_bookings', 0 ) ) {
            $edit_url = wp_nonce_url(
                add_query_arg([
                    'page'   => 'bookaro-bookings',
                    'action' => 'edit',
                    'id'     => $booking_id,
                ], admin_url('admin.php')),
                'edit_booking_' . $booking_id
            );
            $actions['edit'] = sprintf(
                '<a href="%s">%s</a>',
                esc_url($edit_url),
                esc_html__('Edit', 'mybookaro')
            );
        }

        $status = sanitize_key( (string) ( $item['status'] ?? '' ) );
        $status_actions = [
            'approved'  => __( 'Approve', 'mybookaro' ),
            'rejected'  => __( 'Reject', 'mybookaro' ),
            'cancelled' => __( 'Cancel', 'mybookaro' ),
            'completed' => __( 'Complete', 'mybookaro' ),
            'no_show'   => __( 'No-show', 'mybookaro' ),
        ];
        foreach ( $status_actions as $new_status => $label ) {
            if ( $status === $new_status ) {
                continue;
            }
            $status_url = wp_nonce_url(
                add_query_arg([
                    'page'   => 'bookaro-bookings',
                    'action' => 'status_' . $new_status,
                    'id'     => $booking_id,
                ], admin_url('admin.php')),
                'bookaro_status_' . $booking_id . '_' . $new_status
            );
            $actions[ 'status_' . $new_status ] = sprintf(
                '<a href="%s">%s</a>',
                esc_url( $status_url ),
                esc_html( $label )
            );
        }

        $del_url = wp_nonce_url(
            add_query_arg([
                'page'   => 'bookaro-bookings',
                'action' => 'delete',
                'id'     => $booking_id,
            ], admin_url('admin.php')),
            'delete_booking_' . $booking_id
        );
        $actions['delete'] = sprintf(
            '<a href="%s" onclick="return confirm(%s)">%s</a>',
            esc_url($del_url),
            esc_html__("'Are you sure you want to delete this booking?'", 'mybookaro'),
            esc_html__('Delete', 'mybookaro')
        );

        return $actions;
    }

    protected function get_primary_column_name(): string
    {
        return 'service_label';
    }

    protected function get_views(): array
    {
        $views = [];
        $current_status = sanitize_key( (string) wp_unslash( $_REQUEST['status'] ?? '' ) );
        $current_status = $current_status === '' ? 'all' : $current_status;

        $base_url = $this->get_admin_page_url( [], [ 'status', 'paged' ] );
        $all_count = (int) ( $this->status_counts['all'] ?? 0 );
        $views['all'] = sprintf(
            '<a href="%s"%s>%s</a>',
            esc_url( $base_url ),
            $current_status === 'all' ? ' class="current"' : '',
            sprintf(
                /* translators: %d: count */
                esc_html__( 'All (%d)', 'mybookaro' ),
                $all_count
            )
        );

        $labels = $this->get_status_labels();
        foreach ( $labels as $status => $label ) {
            $count = (int) ( $this->status_counts[ $status ] ?? 0 );
            $url = add_query_arg( [ 'status' => $status ], $base_url );
            $views[ $status ] = sprintf(
                '<a href="%s"%s>%s</a>',
                esc_url( $url ),
                $current_status === $status ? ' class="current"' : '',
                sprintf(
                    /* translators: 1: status label, 2: count */
                    esc_html__( '%1$s (%2$d)', 'mybookaro' ),
                    $label,
                    $count
                )
            );
        }

        return $views;
    }

    protected function extra_tablenav( $which ): void
    {
        if ( 'top' === $which ) {
            $status = sanitize_key( (string) wp_unslash( $_REQUEST['status'] ?? '' ) );
            $booking_id = absint( wp_unslash( $_REQUEST['booking_id'] ?? 0 ) );
            $service_id = absint( wp_unslash( $_REQUEST['service_id'] ?? 0 ) );
            $customer_id = absint( wp_unslash( $_REQUEST['customer_id'] ?? 0 ) );
            $provider_id = absint( wp_unslash( $_REQUEST['provider_id'] ?? 0 ) );
            $listing_id = absint( wp_unslash( $_REQUEST['listing_id'] ?? 0 ) );
            $date_from = $this->sanitize_date( sanitize_text_field( wp_unslash( $_REQUEST['date_from'] ?? '' ) ) );
            $date_to = $this->sanitize_date( sanitize_text_field( wp_unslash( $_REQUEST['date_to'] ?? '' ) ) );
            $price_min = sanitize_text_field( wp_unslash( $_REQUEST['price_min'] ?? '' ) );
            $price_max = sanitize_text_field( wp_unslash( $_REQUEST['price_max'] ?? '' ) );
            $resource_id_label = $this->get_resource_id_label();
            ?>
            <div class="alignleft actions bookaro-filters">
                <label class="screen-reader-text" for="bookaro-filter-booking"><?php esc_html_e( 'Booking ID', 'mybookaro' ); ?></label>
                <input id="bookaro-filter-booking" type="number" name="booking_id" value="<?php echo esc_attr( $booking_id ?: '' ); ?>" placeholder="<?php esc_attr_e( 'Booking ID', 'mybookaro' ); ?>" />

                <label class="screen-reader-text" for="bookaro-filter-status"><?php esc_html_e( 'Filter by status', 'mybookaro' ); ?></label>
                <select id="bookaro-filter-status" name="status">
                    <option value=""><?php esc_html_e( 'All statuses', 'mybookaro' ); ?></option>
                    <?php foreach ( $this->get_status_labels() as $key => $label ) : ?>
                        <option value="<?php echo esc_attr( $key ); ?>" <?php selected( $status, $key ); ?>>
                            <?php echo esc_html( $label ); ?>
                        </option>
                    <?php endforeach; ?>
                </select>

                <label class="screen-reader-text" for="bookaro-filter-service"><?php esc_html_e( 'Service ID', 'mybookaro' ); ?></label>
                <input id="bookaro-filter-service" type="number" name="service_id" value="<?php echo esc_attr( $service_id ?: '' ); ?>" placeholder="<?php esc_attr_e( 'Service ID', 'mybookaro' ); ?>" />

                <label class="screen-reader-text" for="bookaro-filter-customer"><?php esc_html_e( 'Filter by customer', 'mybookaro' ); ?></label>
                <select id="bookaro-filter-customer" name="customer_id">
                    <option value="0"><?php esc_html_e( 'All customers', 'mybookaro' ); ?></option>
                    <?php foreach ( $this->get_customer_filter_options() as $id => $name ) : ?>
                        <option value="<?php echo esc_attr( $id ); ?>" <?php selected( $customer_id, $id ); ?>>
                            <?php echo esc_html( $name ); ?>
                        </option>
                    <?php endforeach; ?>
                </select>

                <label class="screen-reader-text" for="bookaro-filter-provider"><?php esc_html_e( 'Filter by provider', 'mybookaro' ); ?></label>
                <select id="bookaro-filter-provider" name="provider_id">
                    <option value="0"><?php esc_html_e( 'All providers', 'mybookaro' ); ?></option>
                    <?php foreach ( $this->get_provider_filter_options() as $id => $name ) : ?>
                        <option value="<?php echo esc_attr( $id ); ?>" <?php selected( $provider_id, $id ); ?>>
                            <?php echo esc_html( $name ); ?>
                        </option>
                    <?php endforeach; ?>
                </select>

                <label class="screen-reader-text" for="bookaro-filter-listing"><?php echo esc_html( $resource_id_label ); ?></label>
                <input id="bookaro-filter-listing" type="number" name="listing_id" value="<?php echo esc_attr( $listing_id ?: '' ); ?>" placeholder="<?php echo esc_attr( $resource_id_label ); ?>" />

                <label class="screen-reader-text" for="bookaro-filter-from"><?php esc_html_e( 'From date', 'mybookaro' ); ?></label>
                <input id="bookaro-filter-from" type="date" name="date_from" value="<?php echo esc_attr( $date_from ); ?>" />

                <label class="screen-reader-text" for="bookaro-filter-to"><?php esc_html_e( 'To date', 'mybookaro' ); ?></label>
                <input id="bookaro-filter-to" type="date" name="date_to" value="<?php echo esc_attr( $date_to ); ?>" />

                <label class="screen-reader-text" for="bookaro-filter-price-min"><?php esc_html_e( 'Minimum price', 'mybookaro' ); ?></label>
                <input id="bookaro-filter-price-min" type="number" step="0.01" name="price_min" value="<?php echo esc_attr( $price_min ); ?>" placeholder="<?php esc_attr_e( 'Min price', 'mybookaro' ); ?>" />

                <label class="screen-reader-text" for="bookaro-filter-price-max"><?php esc_html_e( 'Maximum price', 'mybookaro' ); ?></label>
                <input id="bookaro-filter-price-max" type="number" step="0.01" name="price_max" value="<?php echo esc_attr( $price_max ); ?>" placeholder="<?php esc_attr_e( 'Max price', 'mybookaro' ); ?>" />

                <?php submit_button( __( 'Filter', 'mybookaro' ), 'secondary', 'filter_action', false ); ?>
            </div>
            <?php
        }

        $this->render_per_page_control( (string) $which );
    }

    private function get_status_labels(): array
    {
        return [
            'pending'   => __( 'Pending approval', 'mybookaro' ),
            'approved'  => __( 'Approved', 'mybookaro' ),
            'rejected'  => __( 'Rejected', 'mybookaro' ),
            'cancelled' => __( 'Cancelled', 'mybookaro' ),
            'completed' => __( 'Completed', 'mybookaro' ),
            'no_show'   => __( 'No-show', 'mybookaro' ),
        ];
    }

    private function get_provider_filter_options(): array
    {
        $role = function_exists( 'bookaro_get_option' ) ? (string) bookaro_get_option( 'provider_role', '' ) : '';
        $args = [
            'fields' => [ 'ID', 'display_name' ],
            'orderby' => 'display_name',
            'order' => 'ASC',
        ];
        if ( $role !== '' ) {
            $args['role'] = $role;
        }
        $users = get_users( $args );
        $options = [];
        foreach ( $users as $user ) {
            if ( ! $user instanceof \WP_User ) {
                continue;
            }
            $options[ $user->ID ] = $user->display_name;
        }
        return $options;
    }

    private function get_customer_filter_options(): array
    {
        $role = function_exists( 'bookaro_get_option' ) ? (string) bookaro_get_option( 'owner_role', '' ) : '';
        $args = [
            'fields' => [ 'ID', 'display_name' ],
            'orderby' => 'display_name',
            'order' => 'ASC',
        ];
        if ( $role !== '' ) {
            $args['role'] = $role;
        }
        $users = get_users( $args );
        $options = [];
        foreach ( $users as $user ) {
            if ( ! $user instanceof \WP_User ) {
                continue;
            }
            $options[ $user->ID ] = $user->display_name;
        }
        return $options;
    }

    private function format_status_badge( string $status ): string
    {
        $status = sanitize_key( $status );
        $labels = $this->get_status_labels();
        $label = $labels[ $status ] ?? ( $status !== '' ? ucfirst( $status ) : __( 'Unknown', 'mybookaro' ) );
        $map = [
            'pending'   => 'pending',
            'approved'  => 'approved',
            'rejected'  => 'rejected',
            'cancelled' => 'cancelled',
            'completed' => 'completed',
            'no_show'   => 'no_show',
        ];
        $class = $map[ $status ] ?? 'pending';
        return sprintf(
            '<span class="bookaro-status badge-%s">%s</span>',
            esc_attr( $class ),
            esc_html( $label )
        );
    }

    private function sanitize_date( string $date ): string
    {
        $date = trim( $date );
        if ( $date === '' ) {
            return '';
        }
        if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
            return '';
        }
        $dt = \DateTime::createFromFormat( 'Y-m-d', $date );
        if ( ! $dt || $dt->format( 'Y-m-d' ) !== $date ) {
            return '';
        }
        return $date;
    }

    /**
     * Prepare the table data: sorting, searching, pagination.
     */
    public function prepare_items(): void
    {
        $wpdb       = $this->db->get_wpdb();
        $table      = esc_sql( $this->db->get_table_name() );
        $per_page   = $this->get_per_page();
        $current    = $this->get_pagenum();
        $offset     = ( $current - 1 ) * $per_page;
        $allowed_orderby = [ 'id', 'booking_date', 'status', 'price' ];
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $orderby = sanitize_key( wp_unslash( $_REQUEST['orderby'] ?? 'booking_date' ) );
        if ( ! in_array( $orderby, $allowed_orderby, true ) ) {
            $orderby = 'booking_date';
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $order = strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ?? 'DESC' ) ) );
        $order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC';

        $orderby_sql = esc_sql( $orderby );
        $order_sql = esc_sql( $order );

        $cache_group = 'bookaro_admin_bookings';
        $cache_ttl = 30;

        $base_where = [ '1=1' ];
        $base_params = [];

        $provider_id = absint( wp_unslash( $_REQUEST['provider_id'] ?? 0 ) );
        if ( $provider_id > 0 ) {
            $base_where[] = 'provider_id = %d';
            $base_params[] = $provider_id;
        }

        $listing_id = absint( wp_unslash( $_REQUEST['listing_id'] ?? 0 ) );
        if ( $listing_id > 0 ) {
            $base_where[] = 'listing_id = %d';
            $base_params[] = $listing_id;
        }

        $booking_id = absint( wp_unslash( $_REQUEST['booking_id'] ?? 0 ) );
        if ( $booking_id > 0 ) {
            $base_where[] = 'id = %d';
            $base_params[] = $booking_id;
        }

        $service_id = absint( wp_unslash( $_REQUEST['service_id'] ?? 0 ) );
        if ( $service_id > 0 ) {
            $base_where[] = 'service_id = %d';
            $base_params[] = $service_id;
        }

        $customer_id = absint( wp_unslash( $_REQUEST['customer_id'] ?? 0 ) );
        if ( $customer_id > 0 ) {
            $base_where[] = 'customer_id = %d';
            $base_params[] = $customer_id;
        }

        $date_from = $this->sanitize_date( sanitize_text_field( wp_unslash( $_REQUEST['date_from'] ?? '' ) ) );
        if ( $date_from !== '' ) {
            $base_where[] = 'booking_date >= %s';
            $base_params[] = $date_from . ' 00:00:00';
        }
        $date_to = $this->sanitize_date( sanitize_text_field( wp_unslash( $_REQUEST['date_to'] ?? '' ) ) );
        if ( $date_to !== '' ) {
            $base_where[] = 'booking_date <= %s';
            $base_params[] = $date_to . ' 23:59:59';
        }

        $price_min_raw = sanitize_text_field( wp_unslash( $_REQUEST['price_min'] ?? '' ) );
        if ( $price_min_raw !== '' && is_numeric( $price_min_raw ) ) {
            $base_where[] = 'price >= %f';
            $base_params[] = (float) $price_min_raw;
        }
        $price_max_raw = sanitize_text_field( wp_unslash( $_REQUEST['price_max'] ?? '' ) );
        if ( $price_max_raw !== '' && is_numeric( $price_max_raw ) ) {
            $base_where[] = 'price <= %f';
            $base_params[] = (float) $price_max_raw;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $s_raw = sanitize_text_field( wp_unslash( $_REQUEST['s'] ?? '' ) );
        if ( $s_raw !== '' ) {
            $like = '%' . $wpdb->esc_like( $s_raw ) . '%';
            if ( ctype_digit( $s_raw ) ) {
                $base_where[] = '(service_label LIKE %s OR id = %d OR listing_id = %d)';
                array_push( $base_params, $like, (int) $s_raw, (int) $s_raw );
            } else {
                $base_where[] = 'service_label LIKE %s';
                $base_params[] = $like;
            }
        }

        $where = $base_where;
        $params = $base_params;

        $status = sanitize_key( (string) wp_unslash( $_REQUEST['status'] ?? '' ) );
        if ( $status !== '' ) {
            $where[] = 'status = %s';
            $params[] = $status;
        }

        $where_sql = implode( ' AND ', $where );
        $filter_hash = md5( $where_sql . '|' . wp_json_encode( $params ) . '|' . $orderby_sql . '|' . $order_sql . '|' . $per_page . '|' . $offset );
        $count_key = 'count_' . $filter_hash;
        $items_key = 'items_' . $filter_hash;

        $total_items = wp_cache_get( $count_key, $cache_group );
        if ( false === $total_items ) {
            // $where_sql always starts with the literal `1=1`; when no admin
            // filter is applied, $params is empty and calling prepare() with
            // a placeholder-less query triggers a WP_DEBUG notice. Skip
            // prepare() entirely when there are no placeholders; the literal
            // path is safe because $table comes from esc_sql() and the
            // unfiltered query contains no user input.
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $count_sql = "SELECT COUNT(*) FROM `{$table}` WHERE {$where_sql}";
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
            $total_items = (int) $wpdb->get_var( empty( $params ) ? $count_sql : $wpdb->prepare( $count_sql, $params ) );
            wp_cache_set( $count_key, $total_items, $cache_group, $cache_ttl );
        }

        $items = wp_cache_get( $items_key, $cache_group );
        if ( false === $items ) {
            $items_params = $params;
            $items_params[] = $per_page;
            $items_params[] = $offset;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $items = $wpdb->get_results(
                // $items_params already contains every placeholder value:
                // the WHERE-clause args from $params (one per %s/%d in
                // $where_sql) plus $per_page + $offset for LIMIT/OFFSET.
                // Static analyzer can't trace $where_sql to count its
                // placeholders so it under-counts.
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
                $wpdb->prepare(
                    "SELECT * FROM `{$table}` WHERE {$where_sql} ORDER BY {$orderby_sql} {$order_sql} LIMIT %d OFFSET %d",
                    ...$items_params
                ),
                ARRAY_A
            );
            wp_cache_set( $items_key, $items, $cache_group, $cache_ttl );
        }
        $this->items = is_array( $items ) ? $items : [];

        $this->status_counts = $this->get_status_counts( $base_where, $base_params );

        $this->_column_headers = [
            $this->get_columns(),
            [], // no hidden
            $this->get_sortable_columns(),
        ];

        $this->set_pagination_args([
            'total_items' => $total_items,
            'per_page'    => $per_page,
            'total_pages' => (int) ceil( $total_items / $per_page ),
        ]);
    }

    protected function pagination( $which ): void
    {
        if ( empty( $this->_pagination_args ) ) {
            return;
        }

        $total_items = (int) ( $this->_pagination_args['total_items'] ?? 0 );
        $total_pages = (int) ( $this->_pagination_args['total_pages'] ?? 0 );
        if ( $total_pages < 1 ) {
            $total_pages = 1;
        }

        $current = $this->get_pagenum();
        if ( $current < 1 ) {
            $current = 1;
        } elseif ( $current > $total_pages ) {
            $current = $total_pages;
        }

        $output = '<span class="displaying-num">' . sprintf(
            /* translators: %s: number of items. */
            _n( '%s item', '%s items', $total_items, 'mybookaro' ),
            number_format_i18n( $total_items )
        ) . '</span>';

        $removable_query_args = function_exists( 'wp_removable_query_args' ) ? wp_removable_query_args() : [];
        if ( ! is_array( $removable_query_args ) ) {
            $removable_query_args = [];
        }
        $current_url = $this->get_admin_page_url( [], $removable_query_args );

        $page_links = [];
        $disable_first = $current <= 1;
        $disable_last = $current >= $total_pages;

        if ( $disable_first ) {
            $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&laquo;</span>';
            $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&lsaquo;</span>';
        } else {
            $page_links[] = sprintf(
                '<a class="first-page button" href="%s"><span class="screen-reader-text">%s</span><span aria-hidden="true">&laquo;</span></a>',
                esc_url( remove_query_arg( 'paged', $current_url ) ),
                esc_html__( 'First page', 'mybookaro' )
            );
            $page_links[] = sprintf(
                '<a class="prev-page button" href="%s"><span class="screen-reader-text">%s</span><span aria-hidden="true">&lsaquo;</span></a>',
                esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ),
                esc_html__( 'Previous page', 'mybookaro' )
            );
        }

        $page_links[] = '<span class="paging-input">' .
            sprintf(
                '<label for="%1$s" class="screen-reader-text">%2$s</label><input class="current-page" id="%1$s" type="text" name="paged" value="%3$s" size="1" aria-describedby="table-paging" />',
                esc_attr( 'current-page-selector-' . $which ),
                esc_html__( 'Current Page', 'mybookaro' ),
                (string) $current
            ) .
            sprintf(
                '<span class="tablenav-paging-text"> %s</span>',
                sprintf(
                    /* translators: %s: total pages. */
                    __( 'of %s', 'mybookaro' ),
                    '<span class="total-pages">' . number_format_i18n( $total_pages ) . '</span>'
                )
            ) .
            '</span>';

        if ( $disable_last ) {
            $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&rsaquo;</span>';
            $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&raquo;</span>';
        } else {
            $page_links[] = sprintf(
                '<a class="next-page button" href="%s"><span class="screen-reader-text">%s</span><span aria-hidden="true">&rsaquo;</span></a>',
                esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ),
                esc_html__( 'Next page', 'mybookaro' )
            );
            $page_links[] = sprintf(
                '<a class="last-page button" href="%s"><span class="screen-reader-text">%s</span><span aria-hidden="true">&raquo;</span></a>',
                esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
                esc_html__( 'Last page', 'mybookaro' )
            );
        }

        $output .= '<span class="pagination-links">' . implode( "\n", $page_links ) . '</span>';

        $page_class = $total_pages <= 1 ? ' one-page' : '';
        printf(
            '<div class="tablenav-pages%s">%s</div>',
            esc_attr( $page_class ),
            // $output is composed entirely from internally-built escaped fragments
            // ($page_links entries are produced via add_query_arg + esc_url + esc_html
            // calls higher in this method, and the wrapping span markup is literal).
            wp_kses_post( $output )
        );
    }

    private function get_per_page(): int
    {
        $per_page = absint( wp_unslash( $_REQUEST['per_page'] ?? 0 ) );
        $options = $this->get_per_page_options();
        if ( ! in_array( $per_page, $options, true ) ) {
            $per_page = 20;
        }
        return $per_page;
    }

    /**
     * @return int[]
     */
    private function get_per_page_options(): array
    {
        return [ 10, 20, 50, 100 ];
    }

    private function render_per_page_control( string $which ): void
    {
        $per_page = $this->get_per_page();
        $options = $this->get_per_page_options();
        ?>
        <div class="alignleft actions bookaro-per-page">
            <label class="screen-reader-text" for="bookaro-per-page-<?php echo esc_attr( $which ); ?>">
                <?php esc_html_e( 'Items per page', 'mybookaro' ); ?>
            </label>
            <select id="bookaro-per-page-<?php echo esc_attr( $which ); ?>" name="per_page"
                onchange="this.form.submit()">
                <?php foreach ( $options as $option ) : ?>
                    <option value="<?php echo esc_attr( $option ); ?>" <?php selected( $per_page, $option ); ?>>
                        <?php echo esc_html( $option ); ?>
                    </option>
                <?php endforeach; ?>
            </select>
            <span class="description"><?php esc_html_e( 'per page', 'mybookaro' ); ?></span>
        </div>
        <?php
    }

    public function process_bulk_action( BookingService $service ): void
    {
        $action = $this->current_action();
        if ( ! $action ) {
            return;
        }

        check_admin_referer( 'bulk-' . $this->_args['plural'] );

        $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 perform this action.', 'mybookaro' ) );
        }

        // Every value is forced through absint() on the next line, which
        // is the appropriate sanitizer for booking IDs (integers).
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $ids = isset( $_REQUEST['booking'] ) ? (array) wp_unslash( $_REQUEST['booking'] ) : [];
        $ids = array_map( 'absint', $ids );
        $ids = array_values( array_filter( $ids ) );
        if ( empty( $ids ) ) {
            return;
        }

        $status_map = [
            'status_approved'  => 'approved',
            'status_rejected'  => 'rejected',
            'status_cancelled' => 'cancelled',
            'status_completed' => 'completed',
            'status_no_show'   => 'no_show',
        ];

        if ( 'delete' === $action ) {
            foreach ( $ids as $id ) {
                $this->db->delete_booking( $id );
            }
            BookingAdminController::set_admin_notice_transient( 'deleted', count( $ids ) );
        } elseif ( isset( $status_map[ $action ] ) ) {
            $new_status = $status_map[ $action ];
            // Bug 44: respect the Bug 26 state machine on bulk status changes
            // too. Without this filter the admin bulk-action dropdown was the
            // one path that could silently re-pend a stack of completed
            // bookings while the REST bulk endpoint correctly rejected the
            // same transitions. Skipped rows surface in a single
            // status_locked notice so the admin sees something happened.
            $updated_count = 0;
            $locked_count  = 0;
            foreach ( $ids as $id ) {
                $validation = $service->validateStatusChange( $id, $new_status );
                if ( $validation instanceof \WP_Error ) {
                    $locked_count++;
                    continue;
                }
                $service->updateBookingStatus( $id, $new_status );
                $updated_count++;
            }
            if ( $updated_count > 0 ) {
                BookingAdminController::set_admin_notice_transient( 'status_' . $new_status, $updated_count );
            }
            if ( $locked_count > 0 && 0 === $updated_count ) {
                // Only surface the lock notice when ALL targets failed —
                // otherwise the success notice tells the more useful story
                // and the count discrepancy is enough signal.
                BookingAdminController::set_admin_notice_transient( 'status_locked', $locked_count );
            }
        } else {
            return;
        }

        $redirect = $this->get_admin_page_url( [], [ 'action', 'action2', '_wpnonce', 'booking' ] );
        wp_safe_redirect( $redirect );
        exit;
    }

    private function get_admin_page_url( array $overrides = [], array $remove = [] ): string
    {
        $args = $this->get_safe_url_query_args();

        foreach ( $remove as $key ) {
            unset( $args[ (string) $key ] );
        }

        foreach ( $overrides as $key => $value ) {
            $key = (string) $key;
            if ( null === $value || '' === $value ) {
                unset( $args[ $key ] );
                continue;
            }
            $args[ $key ] = $value;
        }

        return add_query_arg( $args, admin_url( 'admin.php' ) );
    }

    private function get_safe_url_query_args(): array
    {
        $args = [
            'page' => 'bookaro-bookings',
        ];

        $status = sanitize_key( (string) wp_unslash( $_REQUEST['status'] ?? '' ) );
        $status_labels = $this->get_status_labels();
        if ( $status !== '' && isset( $status_labels[ $status ] ) ) {
            $args['status'] = $status;
        }

        $numeric_filters = [ 'booking_id', 'service_id', 'customer_id', 'provider_id', 'listing_id', 'paged' ];
        foreach ( $numeric_filters as $filter_key ) {
            $value = absint( wp_unslash( $_REQUEST[ $filter_key ] ?? 0 ) );
            if ( $value > 0 ) {
                $args[ $filter_key ] = $value;
            }
        }

        $date_from = $this->sanitize_date( sanitize_text_field( wp_unslash( $_REQUEST['date_from'] ?? '' ) ) );
        if ( $date_from !== '' ) {
            $args['date_from'] = $date_from;
        }
        $date_to = $this->sanitize_date( sanitize_text_field( wp_unslash( $_REQUEST['date_to'] ?? '' ) ) );
        if ( $date_to !== '' ) {
            $args['date_to'] = $date_to;
        }

        foreach ( [ 'price_min', 'price_max' ] as $price_key ) {
            $price = sanitize_text_field( wp_unslash( $_REQUEST[ $price_key ] ?? '' ) );
            if ( $price !== '' && is_numeric( $price ) ) {
                $args[ $price_key ] = $price;
            }
        }

        $per_page = absint( wp_unslash( $_REQUEST['per_page'] ?? 0 ) );
        if ( in_array( $per_page, $this->get_per_page_options(), true ) ) {
            $args['per_page'] = $per_page;
        }

        $sortable = $this->get_sortable_columns();
        $orderby = sanitize_key( (string) wp_unslash( $_REQUEST['orderby'] ?? '' ) );
        if ( $orderby !== '' && isset( $sortable[ $orderby ] ) ) {
            $args['orderby'] = $orderby;
        }

        $order = sanitize_key( (string) wp_unslash( $_REQUEST['order'] ?? '' ) );
        if ( in_array( $order, [ 'asc', 'desc' ], true ) ) {
            $args['order'] = $order;
        }

        $search = sanitize_text_field( wp_unslash( $_REQUEST['s'] ?? '' ) );
        if ( $search !== '' ) {
            $args['s'] = $search;
        }

        return $args;
    }

    private function get_status_counts( array $where, array $params ): array
    {
        $wpdb = $this->db->get_wpdb();
        $table = esc_sql( $this->db->get_table_name() );
        $where_sql = implode( ' AND ', $where );

        $cache_group = 'bookaro_admin_bookings';
        $cache_key = 'status_counts_' . md5( $where_sql . '|' . wp_json_encode( $params ) );
        $counts = wp_cache_get( $cache_key, $cache_group );
        if ( false !== $counts && is_array( $counts ) ) {
            return $counts;
        }

        // Same empty-$params guard as in prepare_items() — see comment there.
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $status_sql = "SELECT status, COUNT(*) AS total FROM `{$table}` WHERE {$where_sql} GROUP BY status";
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
        $rows = $wpdb->get_results( empty( $params ) ? $status_sql : $wpdb->prepare( $status_sql, $params ), ARRAY_A );
        $counts = [ 'all' => 0 ];
        if ( is_array( $rows ) ) {
            foreach ( $rows as $row ) {
                $status = sanitize_key( (string) ( $row['status'] ?? '' ) );
                $total = (int) ( $row['total'] ?? 0 );
                if ( $status !== '' ) {
                    $counts[ $status ] = $total;
                    $counts['all'] += $total;
                }
            }
        }

        wp_cache_set( $cache_key, $counts, $cache_group, 30 );
        return $counts;
    }

    private function get_resource_singular_label(): string
    {
        if ( function_exists( 'bookaro_get_resource_label_singular' ) ) {
            $label = sanitize_text_field( (string) bookaro_get_resource_label_singular() );
            if ( '' !== $label ) {
                return $label;
            }
        }

        return __( 'Listing', 'mybookaro' );
    }

    private function get_resource_id_label(): string
    {
        return sprintf(
            /* translators: %s: resource label singular */
            __( '%s ID', 'mybookaro' ),
            $this->get_resource_singular_label()
        );
    }

    private function get_provider_context_label( array $item ): string
    {
        $provider_label = ( function_exists( 'bookaro_is_single_business_site' ) && bookaro_is_single_business_site() )
            ? $this->get_resource_singular_label()
            : __( 'Provider', 'mybookaro' );

        $provider_id = absint( $item['provider_id'] ?? 0 );
        if ( $provider_id > 0 ) {
            $provider = get_userdata( $provider_id );
            if ( $provider && isset( $provider->display_name ) && (string) $provider->display_name !== '' ) {
                return sprintf(
                    /* translators: 1: context label, 2: provider display name */
                    __( '%1$s: %2$s', 'mybookaro' ),
                    $provider_label,
                    (string) $provider->display_name
                );
            }
            return sprintf(
                /* translators: 1: context label, 2: provider user id */
                __( '%1$s ID: %2$d', 'mybookaro' ),
                $provider_label,
                $provider_id
            );
        }

        return sprintf(
            /* translators: %s: context label */
            __( '%s context', 'mybookaro' ),
            $provider_label
        );
    }
}
