<?php

namespace Bookaro\Service;


defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- GDPR erasure/export runs UPDATE/SELECT against custom plugin tables (names from $wpdb->prefix); every value is bound via $wpdb->prepare(), and IN() lists use dynamically-generated %d placeholders that these sniffs cannot statically count.

use Bookaro\Repository\BookingRepository;
use RuntimeException;

/**
 * Handles GDPR data erasure for Bookaro.
 */
class PrivacyService
{
    private BookingRepository $booking_db;

    public function __construct( BookingRepository $booking_db )
    {
        $this->booking_db = $booking_db;
    }

    /**
     * Register hooks for WordPress privacy API.
     */
    public function register_hooks(): void
    {
        if ( ! function_exists( 'wp_privacy_add_personal_data_exporter' )
            || ! function_exists( 'wp_privacy_add_personal_data_eraser' )
        ) {
            return;
        }

        add_filter( 'wp_privacy_personal_data_erasers', [ $this, 'register_eraser' ] );
        add_filter( 'wp_privacy_personal_data_exporters', [ $this, 'register_exporter' ] );
    }

    /**
     * Bug 38: anonymise booking rows when WP deletes a user.
     *
     * Without this listener, deleting a WP user via Users > All Users
     * leaves customer_id / provider_id in the bookings table pointing
     * to a now-non-existent wp_users row. Dashboards render blank
     * names, the privacy export still finds the booking by ID but
     * the user lookup fails, and the data remains effectively
     * un-anonymised — at odds with the GDPR-driven erase flow.
     *
     * Route through delete_user_data() so the row is reduced to the
     * same shape an end-user-initiated erasure would leave: customer_id
     * = 0, customer_phone = '', notes / cancel_reason cleared, related
     * notification-log summaries scrubbed. Wired separately from
     * register_hooks() so it always runs — register_hooks() short-
     * circuits on environments without the WP privacy API but the user-
     * delete hook should still fire there.
     */
    public function register_user_delete_hook(): void
    {
        add_action( 'deleted_user', [ $this, 'on_user_deleted' ] );
    }

    public function on_user_deleted( int $user_id ): void
    {
        $user_id = absint( $user_id );
        if ( $user_id < 1 ) {
            return;
        }
        try {
            $this->delete_user_data( $user_id );
        } catch ( \Throwable $e ) {
            // Don't break user deletion if our cleanup fails — WP has
            // already removed the wp_users row by the time deleted_user
            // fires. Logging keeps the failure visible.
            if ( class_exists( '\\Bookaro\\Utils\\Logger' ) ) {
                \Bookaro\Utils\Logger::log( 'on_user_deleted_failed', [
                    'user_id'   => $user_id,
                    'exception' => $e->getMessage(),
                ] );
            }
        }
    }

    /**
     * Register personal data eraser callback.
     */
    public function register_eraser( array $erasers ): array
    {
        $erasers['bookaro_eraser'] = [
            'eraser_friendly_name' => __( 'MyBookaro Booking Data', 'mybookaro' ),
            'callback'             => [ $this, 'erase_personal_data' ],
        ];
        return $erasers;
    }

    /**
     * Register personal data exporter callback.
     */
    public function register_exporter( array $exporters ): array
    {
        $exporters['bookaro_exporter'] = [
            'exporter_friendly_name' => __( 'MyBookaro Booking Data', 'mybookaro' ),
            'callback'               => [ $this, 'export_personal_data' ],
        ];
        return $exporters;
    }

    /**
     * Erase personal data via WordPress Tools -> Erase Personal Data.
     */
    public function erase_personal_data( string $email, int $page = 1 ): array
    {
        $user = get_user_by( 'email', $email );
        if ( ! $user ) {
            return [
                'items_removed'  => false,
                'items_retained' => false,
                'messages'       => [],
                'done'           => true,
            ];
        }

        $this->delete_user_data( (int) $user->ID );

        return [
            'items_removed'  => true,
            'items_retained' => false,
            'messages'       => [],
            'done'           => true,
        ];
    }

    /**
     * Delete all plugin data for a user.
     */
    public function delete_user_data( int $user_id ): void
    {
        global $wpdb;

        $booking_ids = $this->get_all_booking_ids_for_user( $user_id );
        $table   = $this->booking_db->get_table_name();

        // Anonymize bookings where the user is the customer.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $updated = $wpdb->update(
            $table,
            [
                'customer_id'    => 0,
                'customer_phone' => '',
                'notes'          => '',
                'cancel_reason'  => '',
            ],
            [ 'customer_id' => $user_id ],
            [ '%d', '%s', '%s', '%s' ],
            [ '%d' ]
        );
        if ( false === $updated ) {
            throw new RuntimeException( 'Failed to anonymize customer bookings.' );
        }

        // Anonymize bookings where the user is the provider.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $updated = $wpdb->update(
            $table,
            [
                'provider_id'   => 0,
                'notes'         => '',
                'cancel_reason' => '',
            ],
            [ 'provider_id' => $user_id ],
            [ '%d', '%s', '%s' ],
            [ '%d' ]
        );
        if ( false === $updated ) {
            throw new RuntimeException( 'Failed to anonymize provider bookings.' );
        }

        // Bug 42: cancel still-active future bookings AFTER the anonymize
        // pass, so the system-generated cancel_reason ("Account removed.")
        // survives. Past 'approved' bookings stay as-is — they're history,
        // not in-flight, and the cron auto-complete handles them. Pending
        // bookings get cancelled regardless of date (they're never going
        // to be approved by a deleted user).
        //
        // Without this, a future booking where the departing user was
        // customer OR provider stays in 'approved' state with the IDs
        // zeroed: the slot conflict query (which excludes only
        // cancelled/rejected/no_show) keeps the slot occupied, the
        // dashboard shows a "confirmed" booking nobody will fulfil, and
        // the reminder cron fires until it bails out at get_user_by(0).
        if ( ! empty( $booking_ids ) ) {
            $now_mysql = current_time( 'mysql' );
            $ids_placeholders = implode( ',', array_fill( 0, count( $booking_ids ), '%d' ) );
            $params = array_merge(
                [ 'cancelled', __( 'Account removed.', 'mybookaro' ) ],
                $booking_ids,
                [ $now_mysql ]
            );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $wpdb->query(
                $wpdb->prepare(
                    "UPDATE {$table}
                     SET status = %s, cancel_reason = %s
                     WHERE id IN ({$ids_placeholders})
                       AND ( status = 'pending'
                          OR ( status = 'approved' AND booking_date > %s ) )",
                    ...$params
                )
            );
        }

        if ( ! empty( $booking_ids ) && $this->booking_db->notification_table_exists() ) {
            $log_table = esc_sql( $this->booking_db->get_notification_log_table() );
            $placeholders = implode( ',', array_fill( 0, count( $booking_ids ), '%d' ) );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $sql = $wpdb->prepare(
                "UPDATE {$log_table} SET summary = '', error_message = '' WHERE booking_id IN ({$placeholders})",
                ...$booking_ids
            );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- $sql was just built via $wpdb->prepare() above with %d placeholders for every value.
            $wpdb->query( $sql );
        }

        $this->cleanup_webhook_queue( $booking_ids );
        $this->cleanup_pro_booking_meta( $booking_ids );


        // Delete related user meta.
        $default_keys = [
            'bookaro_slots_per_interval',
            'bookaro_viewed_exceptions',
        ];
        $meta_keys = apply_filters( 'bookaro_privacy_user_meta_keys', $default_keys, $user_id );
        if ( ! is_array( $meta_keys ) ) {
            $meta_keys = $default_keys;
        }
        foreach ( $meta_keys as $key ) {
            delete_user_meta( $user_id, $key );
        }

        // Clear transients.
        delete_transient( 'bookaro_avail_weekly_' . $user_id );
        delete_transient( 'bookaro_timeslots_' . $user_id );
        delete_transient( 'bookaro_exceptions_' . $user_id );

        // Allow add-ons to perform additional cleanup. Pass the booking_ids
        // we collected before anonymization so listeners can scrub
        // booking-id-keyed Pro tables (custom forms, etc.) that can no
        // longer be queried via user_id once the rows above are anonymized.
        do_action( 'bookaro_privacy_cleanup', $user_id, $booking_ids );
    }

    /**
     * Export personal data via WordPress Tools -> Export Personal Data.
     */
    public function export_personal_data( string $email, int $page = 1 ): array
    {
        $user = get_user_by( 'email', $email );
        if ( ! $user ) {
            return [
                'data' => [],
                'done' => true,
            ];
        }

        $items = [];
        if ( $page === 1 ) {
            $items = array_merge( $items, $this->export_user_meta( (int) $user->ID ) );
        }
        $items = array_merge( $items, $this->export_bookings( (int) $user->ID, $page ) );
        $items = array_merge( $items, $this->export_notification_logs( (int) $user->ID, $page ) );

        return [
            'data' => $items,
            'done' => ( $this->export_bookings_done( (int) $user->ID, $page )
                && $this->export_notification_logs_done( (int) $user->ID, $page ) ),
        ];
    }

    private function export_user_meta( int $user_id ): array
    {
        global $wpdb;

        $data = [];
        $default_keys = [
            'bookaro_slots_per_interval',
            'bookaro_viewed_exceptions',
        ];
        $meta_keys = apply_filters( 'bookaro_privacy_user_meta_keys', $default_keys, $user_id );
        if ( ! is_array( $meta_keys ) ) {
            $meta_keys = $default_keys;
        }
        $sensitive_keys = $this->get_sensitive_meta_keys( $user_id );
        $masked_keys = apply_filters( 'bookaro_privacy_export_masked_meta_keys', [], $user_id );
        if ( ! is_array( $masked_keys ) ) {
            $masked_keys = [];
        }
        foreach ( $meta_keys as $key ) {
            $value = get_user_meta( $user_id, $key, true );
            if ( $value !== '' && $value !== null ) {
                if ( $key === 'bookaro_payout_profile' ) {
                    if ( function_exists( 'bookaro_pro_get_payout_profile' ) ) {
                        $profile = bookaro_pro_get_payout_profile( $user_id, false );
                        $value = is_array( $profile ) ? wp_json_encode( $profile ) : '';
                    } else {
                        $value = __( '[payout profile stored]', 'mybookaro' );
                    }
                }
                if ( in_array( $key, $sensitive_keys, true ) ) {
                    $value = $this->get_sensitive_meta_placeholder( $key );
                } elseif ( in_array( $key, $masked_keys, true ) ) {
                    $value = $this->mask_generic_value( (string) $value );
                }
                $data[] = [
                    'name'  => $key,
                    'value' => is_scalar( $value ) ? (string) $value : wp_json_encode( $value ),
                ];
            }
        }

        $like_keys = apply_filters( 'bookaro_privacy_user_meta_like_keys', [], $user_id );
        if ( ! is_array( $like_keys ) ) {
            $like_keys = [];
        }
        foreach ( $like_keys as $like_key ) {
            $like_key = (string) $like_key;
            if ( $like_key === '' ) {
                continue;
            }
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $pattern_keys = $wpdb->get_col(
                $wpdb->prepare(
                    "SELECT meta_key FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key LIKE %s",
                    $user_id,
                    $like_key
                )
            );
            foreach ( $pattern_keys as $meta_key ) {
                $value = get_user_meta( $user_id, $meta_key, true );
                if ( $value !== '' && $value !== null ) {
                    if ( $meta_key === 'bookaro_payout_profile' ) {
                        if ( function_exists( 'bookaro_pro_get_payout_profile' ) ) {
                            $profile = bookaro_pro_get_payout_profile( $user_id, false );
                            $value = is_array( $profile ) ? wp_json_encode( $profile ) : '';
                        } else {
                            $value = __( '[payout profile stored]', 'mybookaro' );
                        }
                    }
                    if ( in_array( $meta_key, $sensitive_keys, true ) ) {
                        $value = $this->get_sensitive_meta_placeholder( (string) $meta_key );
                    } elseif ( in_array( $meta_key, $masked_keys, true ) ) {
                        $value = $this->mask_generic_value( (string) $value );
                    }
                    $data[] = [
                        'name'  => $meta_key,
                        'value' => is_scalar( $value ) ? (string) $value : wp_json_encode( $value ),
                    ];
                }
            }
        }

        if ( empty( $data ) ) {
            return [];
        }

        return [
            [
                'group_id'    => 'bookaro-user-meta',
                'group_label' => __( 'MyBookaro User Data', 'mybookaro' ),
                'item_id'     => 'bookaro-user-meta-' . $user_id,
                'data'        => $data,
            ],
        ];
    }

    private function export_bookings( int $user_id, int $page ): array
    {
        global $wpdb;
        $per_page = 50;
        $offset = max( 0, ( $page - 1 ) * $per_page );
        $table = esc_sql( $this->booking_db->get_table_name() );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                "SELECT * FROM `{$table}` WHERE customer_id = %d OR provider_id = %d ORDER BY id ASC LIMIT %d OFFSET %d",
                $user_id,
                $user_id,
                $per_page,
                $offset
            ),
            ARRAY_A
        );
        if ( empty( $rows ) ) {
            return [];
        }

        $items = [];
        foreach ( $rows as $row ) {
            $booking_id = (int) $row['id'];
            $pro_meta = $this->get_pro_booking_meta( $booking_id );
            $role = $row['customer_id'] == $user_id ? 'customer' : 'provider';
            $data = [
                [ 'name' => 'role', 'value' => $role ],
                [ 'name' => 'booking_id', 'value' => (string) $row['id'] ],
                [ 'name' => 'listing_id', 'value' => (string) $row['listing_id'] ],
                [ 'name' => 'service_label', 'value' => (string) $row['service_label'] ],
                [ 'name' => 'booking_date', 'value' => (string) $row['booking_date'] ],
                [ 'name' => 'booking_date_to', 'value' => (string) ( $row['booking_date_to'] ?? '' ) ],
                [ 'name' => 'status', 'value' => (string) $row['status'] ],
                [ 'name' => 'price', 'value' => (string) $row['price'] ],
                [ 'name' => 'notes', 'value' => (string) ( $row['notes'] ?? '' ) ],
                [ 'name' => 'cancel_reason', 'value' => (string) ( $row['cancel_reason'] ?? '' ) ],
                [ 'name' => 'customer_phone', 'value' => (string) ( $row['customer_phone'] ?? '' ) ],
                [ 'name' => 'payment_choice', 'value' => (string) ( $pro_meta['payment_choice'] ?? '' ) ],
                [ 'name' => 'order_id', 'value' => (string) ( $pro_meta['order_id'] ?? '' ) ],
                [ 'name' => 'google_event_id', 'value' => (string) ( $pro_meta['google_event_id'] ?? '' ) ],
                [ 'name' => 'sms_message_sid', 'value' => (string) ( $pro_meta['sms_message_sid'] ?? '' ) ],
                [ 'name' => 'sms_message_status', 'value' => (string) ( $pro_meta['sms_message_status'] ?? '' ) ],
            ];

            // Allow Pro (and third parties) to append per-booking personal
            // data — for example custom-form responses stored in the Pro
            // wp_bookaro_booking_custom_data table — so the GDPR export the
            // user receives is actually complete instead of silently
            // omitting Pro-only fields.
            $data = (array) apply_filters(
                'bookaro_privacy_export_booking_data',
                $data,
                $booking_id,
                $user_id,
                $row
            );

            $items[] = [
                'group_id'    => 'bookaro-bookings',
                'group_label' => __( 'MyBookaro Bookings', 'mybookaro' ),
                'item_id'     => 'bookaro-booking-' . $booking_id,
                'data'        => array_values( array_filter( $data, 'is_array' ) ),
            ];
        }

        return $items;
    }

    private function export_bookings_done( int $user_id, int $page ): bool
    {
        global $wpdb;
        $per_page = 50;
        $offset = max( 0, ( $page - 1 ) * $per_page );
        $table = esc_sql( $this->booking_db->get_table_name() );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $total = (int) $wpdb->get_var(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                "SELECT COUNT(*) FROM `{$table}` WHERE customer_id = %d OR provider_id = %d",
                $user_id,
                $user_id
            )
        );
        return ( $offset + $per_page ) >= $total;
    }


    private function export_notification_logs( int $user_id, int $page ): array
    {
        global $wpdb;
        $per_page = 50;
        $offset = max( 0, ( $page - 1 ) * $per_page );

        $booking_ids = $this->get_user_booking_ids( $user_id, $per_page, $offset );
        if ( empty( $booking_ids ) ) {
            return [];
        }

        if ( ! $this->booking_db->notification_table_exists() ) {
            return [];
        }

        $table = esc_sql( $this->booking_db->get_notification_log_table() );
        $placeholders = implode( ',', array_fill( 0, count( $booking_ids ), '%d' ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE booking_id IN ({$placeholders}) ORDER BY created_at DESC, id DESC",
                ...$booking_ids
            ),
            ARRAY_A
        );

        if ( empty( $rows ) ) {
            return [];
        }

        $items = [];
        foreach ( $rows as $row ) {
            $summary = $row['summary'] ?? '';
            $decoded = is_string( $summary ) ? json_decode( $summary, true ) : null;
            $summary_clean = is_array( $decoded ) ? $this->mask_notification_summary( $decoded ) : [];
            $items[] = [
                'group_id' => 'bookaro-notification-logs',
                'group_label' => __( 'MyBookaro Notification Logs', 'mybookaro' ),
                'item_id' => 'bookaro-notification-log-' . (int) $row['id'],
                'data' => [
                    [ 'name' => 'booking_id', 'value' => (string) ( $row['booking_id'] ?? '' ) ],
                    [ 'name' => 'channel', 'value' => (string) ( $row['channel'] ?? '' ) ],
                    [ 'name' => 'template_key', 'value' => (string) ( $row['template_key'] ?? '' ) ],
                    [ 'name' => 'status', 'value' => (string) ( $row['status'] ?? '' ) ],
                    [ 'name' => 'created_at', 'value' => (string) ( $row['created_at'] ?? '' ) ],
                    [ 'name' => 'summary', 'value' => $summary_clean ? wp_json_encode( $summary_clean ) : '' ],
                    [ 'name' => 'error_message', 'value' => $this->redact_text( (string) ( $row['error_message'] ?? '' ) ) ],
                ],
            ];
        }

        return $items;
    }

    private function export_notification_logs_done( int $user_id, int $page ): bool
    {
        return $this->export_bookings_done( $user_id, $page );
    }


    private function get_user_booking_ids( int $user_id, int $limit, int $offset ): array
    {
        $limit = max( 1, min( 200, absint( $limit ) ) );
        $offset = max( 0, absint( $offset ) );
        $table = esc_sql( $this->booking_db->get_table_name() );
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $ids = $wpdb->get_col(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                "SELECT id FROM {$table} WHERE customer_id = %d OR provider_id = %d ORDER BY id ASC LIMIT %d OFFSET %d",
                $user_id,
                $user_id,
                $limit,
                $offset
            )
        );
        return array_values( array_filter( array_map( 'absint', $ids ) ) );
    }

    private function get_sensitive_meta_keys( int $user_id ): array
    {
        $defaults = [
            'bookaro_gcal_access_token',
            'bookaro_gcal_refresh_token',
            'bookaro_gcal_token_expiry',
            'bookaro_gcal_calendar_id',
            'bookaro_gcal_scope_hash',
        ];
        $keys = apply_filters( 'bookaro_privacy_export_sensitive_meta_keys', $defaults, $user_id );
        if ( ! is_array( $keys ) ) {
            $keys = $defaults;
        }
        $keys = array_values( array_unique( array_filter( array_map( 'strval', $keys ) ) ) );
        return array_values( array_diff( $keys, [ 'bookaro_payout_profile' ] ) );
    }

    private function get_sensitive_meta_placeholder( string $key ): string
    {
        $key = sanitize_key( $key );
        return __( '[redacted: stored for integrations]', 'mybookaro' );
    }

    private function mask_generic_value( string $value ): string
    {
        if ( $value === '' ) {
            return '';
        }
        if ( strpos( $value, '@' ) !== false ) {
            return $this->mask_email( $value );
        }
        return '[redacted]';
    }

    private function get_pro_booking_meta( int $booking_id ): array
    {
        $booking_id = absint( $booking_id );
        if ( $booking_id < 1 ) {
            return [];
        }
        global $wpdb;
        $table = $wpdb->prefix . 'bookaro_booking_pro_meta';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( empty( $exists ) ) {
            return [];
        }
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT payment_choice, order_id, google_event_id, sms_message_sid, sms_message_status FROM {$table} WHERE booking_id = %d",
                $booking_id
            ),
            ARRAY_A
        );
        return is_array( $row ) ? $row : [];
    }

    private function mask_notification_summary( array $summary ): array
    {
        $clean = [];
        foreach ( $summary as $key => $value ) {
            $key_str = sanitize_key( (string) $key );
            if ( is_array( $value ) ) {
                $value = wp_json_encode( $this->mask_notification_summary( $value ) );
            } else {
                $value = (string) $value;
            }
            if ( $key_str === 'email' || $key_str === 'to' || $key_str === 'recipient' ) {
                $value = $this->mask_email( $value );
            } elseif ( $key_str === 'phone' || $key_str === 'to_phone' ) {
                $value = $this->mask_phone( $value );
            } elseif ( $key_str === 'endpoint' || $key_str === 'url' ) {
                $value = $this->mask_url( $value );
            }
            $clean[ $key_str !== '' ? $key_str : (string) $key ] = $value;
        }
        return $clean;
    }

    private function redact_text( string $value ): string
    {
        if ( $value === '' ) {
            return '';
        }
        $value = preg_replace( '/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i', '[redacted-email]', $value );
        $value = preg_replace( '/Bearer\\s+[A-Za-z0-9\\-._~+/]+=*/i', 'Bearer [redacted]', $value );
        $value = preg_replace( '/(token|key|secret|password|authorization|signature|nonce|api_key|access_token|refresh_token)=([A-Za-z0-9._~\\-]+)/i', '$1=[redacted]', $value );
        $value = preg_replace_callback(
            '/\\+?\\d[\\d\\s\\-()]{6,}\\d/',
            fn( $m ) => $this->mask_phone( (string) $m[0] ),
            $value
        );
        return (string) $value;
    }

    private function mask_email( string $email ): string
    {
        $email = trim( $email );
        if ( $email === '' ) {
            return '';
        }
        $parts = explode( '@', $email, 2 );
        if ( count( $parts ) !== 2 ) {
            return '[redacted-email]';
        }
        $local = $parts[0];
        $domain = $parts[1];
        $mask = $local !== '' ? substr( $local, 0, 1 ) . '***' : '***';
        return $mask . '@' . $domain;
    }

    private function mask_phone( string $phone ): string
    {
        $phone = trim( $phone );
        if ( $phone === '' ) {
            return '';
        }
        $digits = preg_replace( '/\\D+/', '', $phone );
        if ( ! is_string( $digits ) || $digits === '' ) {
            return '***';
        }
        $last4 = substr( $digits, -4 );
        $prefix = strpos( $phone, '+' ) !== false ? '+' : '';
        return $prefix . '***' . $last4;
    }

    private function mask_url( string $url ): string
    {
        $url = trim( $url );
        if ( $url === '' ) {
            return '';
        }
        $parts = wp_parse_url( $url );
        if ( ! is_array( $parts ) || empty( $parts['host'] ) ) {
            return $url;
        }
        $scheme = $parts['scheme'] ?? 'https';
        return $scheme . '://' . $parts['host'];
    }

    private function mask_ip( string $ip ): string
    {
        $ip = trim( $ip );
        if ( $ip === '' ) {
            return '';
        }
        if ( strpos( $ip, ':' ) !== false ) {
            $parts = explode( ':', $ip );
            $keep = array_slice( $parts, 0, 2 );
            return implode( ':', $keep ) . ':*:*:*';
        }
        $parts = explode( '.', $ip );
        if ( count( $parts ) === 4 ) {
            return $parts[0] . '.' . $parts[1] . '.*.*';
        }
        return $ip;
    }

    private function mask_user_agent( string $ua ): string
    {
        $ua = trim( $ua );
        if ( $ua === '' ) {
            return '';
        }
        if ( strlen( $ua ) <= 60 ) {
            return $ua;
        }
        return substr( $ua, 0, 60 ) . '...';
    }


    private function get_all_booking_ids_for_user( int $user_id ): array
    {
        $table = esc_sql( $this->booking_db->get_table_name() );
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $ids = $wpdb->get_col(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                "SELECT id FROM {$table} WHERE customer_id = %d OR provider_id = %d",
                $user_id,
                $user_id
            )
        );
        return array_values( array_filter( array_map( 'absint', $ids ) ) );
    }

    private function cleanup_webhook_queue( array $booking_ids ): void
    {
        if ( empty( $booking_ids ) ) {
            return;
        }
        $queue = get_option( 'bookaro_webhook_queue', [] );
        if ( ! is_array( $queue ) || empty( $queue ) ) {
            return;
        }
        $booking_map = array_fill_keys( $booking_ids, true );
        foreach ( $queue as $id => $item ) {
            $booking_id = absint( $item['booking_id'] ?? 0 );
            if ( $booking_id > 0 && isset( $booking_map[ $booking_id ] ) ) {
                unset( $queue[ $id ] );
            }
        }
        update_option( 'bookaro_webhook_queue', $queue, false );
    }

    private function cleanup_pro_booking_meta( array $booking_ids ): void
    {
        if ( empty( $booking_ids ) ) {
            return;
        }
        global $wpdb;
        $table = $wpdb->prefix . 'bookaro_booking_pro_meta';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( empty( $exists ) ) {
            return;
        }
        $placeholders = implode( ',', array_fill( 0, count( $booking_ids ), '%d' ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query(
            $wpdb->prepare(
                "UPDATE {$table}
                 SET google_event_id = NULL,
                     sms_message_sid = NULL,
                     sms_message_status = NULL,
                     sms_message_updated_at = NULL
                 WHERE booking_id IN ({$placeholders})",
                ...$booking_ids
            )
        );
    }
}
