<?php
namespace Bookaro\Service;

defined( 'ABSPATH' ) || exit;

use Bookaro\Repository\BookingRepository;
use Bookaro\Utils\BookingPolicyPresenter;
use Bookaro\Utils\Logger;
use Bookaro\Utils\UserHelper;

/**
 * Handles all outbound email functionality for the booking system,
 * including templated emails and WP-Cron callbacks.
 */
class EmailService
{

    /**
     * Send a templated email with HTML and plain-text parts.
     */
    public function sendTemplatedEmail(
        string $to,
        string $key,
        array $replacements,
        int $provider_id = 0,
        array $attachments = [],
        int $booking_id = 0
    ): void
    {
        try {
            $to = sanitize_email($to);
            if (empty($to)) {
                return;
            }
            $key = sanitize_key( $key );
            if ( $key === '' || ! $this->is_template_enabled( $key ) ) {
                return;
            }
            if ( $booking_id < 1 && isset( $replacements['booking_id'] ) ) {
                $booking_id = absint( $replacements['booking_id'] );
            }
            if ( $booking_id > 0 && ! isset( $replacements['booking_id'] ) ) {
                $replacements['booking_id'] = $booking_id;
            }
            if ( $booking_id > 0 ) {
                $context = $this->build_booking_email_context( $booking_id, $to, $replacements );
                if ( ! isset( $replacements['bookaro_details'] ) && ! empty( $context['details'] ) ) {
                    $replacements['bookaro_details'] = $context['details'];
                }
                if ( ! isset( $replacements['bookaro_contacts'] ) && ! empty( $context['contacts'] ) ) {
                    $replacements['bookaro_contacts'] = $context['contacts'];
                }
                if ( ! isset( $replacements['bookaro_timezone_label'] ) && ! empty( $context['timezone'] ) ) {
                    $replacements['bookaro_timezone_label'] = $context['timezone'];
                }
                if ( ! isset( $replacements['timezone'] ) && ! empty( $context['timezone'] ) ) {
                    $replacements['timezone'] = $context['timezone'];
                }
                if ( ! isset( $replacements['bookaro_policy_cancel'] ) && ! empty( $context['policies']['cancel'] ) ) {
                    $replacements['bookaro_policy_cancel'] = $context['policies']['cancel'];
                }
                if ( ! isset( $replacements['bookaro_policy_reschedule'] ) && ! empty( $context['policies']['reschedule'] ) ) {
                    $replacements['bookaro_policy_reschedule'] = $context['policies']['reschedule'];
                }
                if ( ! isset( $replacements['bookaro_policies'] ) && ! empty( $context['policies']['combined'] ) ) {
                    $replacements['bookaro_policies'] = $context['policies']['combined'];
                }
                if ( ! isset( $replacements['bookaro_policy_lines'] ) && ! empty( $context['policies']['lines'] ) ) {
                    $replacements['bookaro_policy_lines'] = $context['policies']['lines'];
                }
                if ( ! isset( $replacements['bookaro_policy_payment'] ) && ! empty( $context['policies']['payment_policy'] ) ) {
                    $replacements['bookaro_policy_payment'] = $context['policies']['payment_policy'];
                }
                if ( ! isset( $replacements['bookaro_policy_deposit'] ) && ! empty( $context['policies']['deposit'] ) ) {
                    $replacements['bookaro_policy_deposit'] = $context['policies']['deposit'];
                }
                if ( ! isset( $replacements['bookaro_policy_cancel_fee'] ) && ! empty( $context['policies']['cancel_fee'] ) ) {
                    $replacements['bookaro_policy_cancel_fee'] = $context['policies']['cancel_fee'];
                }
                if ( ! isset( $replacements['bookaro_policy_no_show_fee'] ) && ! empty( $context['policies']['no_show_fee'] ) ) {
                    $replacements['bookaro_policy_no_show_fee'] = $context['policies']['no_show_fee'];
                }
                if ( ! isset( $replacements['manage_booking_url'] ) && ! empty( $context['dashboard_url'] ) ) {
                    $replacements['manage_booking_url'] = $context['dashboard_url'];
                }
                if ( ! isset( $replacements['dashboard_url'] ) && ! empty( $context['dashboard_url'] ) ) {
                    $replacements['dashboard_url'] = $context['dashboard_url'];
                }
                if ( ! isset( $replacements['listing_title'] ) && ! empty( $context['listing_title'] ) ) {
                    $replacements['listing_title'] = $context['listing_title'];
                }
                if ( ! isset( $replacements['listing_url'] ) && ! empty( $context['listing_url'] ) ) {
                    $replacements['listing_url'] = $context['listing_url'];
                }
                if ( ! isset( $replacements['provider_email'] ) && ! empty( $context['provider_email'] ) ) {
                    $replacements['provider_email'] = $context['provider_email'];
                }
                if ( ! isset( $replacements['provider_phone'] ) && ! empty( $context['provider_phone'] ) ) {
                    $replacements['provider_phone'] = $context['provider_phone'];
                }
                if ( ! isset( $replacements['customer_phone'] ) && ! empty( $context['customer_phone'] ) ) {
                    $replacements['customer_phone'] = $context['customer_phone'];
                }
            }

            $token_replacements = $this->normalize_template_replacements( $replacements );
            $setting_subject = '';
            $setting_body = '';
            if ( function_exists( 'bookaro_get_option' ) ) {
                $setting_subject = trim( (string) bookaro_get_option( "{$key}_subject", '' ) );
                $setting_body = trim( (string) bookaro_get_option( "{$key}_body", '' ) );
            }
            if ( $setting_body !== '' && ! isset( $replacements['bookaro_intro_html'] ) ) {
                $replacements['bookaro_intro_html'] = $this->format_custom_intro_html(
                    $this->render_template_tokens( $setting_body, $token_replacements )
                );
            }

            $file_subject = '';
            $file_body    = '';
            $template_file = bookaro_locate_template('templates/emails/' . $key . '.php');
            if ($template_file && file_exists($template_file)) {
                extract($replacements, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- Controlled token-replacement array: keys are developer-defined email-template variables (not user input), and EXTR_SKIP prevents clobbering any existing variable.
                ob_start();
                include $template_file;
                $file_body = ob_get_clean();
                if ( isset( $bookaro_subject ) ) {
                    $file_subject = $bookaro_subject;
                } elseif ( isset( $subject ) ) {
                    $file_subject = $subject;
                }
            } else {
                return;
            }

            if (empty($file_subject)) {
                return;
            }

            if ( $setting_subject !== '' ) {
                $file_subject = $this->render_template_tokens( $setting_subject, $token_replacements );
            }
            $file_subject = trim( $this->strip_all_tags( (string) $file_subject ) );
            if ( $file_subject === '' ) {
                return;
            }

            $multipart = $this->build_multipart_payload( (string) $file_body );
            $sent = wp_mail( $to, $file_subject, $multipart['message'], $multipart['headers'], $attachments );

            if ( $booking_id > 0 ) {
                $logger = new NotificationLogService();
                $logger->record(
                    $booking_id,
                    'email',
                    $sent ? 'sent' : 'failed',
                    [
                        'template' => $key,
                        'to' => $to,
                        'subject' => $file_subject,
                    ],
                    $sent ? '' : 'wp_mail_failed',
                    $key
                );
            }

            foreach ($attachments as $file) {
                if (is_string($file) && file_exists($file)) {
                    wp_delete_file( $file );
                }
            }
        } catch (\Throwable $e) {
            if ( $booking_id > 0 ) {
                $logger = new NotificationLogService();
                $logger->record(
                    $booking_id,
                    'email',
                    'failed',
                    [
                        'template' => $key,
                        'to' => $to ?? '',
                    ],
                    $e->getMessage(),
                    $key
                );
            }
            Logger::log('send_templated_email failed', [
                'exception' => $e->getMessage(),
                'code' => $e->getCode(),
                'trace' => $e->getTraceAsString(),
            ]);
        }
    }

    private function is_template_enabled( string $key ): bool
    {
        $enabled = true;
        if ( function_exists( 'bookaro_get_option' ) ) {
            // The Settings UI exposes ONE toggle per event keyed `notify_{key}`
            // (e.g. notify_new_booking, notify_approved). That is the source
            // of truth for "send this email or not". Earlier builds also
            // checked a parallel `notify_template_{key}` key for the same
            // event — that's been retired here because it could disagree
            // with the master switch and cause silent "I enabled the email
            // in the UI but nothing fires" reports. Sites that need
            // finer-grained control can hook the filter below.
            $master = bookaro_get_option( "notify_{$key}", 1 );
            $enabled = ! empty( $master );

            // Bug 37: some templates are grouped under a single UI toggle
            // (notify_reminder controls both reminder_24h and reminder_1h
            // — the Settings UI shows ONE "Reminder emails" switch, not
            // two). Without this group-fallback, turning the master
            // switch OFF only stopped NEW scheduling — already-scheduled
            // reminders fired, hit `notify_reminder_24h` (a key the UI
            // never sets), defaulted to enabled, and sent anyway. Honor
            // the master group toggle when it's been explicitly turned
            // off so the UI control means what it says.
            $group_master = [
                'reminder_24h' => 'notify_reminder',
                'reminder_1h'  => 'notify_reminder',
            ];
            if ( $enabled && isset( $group_master[ $key ] ) ) {
                $group_val = bookaro_get_option( $group_master[ $key ], 1 );
                $enabled = ! empty( $group_val );
            }
        }
        /**
         * Filter whether a specific email template is enabled.
         *
         * @param bool   $enabled
         * @param string $key
         */
        return (bool) apply_filters( 'bookaro_email_template_enabled', $enabled, $key );
    }

    private function normalize_template_replacements( array $replacements ): array
    {
        $normalized = [];
        foreach ( $replacements as $token => $value ) {
            $token = sanitize_key( (string) $token );
            if ( $token === '' ) {
                continue;
            }
            if ( is_scalar( $value ) || $value === null ) {
                $normalized[ $token ] = (string) $value;
            }
        }

        if ( empty( $normalized['date'] ) && ! empty( $normalized['booking_date'] ) ) {
            $normalized['date'] = $normalized['booking_date'];
        }
        if ( empty( $normalized['date_to'] ) && ! empty( $normalized['booking_date_to'] ) ) {
            $normalized['date_to'] = $normalized['booking_date_to'];
        }
        if ( empty( $normalized['date_range'] ) && ! empty( $normalized['date'] ) ) {
            $normalized['date_range'] = ! empty( $normalized['date_to'] )
                ? sprintf(
                    /* translators: 1: start date, 2: end date */
                    __( '%1$s to %2$s', 'mybookaro' ),
                    $normalized['date'],
                    $normalized['date_to']
                )
                : $normalized['date'];
        }
        if ( empty( $normalized['old_date_range'] ) && ! empty( $normalized['old_date'] ) ) {
            $normalized['old_date_range'] = ! empty( $normalized['old_date_to'] )
                ? sprintf(
                    /* translators: 1: start date, 2: end date */
                    __( '%1$s to %2$s', 'mybookaro' ),
                    $normalized['old_date'],
                    $normalized['old_date_to']
                )
                : $normalized['old_date'];
        }
        if ( empty( $normalized['owner_name'] ) && ! empty( $normalized['owner'] ) ) {
            $normalized['owner_name'] = $normalized['owner'];
        }

        return $normalized;
    }

    private function render_template_tokens( string $template, array $replacements ): string
    {
        if ( trim( $template ) === '' || empty( $replacements ) ) {
            return $template;
        }
        $search = [];
        $replace = [];
        foreach ( $replacements as $token => $value ) {
            $search[] = '{' . $token . '}';
            $replace[] = (string) $value;
        }
        return str_replace( $search, $replace, $template );
    }

    private function format_custom_intro_html( string $content ): string
    {
        $content = trim( $content );
        if ( $content === '' ) {
            return '';
        }
        $content = wp_kses_post( $content );
        if ( $content === '' ) {
            return '';
        }
        if ( $content === $this->strip_all_tags( $content ) ) {
            return wpautop( esc_html( $content ) );
        }
        return wpautop( $content );
    }

    private function build_multipart_payload( string $html_body ): array
    {
        $html_body = (string) $html_body;
        $charset = (string) get_bloginfo( 'charset' );
        if ( $charset === '' ) {
            $charset = 'UTF-8';
        }

        $plain_text = $this->build_plain_text_body( $html_body );
        if ( $plain_text === '' || ! function_exists( 'wp_generate_password' ) ) {
            return [
                'message' => $html_body,
                'headers' => [ 'Content-Type: text/html; charset=' . $charset ],
            ];
        }

        $boundary = 'bookaro-alt-' . $this->generate_mime_boundary_token();
        $message  = '--' . $boundary . "\r\n";
        $message .= 'Content-Type: text/plain; charset=' . $charset . "\r\n";
        $message .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
        $message .= $plain_text . "\r\n\r\n";
        $message .= '--' . $boundary . "\r\n";
        $message .= 'Content-Type: text/html; charset=' . $charset . "\r\n";
        $message .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
        $message .= $html_body . "\r\n\r\n";
        $message .= '--' . $boundary . '--';

        return [
            'message' => $message,
            'headers' => [
                'MIME-Version: 1.0',
                'Content-Type: multipart/alternative; boundary="' . $boundary . '"',
            ],
        ];
    }

    private function build_plain_text_body( string $html_body ): string
    {
        if ( trim( $html_body ) === '' ) {
            return '';
        }

        $text = preg_replace( '/<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is', '$2 ($1)', $html_body );
        $text = preg_replace( '/<\s*br\s*\/?\s*>/i', "\n", (string) $text );
        $text = preg_replace( '/<\/(p|div|h1|h2|h3|h4|h5|h6|li|tr|table|section)\s*>/i', "\n", (string) $text );
        $text = $this->strip_all_tags( (string) $text, true );
        $text = html_entity_decode( $text, ENT_QUOTES, get_bloginfo( 'charset' ) ?: 'UTF-8' );
        $text = preg_replace( "/[ \t]+\n/", "\n", $text );
        $text = preg_replace( "/\n{3,}/", "\n\n", $text );
        return trim( (string) $text );
    }

    /**
     * Strip tags using WordPress's hardened wrapper.
     *
     * wp_strip_all_tags() has been available since WP 2.9 (2010) — well
     * below our 5.8 minimum — so the legacy non-WP-context fallback is
     * no longer needed. Plugin Check (WP.org) flags the bare PHP
     * strip_tags() as discouraged; this thin wrapper keeps callers
     * unchanged while making the WP-only dependency explicit.
     */
    private function strip_all_tags( string $text, bool $remove_breaks = false ): string
    {
        return (string) wp_strip_all_tags( $text, $remove_breaks );
    }

    /**
     * Generate a MIME boundary token without relying solely on WP helpers.
     */
    private function generate_mime_boundary_token(): string
    {
        if ( function_exists( 'wp_generate_password' ) ) {
            $token = (string) wp_generate_password( 16, false, false );
            if ( $token !== '' ) {
                return $token;
            }
        }

        try {
            return bin2hex( random_bytes( 8 ) );
        } catch ( \Throwable $e ) {
            return dechex( absint( (int) ( microtime( true ) * 1000000 ) ) );
        }
    }

    private function get_resource_label_singular(): 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_provider_display_name( int $provider_id ): string
    {
        $provider_id = absint( $provider_id );
        if ( $provider_id < 1 ) {
            return '';
        }

        $provider = get_user_by( 'ID', $provider_id );
        if ( ! $provider || ! isset( $provider->display_name ) ) {
            return '';
        }

        return sanitize_text_field( (string) $provider->display_name );
    }

    /**
     * Resolve listing-like display data for emails.
     *
     * @return array{title:string,url:string}
     */
    private function resolve_booking_resource_context( int $listing_id, int $provider_id ): array
    {
        $listing_id = absint( $listing_id );
        $provider_id = absint( $provider_id );

        $listing_title = '';
        $listing_url = '';
        if ( $listing_id > 0 ) {
            $listing_title = sanitize_text_field( (string) get_the_title( $listing_id ) );
            $permalink = get_permalink( $listing_id );
            $listing_url = is_string( $permalink ) ? $permalink : '';
            if ( $listing_url !== '' && function_exists( 'bookaro_append_booking_button_anchor' ) ) {
                $listing_url = bookaro_append_booking_button_anchor( $listing_url );
            }
        }

        if ( '' === $listing_title ) {
            $provider_name = $this->get_provider_display_name( $provider_id );
            if ( '' !== $provider_name ) {
                $listing_title = $provider_name;
            } elseif ( $listing_id > 0 ) {
                $listing_title = sprintf(
                    /* translators: 1: resource label singular, 2: listing id */
                    __( '%1$s #%2$d', 'mybookaro' ),
                    $this->get_resource_label_singular(),
                    $listing_id
                );
            } else {
                $listing_title = $this->get_resource_label_singular();
            }
        }

        return [
            'title' => $listing_title,
            'url' => $listing_url,
        ];
    }

    private function build_booking_email_context( int $booking_id, string $viewer_email = '', array $replacements = [] ): array
    {
        $booking_id = absint( $booking_id );
        if ( $booking_id < 1 ) {
            return [
                'details' => [],
                'contacts' => [],
                'timezone' => '',
                'policies' => [ 'cancel' => '', 'reschedule' => '', 'combined' => '' ],
                'dashboard_url' => '',
                'listing_title' => '',
                'listing_url' => '',
                'provider_email' => '',
                'provider_phone' => '',
                'customer_phone' => '',
            ];
        }

        $db = new BookingRepository();
        $booking = $db->get_booking( $booking_id );
        if ( ! $booking ) {
            return [
                'details' => [],
                'contacts' => [],
                'timezone' => '',
                'policies' => [ 'cancel' => '', 'reschedule' => '', 'combined' => '' ],
                'dashboard_url' => '',
                'listing_title' => '',
                'listing_url' => '',
                'provider_email' => '',
                'provider_phone' => '',
                'customer_phone' => '',
            ];
        }

        $listing_id = absint( $booking->listing_id ?? 0 );
        $provider_id = absint( $booking->provider_id ?? 0 );
        $resource_context = $this->resolve_booking_resource_context( $listing_id, $provider_id );
        $listing_title = (string) ( $resource_context['title'] ?? '' );
        $listing_url = (string) ( $resource_context['url'] ?? '' );
        $provider_user = $provider_id > 0 ? get_user_by( 'ID', $provider_id ) : null;
        $provider_name = $provider_user ? $provider_user->display_name : '';
        $provider_email = $provider_user ? $provider_user->user_email : '';
        $provider_phone = $provider_id > 0 ? (string) get_user_meta( $provider_id, 'billing_phone', true ) : '';
        $customer_id = absint( $booking->customer_id ?? 0 );
        $customer_user = $customer_id > 0 ? get_user_by( 'ID', $customer_id ) : null;
        $customer_name = $customer_user ? UserHelper::preferred_name( $customer_user ) : '';
        $customer_email = $customer_user ? (string) $customer_user->user_email : '';
        $customer_phone = (string) ( $booking->customer_phone ?? '' );
        if ( $customer_phone === '' && $customer_id > 0 ) {
            $customer_phone = (string) get_user_meta( $customer_id, 'billing_phone', true );
        }
        if ( $customer_name === '' ) {
            $customer_name = (string) ( $booking->customer_name ?? $booking->owner_name ?? '' );
        }
        if ( $customer_email === '' ) {
            $customer_email = (string) ( $booking->customer_email ?? $booking->email ?? '' );
        }

        if ( $listing_id > 0 ) {
            if ( $provider_email === '' ) {
                $provider_email = (string) get_post_meta( $listing_id, BOOKARO_PROVIDER_EMAIL_META, true );
                if ( $provider_email === '' ) {
                    $provider_email = (string) get_post_meta( $listing_id, '_job_email', true );
                }
            }
            if ( $provider_phone === '' ) {
                $provider_phone = (string) get_post_meta( $listing_id, BOOKARO_PROVIDER_PHONE_META, true );
                if ( $provider_phone === '' ) {
                    $provider_phone = (string) get_post_meta( $listing_id, '_job_phone', true );
                }
            }
        }

        $service_label = (string) ( $booking->service_label ?? '' );

        $booking_date = (string) ( $booking->booking_date ?? '' );
        $booking_date_to = (string) ( $booking->booking_date_to ?? '' );
        // Booking datetimes are naive site-local strings; format via the presenter
        // so emails are not shifted by the GMT offset on non-UTC sites (Bug 72).
        $booking_label = BookingPolicyPresenter::format_date_range_label( $booking_date, $booking_date_to );

        $duration_minutes = 0;
        if ( $booking_date !== '' && $booking_date_to !== '' ) {
            try {
                $tz = wp_timezone();
                $start_dt = new \DateTime( $booking_date, $tz );
                $end_dt = new \DateTime( $booking_date_to, $tz );
                if ( $db->is_day_interval( $provider_id ) ) {
                    $days = (int) $start_dt->diff( $end_dt )->days + 1;
                    $duration_minutes = max( 1, $days ) * 1440;
                } else {
                    $diff = (int) round( ( $end_dt->getTimestamp() - $start_dt->getTimestamp() ) / 60 );
                    if ( $diff > 0 ) {
                        $duration_minutes = $diff;
                    }
                }
            } catch ( \Throwable $e ) {
                $duration_minutes = 0;
            }
        }
        if ( $duration_minutes <= 0 && function_exists( 'bookaro_get_service_duration_minutes' ) ) {
            $service_id = absint( $booking->service_id ?? 0 );
            if ( $service_id > 0 ) {
                $duration_minutes = (int) bookaro_get_service_duration_minutes( $service_id );
            }
        }
        $duration_label = '';
        if ( $duration_minutes > 0 ) {
            $hours = intdiv( $duration_minutes, 60 );
            $mins = $duration_minutes % 60;
            if ( $hours > 0 && $mins > 0 ) {
                /* translators: %1$d: numeric value, %2$d: numeric value, %d: numeric value */
                $duration_label = sprintf( __( '%1$dh %2$dm', 'mybookaro' ), $hours, $mins );
            } elseif ( $hours > 0 ) {
                /* translators: %d: numeric value */
                $duration_label = sprintf( __( '%dh', 'mybookaro' ), $hours );
            } else {
                /* translators: %d: numeric value */
                $duration_label = sprintf( __( '%dm', 'mybookaro' ), $mins );
            }
        }

        $location_label = '';
        if ( $listing_id > 0 ) {
            foreach ( [ 'job_location', 'geolocation_formatted_address', 'listing_location', 'location', 'address' ] as $meta_key ) {
                $value = get_post_meta( $listing_id, $meta_key, true );
                if ( is_string( $value ) && trim( $value ) !== '' ) {
                    $location_label = trim( $value );
                    break;
                }
            }
            $location_label = (string) apply_filters( 'bookaro_booking_location', $location_label, $listing_id, $booking_id, null );
        }

        $notes = (string) ( $booking->notes ?? '' );
        $timezone_label = function_exists( 'wp_timezone_string' ) ? (string) wp_timezone_string() : '';
        if ( $timezone_label === '' ) {
            $timezone_object = wp_timezone();
            $timezone_label = $timezone_object instanceof \DateTimeZone ? (string) $timezone_object->getName() : '';
        }
        if ( $timezone_label === '' ) {
            $timezone_label = 'UTC';
        }

        $status = (string) ( $booking->status ?? '' );
        $status_labels = [
            'pending'   => __( 'Pending', 'mybookaro' ),
            'approved'  => __( 'Approved', 'mybookaro' ),
            'rejected'  => __( 'Rejected', 'mybookaro' ),
            'cancelled' => __( 'Cancelled', 'mybookaro' ),
            'completed' => __( 'Completed', 'mybookaro' ),
            'no_show'   => __( 'No-show', 'mybookaro' ),
        ];
        $status_label = $status !== '' ? ( $status_labels[ $status ] ?? ucfirst( $status ) ) : '';

        $cancel_hours = absint( bookaro_get_option( 'booking_cancel_window_hours', 0 ) );
        $reschedule_hours = absint( bookaro_get_option( 'booking_reschedule_window_hours', 0 ) );
        $cancel_template = trim( (string) bookaro_get_option( 'booking_cancel_policy_text', '' ) );
        $reschedule_template = trim( (string) bookaro_get_option( 'booking_reschedule_policy_text', '' ) );
        $viewer_email = strtolower( trim( $viewer_email ) );
        $viewer_role = (string) ( $replacements['recipient_role'] ?? '' );
        $viewer_role = function_exists( 'sanitize_key' )
            ? sanitize_key( $viewer_role )
            : strtolower( preg_replace( '/[^a-z0-9_\-]/', '', $viewer_role ) );
        if ( $viewer_role === '' ) {
            if ( $viewer_email !== '' && strtolower( (string) $provider_email ) === $viewer_email ) {
                $viewer_role = 'provider';
            } elseif ( $viewer_email !== '' && strtolower( (string) $customer_email ) === $viewer_email ) {
                $viewer_role = 'customer';
            }
        }
        $cancel_window_applies = ! ( $viewer_role === 'provider' && $cancel_hours > 0 && ! bookaro_get_option( 'booking_cancel_policy_apply_providers', 1 ) );
        $payment_policy_text = function_exists( 'bookaro_pro_get_payment_policy_behavior_text' )
            ? (string) bookaro_pro_get_payment_policy_behavior_text()
            : '';
        $deposit_text = function_exists( 'bookaro_pro_get_deposit_behavior_text' )
            ? (string) bookaro_pro_get_deposit_behavior_text()
            : '';
        $cancel_fee_text = function_exists( 'bookaro_pro_get_cancel_fee_behavior_text' )
            ? (string) bookaro_pro_get_cancel_fee_behavior_text()
            : '';
        $no_show_fee_text = function_exists( 'bookaro_pro_get_no_show_fee_behavior_text' )
            ? (string) bookaro_pro_get_no_show_fee_behavior_text()
            : '';
        $policy_bundle = BookingPolicyPresenter::build_policy_bundle(
            [
                'cancel_window_hours' => $cancel_hours,
                'reschedule_window_hours' => $reschedule_hours,
                'cancel_template' => $cancel_template,
                'reschedule_template' => $reschedule_template,
                'cancel_window_applies' => $cancel_window_applies,
                'payment_policy_text' => $payment_policy_text,
                'deposit_text' => $deposit_text,
                'cancel_fee_text' => $cancel_fee_text,
                'no_show_fee_text' => $no_show_fee_text,
            ]
        );
        $policy_cancel = (string) ( $policy_bundle['cancel_text'] ?? '' );
        $policy_reschedule = (string) ( $policy_bundle['reschedule_text'] ?? '' );
        $policies_label = (string) ( $policy_bundle['combined'] ?? '' );
        $policy_lines = isset( $policy_bundle['lines'] ) && is_array( $policy_bundle['lines'] )
            ? $policy_bundle['lines']
            : [];
        $dashboard_url = function_exists( 'bookaro_get_dashboard_url' )
            ? bookaro_get_dashboard_url( [ 'tab' => 'bookings', 'listing_id' => $listing_id ] )
            : site_url( '/my-account/bookings-and-management/?tab=bookings' );
        if ( ! is_string( $dashboard_url ) ) {
            $dashboard_url = '';
        }

        $details = [];
        if ( $status_label !== '' ) {
            $details[ __( 'Status', 'mybookaro' ) ] = $status_label;
        }
        $details[ __( 'Booking ID', 'mybookaro' ) ] = '#' . (string) $booking_id;
        if ( $service_label !== '' ) {
            $details[ __( 'Service', 'mybookaro' ) ] = $service_label;
        }
        if ( $booking_label !== '' ) {
            $details[ __( 'Date & time', 'mybookaro' ) ] = $booking_label;
        }
        if ( $timezone_label !== '' ) {
            $details[ __( 'Timezone', 'mybookaro' ) ] = $timezone_label;
        }
        if ( $duration_label !== '' ) {
            $details[ __( 'Duration', 'mybookaro' ) ] = $duration_label;
        }
        if ( $listing_title !== '' ) {
            $details[ $this->get_resource_label_singular() ] = $listing_url !== '' ? $listing_title . ' (' . $listing_url . ')' : $listing_title;
        }
        if ( $location_label !== '' ) {
            $details[ __( 'Location', 'mybookaro' ) ] = $location_label;
        }
        if ( $provider_name !== '' ) {
            $details[ __( 'Provider', 'mybookaro' ) ] = $provider_name;
        }
        if ( $customer_name !== '' ) {
            $details[ __( 'Customer', 'mybookaro' ) ] = $customer_name;
        }
        if ( $provider_email !== '' ) {
            $details[ __( 'Provider email', 'mybookaro' ) ] = $provider_email;
        }
        if ( $provider_phone !== '' ) {
            $details[ __( 'Provider phone', 'mybookaro' ) ] = $provider_phone;
        }
        if ( $customer_email !== '' ) {
            $details[ __( 'Customer email', 'mybookaro' ) ] = $customer_email;
        }
        if ( $customer_phone !== '' ) {
            $details[ __( 'Customer phone', 'mybookaro' ) ] = $customer_phone;
        }
        if ( $notes !== '' ) {
            $details[ __( 'Notes', 'mybookaro' ) ] = $notes;
        }
        if ( $policies_label !== '' ) {
            $details[ __( 'Policies', 'mybookaro' ) ] = $policies_label;
        }
        $details = apply_filters( 'bookaro_booking_email_details', $details, $booking_id );

        $context = [
            'details' => is_array( $details ) ? $details : [],
            'contacts' => [
                'provider' => [
                    'label' => __( 'Provider', 'mybookaro' ),
                    'name' => $provider_name,
                    'email' => $provider_email,
                    'phone' => $provider_phone,
                ],
                'customer' => [
                    'label' => __( 'Customer', 'mybookaro' ),
                    'name' => $customer_name,
                    'email' => $customer_email,
                    'phone' => $customer_phone,
                ],
            ],
            'timezone' => $timezone_label,
            'policies' => [
                'cancel' => $policy_cancel,
                'reschedule' => $policy_reschedule,
                'payment_policy' => (string) ( $policy_bundle['payment_policy_text'] ?? '' ),
                'deposit' => (string) ( $policy_bundle['deposit_text'] ?? '' ),
                'cancel_fee' => (string) ( $policy_bundle['cancel_fee_text'] ?? '' ),
                'no_show_fee' => (string) ( $policy_bundle['no_show_fee_text'] ?? '' ),
                'lines' => $policy_lines,
                'combined' => $policies_label,
            ],
            'dashboard_url' => $dashboard_url,
            'listing_title' => $listing_title,
            'listing_url' => $listing_url,
            'provider_email' => $provider_email,
            'provider_phone' => $provider_phone,
            'customer_phone' => $customer_phone,
        ];

        /**
         * Filter full booking email context before template rendering.
         *
         * @param array $context
         * @param int   $booking_id
         * @param mixed $booking
         */
        $context = apply_filters( 'bookaro_booking_email_context', $context, $booking_id, $booking );
        return is_array( $context ) ? $context : [];
    }

    private function build_booking_details_for_email( int $booking_id ): array
    {
        $context = $this->build_booking_email_context( $booking_id );
        $details = $context['details'] ?? [];
        return is_array( $details ) ? $details : [];
    }

    /**
     * Send reminder emails (24h/1h).
     */
    public function sendReminder(int $booking_id, string $template_key): void
    {
        try {
            global $wpdb;
            $db    = new BookingRepository();

            // Skip if a successful send already exists for this booking +
            // template. Handles overlapping cron runs and post-failure
            // retries without sending the customer the same reminder twice.
            // (This is a best-effort check — an exact-concurrent race
            // between the SELECT and INSERT remains theoretically possible
            // but is rare for a hourly/daily cron.)
            if ( $db->notification_log_has_send( $booking_id, 'email', $template_key ) ) {
                return;
            }

            $table = esc_sql( $db->get_table_name() );

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $b = $wpdb->get_row(
                $wpdb->prepare(
                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    "SELECT * FROM `{$table}` WHERE id = %d",
                    $booking_id
                ),
                ARRAY_A
            );
            if (!$b) {
                return;
            }

            $status = sanitize_key( (string) ( $b['status'] ?? '' ) );
            if ( $status !== 'approved' ) {
                return;
            }

            $owner = get_user_by('ID', $b['customer_id']);
            $prov  = get_user_by('ID', $b['provider_id']);
            if (!$owner || !$prov) {
                return;
            }
            $dashboard_url = function_exists( 'bookaro_get_dashboard_url' )
                ? bookaro_get_dashboard_url( [ 'tab' => 'bookings', 'listing_id' => (int) $b['listing_id'] ] )
                : site_url( '/my-account/bookings-and-management/?tab=bookings' );
            $this->sendTemplatedEmail(
                $owner->user_email,
                $template_key,
                [
                    'owner'    => UserHelper::preferred_name($owner),
                    'service'  => $b['service_label'],
                    'date'     => $b['booking_date'],
                    'date_to'  => $b['booking_date_to'],
                    'provider' => UserHelper::preferred_name($prov),
                    'manage_booking_url' => $dashboard_url,
                ],
                $prov->ID,
                [],
                $booking_id
            );
        } catch (\Throwable $e) {
            Logger::log('send_reminder failed', [
                'exception' => $e->getMessage(),
                'code' => $e->getCode(),
                'trace' => $e->getTraceAsString(),
            ]);
        }
    }

    /**
     * Send a "leave a review" invitation email.
     */
    public function sendReviewInvite(int $booking_id): void
    {
        try {
            $db      = new BookingRepository();

            // Skip if we've already sent the review invite for this booking.
            // sendReviewInvite is scheduled once per booking but may be
            // re-triggered by status churn or duplicate cron runs.
            if ( $db->notification_log_has_send( $booking_id, 'email', 'review_invite' ) ) {
                return;
            }

            $booking = $db->get_booking($booking_id);
            if (!$booking) {
                return;
            }

            if ( sanitize_key( (string) ( $booking->status ?? '' ) ) !== 'completed' ) {
                return;
            }

            $owner    = get_user_by('ID', $booking->customer_id);
            $provider = get_user_by('ID', $booking->provider_id);
            if (!$owner || !$provider) {
                return;
            }

            $listing_id = absint( $booking->listing_id ?? 0 );
            $provider_id = absint( $booking->provider_id ?? 0 );
            $resource_context = $this->resolve_booking_resource_context( $listing_id, $provider_id );
            $listing_url = (string) ( $resource_context['url'] ?? '' );
            if ( $listing_url !== '' ) {
                $review_link = trailingslashit( $listing_url ) . '#reviews';
            } else {
                $review_link = function_exists( 'bookaro_get_dashboard_url' )
                    ? (string) bookaro_get_dashboard_url( [ 'tab' => 'bookings', 'listing_id' => $listing_id ] )
                    : site_url( '/my-account/bookings-and-management/?tab=bookings' );
            }

            $this->sendTemplatedEmail(
                $owner->user_email,
                'review_invite',
                [
                    'owner_name'      => UserHelper::preferred_name($owner),
                    'service'         => $booking->service_label,
                    'booking_date'    => $booking->booking_date,
                    'booking_date_to' => $booking->booking_date_to,
                    'provider'        => UserHelper::preferred_name($provider),
                    'review_link'     => $review_link,
                    'listing_title'   => (string) ( $resource_context['title'] ?? '' ),
                    'listing_url'     => $listing_url,
                ],
                $provider->ID,
                [],
                $booking_id
            );
        } catch (\Throwable $e) {
            Logger::log('send_review_invite failed', [
                'exception' => $e->getMessage(),
                'code' => $e->getCode(),
                'trace' => $e->getTraceAsString(),
            ]);
        }
    }

}
