<?php
/**
 * Email Notifications Handler.
 *
 * Handles sending email notifications for call events by delegating
 * to the SiteStaffr middleware (SendGrid) instead of wp_mail().
 *
 * @link       https://github.com/sitestaffr/sitestaffr-plugin
 * @since      1.0.0
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 */

/**
 * Email Notifications Handler class.
 *
 * Provides methods for sending email notifications when calls complete.
 * Email rendering and delivery is handled by the middleware via SendGrid.
 * This class acts as a data provider — it fetches call data from the DB,
 * builds a structured payload, and sends it to the middleware.
 *
 * @since      1.0.0
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 * @author     SiteStaffr
 */
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin table names are internally derived from $wpdb->prefix and values are placeholder-prepared.
class SiteStaffr_Email_Notifications {

    /**
     * Last mail error message for diagnostic display.
     *
     * @since 0.14.269
     * @var string
     */
    private $last_mail_error = '';

    /**
     * Get the last mail error message.
     *
     * @since 0.14.269
     * @return string
     */
    public function get_last_mail_error() {
        return $this->last_mail_error;
    }

    /**
     * Send notification email for a completed call via middleware.
     *
     * Fetches call data from the database, builds a structured
     * payload, and sends it to the middleware for rendering and delivery via SendGrid.
     *
     * This method is triggered by the sitestaffr_call_completed action which fires
     * when a call ends with any terminal status (completed, no-answer, failed, busy, canceled).
     *
     * @since    1.0.0
     * @param    int        $call_id      The call ID to send notification for.
     * @param    string     $call_status  Optional. The final call status.
     * @param    array|null $recipients   Optional explicit recipient list (from the
     *                                     "Email Conversation" picker). When given,
     *                                     these override the configured primary+CC and
     *                                     the enabled-toggle gate is skipped (the send
     *                                     is a deliberate manual action).
     * @return   bool                    True if email sent successfully, false otherwise.
     */
    public function send_call_notification( $call_id, $call_status = '', $recipients = null ) {
        SiteStaffr_Logger::legacy( 'SiteStaffr Email: send_call_notification called for call_id: ' . $call_id . ', status: ' . $call_status );

        // Get settings
        $settings = get_option( 'sitestaffr_settings', array() );

        $manual = is_array( $recipients ) && ! empty( $recipients );

        // Check if notifications are enabled (automatic sends only; a manual
        // "Email Conversation" is an explicit user action and always sends).
        if ( ! $manual && isset( $settings['email_notifications_enabled'] ) && ! $settings['email_notifications_enabled'] ) {
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                SiteStaffr_Logger::legacy( 'SiteStaffr Email: Notifications are disabled in settings' );
            }
            return false;
        }

        // Resolve recipients: explicit picker list, else configured primary + CC.
        if ( $manual ) {
            list( $to, $cc_recipients ) = $this->split_recipients( $recipients );
        } else {
            $to            = ! empty( $settings['notification_email'] ) ? $settings['notification_email'] : get_option( 'admin_email' );
            $cc_recipients = $this->get_additional_notification_recipients( $settings, $to );
        }

        if ( empty( $to ) || ! is_email( $to ) ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: No valid notification email configured' );
            return false;
        }

        // Get call data
        $call = $this->get_call_data( $call_id );
        if ( ! $call ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: Cannot send notification - call not found: ' . $call_id );
            return false;
        }

        // Skip filtered calls (is_billable = 0) - these are spam/low-quality calls
        if ( isset( $call->is_billable ) && $call->is_billable == 0 ) {
            SiteStaffr_Logger::legacy( "SiteStaffr Email: Call {$call_id} filtered (is_billable=0) - email notification skipped" );
            return false;
        }

        // Skip test conversations for automatic sends — "Test your widget" runs
        // should never notify. A manual "Email Conversation" still sends.
        if ( ! $manual && ! empty( $call->is_test_call ) ) {
            SiteStaffr_Logger::legacy( "SiteStaffr Email: Call {$call_id} is a test conversation - email notification skipped" );
            return false;
        }

        // Honour the owner's notification scope. Default is every conversation;
        // "qualified" limits automatic emails to conversations that captured a lead.
        // The qualified flag is set at priority 5 by
        // SiteStaffr_Leads::maybe_flag_qualified_lead, before this notify (10).
        if ( ! SiteStaffr_Notification_Scope::should_notify(
            SiteStaffr_Notification_Scope::from_settings( $settings ),
            ! empty( $call->is_qualified_lead ),
            $manual
        ) ) {
            SiteStaffr_Logger::legacy( "SiteStaffr Email: Call {$call_id} is not a qualified lead and the notification scope is qualified-only - skipped" );
            return false;
        }

        SiteStaffr_Logger::legacy( "SiteStaffr Email: Sending notification for billable call {$call_id}" );

        // Build structured email data for middleware
        $email_data = array(
            'business_name'  => ! empty( $settings['business_name'] ) ? stripslashes( $settings['business_name'] ) : 'SiteStaffr',
            'call_started_at' => $this->format_started_at_eastern( $call->started_at ),
            'call_duration_seconds' => ! empty( $call->call_duration ) ? (int) $call->call_duration : 0,
            'call_status'    => $call->call_status,
            'is_test_call'   => ! empty( $call->is_test_call ),
            'summary_html'   => ! empty( $call->ai_summary ) ? stripslashes( $call->ai_summary ) : '',
            'dashboard_url'  => admin_url( 'admin.php?page=sitestaffr' ),
            'call_detail_url' => $this->get_call_url( $call_id ),
            'settings_url'   => admin_url( 'admin.php?page=sitestaffr-settings' ),
            'leads_url'      => class_exists( 'SiteStaffr_Leads' ) ? SiteStaffr_Leads::get_page_url() : '',
            'plan_inactive'  => ( class_exists( 'SiteStaffr_Billing_State' ) && SiteStaffr_Billing_State::is_subscription_inactive() ),
            // Drives the email status pill: qualified leads read "Qualified Lead",
            // ordinary conversations get no pill. Flag is set at priority 5 by
            // SiteStaffr_Leads::maybe_flag_qualified_lead, before this notify (10).
            'qualified_lead' => ! empty( $call->is_qualified_lead ),
        );

        // Structured lead contact for the actionable card in the email (name +
        // one-tap Call/Email). Only for qualified leads — these columns are
        // populated by maybe_flag_qualified_lead. The recap already carries the
        // same contact, so this adds no new data exposure to the middleware.
        if ( ! empty( $call->is_qualified_lead ) ) {
            $email_data['lead_name']     = isset( $call->lead_name ) ? (string) $call->lead_name : '';
            $email_data['lead_phone']    = isset( $call->lead_phone ) ? (string) $call->lead_phone : '';
            $email_data['lead_email']    = isset( $call->lead_email ) ? (string) $call->lead_email : '';
            $email_data['lead_interest'] = isset( $call->lead_interest ) ? (string) $call->lead_interest : '';
        }

        // Chat-ended indicator: only forward allowlisted values; legacy rows /
        // absent column stay omitted so middleware renders nothing.
        if ( isset( $call->ended_by ) && in_array( $call->ended_by, array( 'ai_ended', 'visitor_left', 'timed_out', 'link_clicked' ), true ) ) {
            $email_data['ended_by'] = $call->ended_by;
        }

        // Send one notification request with optional CC recipients.
        $result = $this->send_via_middleware(
            SiteStaffr_Public_Middleware_Client::path_send_notification_email(),
            $to,
            $email_data,
            $cc_recipients
        );

        $recipient_log = $to;
        if ( ! empty( $cc_recipients ) ) {
            $recipient_log .= sprintf(
                /* translators: %d: Number of CC recipients */
                __( ' (+%d CC)', 'sitestaffr' ),
                count( $cc_recipients )
            );
        }
        $this->log_email_result( $call_id, $recipient_log, $result );

        return $result;
    }

    /**
     * Send a "post ready to review" email with a no-login review/publish link.
     *
     * Fires when a Blog Agent post lands as a draft under review-by-email mode.
     * Carries a scoped, expiring magic link to the no-login review surface; the
     * post body stays in WordPress (only title + a short preview travel to the
     * middleware for the email).
     *
     * @since 1.28.0
     * @param int        $wp_post_id Draft post ID.
     * @param array|null $recipients Optional explicit recipient list (from the
     *                               "Email Review Link" picker). Overrides the
     *                               configured primary+CC when given.
     * @param bool       $force      When true, send even if already emailed once
     *                               (a deliberate manual re-send) and ignore the
     *                               enabled-toggle gate.
     * @return bool True if the middleware accepted the request.
     */
    public function send_blog_ready_notification( $wp_post_id, $recipients = null, $force = false ) {
        $wp_post_id = absint( $wp_post_id );
        $post       = $wp_post_id ? get_post( $wp_post_id ) : null;
        if ( ! $post ) {
            return false;
        }

        $manual = is_array( $recipients ) && ! empty( $recipients );

        // Send-once invariant lives with the sender, so every automatic caller gets
        // it for free. A forced manual send (Email Review Link) bypasses it.
        if ( ! $force && get_post_meta( $wp_post_id, '_sitestaffr_blog_review_emailed', true ) ) {
            return true;
        }

        $settings = get_option( 'sitestaffr_settings', array() );

        // Blog emails target the Blog agent's OWN recipient list (separate from
        // conversation notifications). The automatic path honors the Blog agent's
        // own enable toggle; a manual send (Email Review Link) is deliberate and
        // bypasses it.
        if ( $manual ) {
            list( $to, $cc_recipients ) = $this->split_recipients( $recipients );
        } else {
            if ( ! $this->blog_notifications_enabled() ) {
                return false;
            }
            list( $to, $cc_recipients ) = $this->resolve_blog_recipients();
        }
        if ( empty( $to ) || ! is_email( $to ) ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: No valid recipient for blog-ready notice' );
            return false;
        }

        $review_url = class_exists( 'SiteStaffr_Magic_Link' )
            ? SiteStaffr_Magic_Link::generate_scoped_url( 'blog-review', $wp_post_id, 'sitestaffr_blog_review', 30 * DAY_IN_SECONDS )
            : '';
        if ( '' === $review_url ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: Could not sign blog review link (no secret); skipping notice' );
            return false;
        }

        $preview = wp_trim_words( wp_strip_all_tags( $post->post_content ), 40, '…' );
        $thumb   = get_the_post_thumbnail_url( $wp_post_id, 'large' );

        $email_data = array(
            'business_name'      => ! empty( $settings['business_name'] ) ? stripslashes( $settings['business_name'] ) : 'SiteStaffr',
            'post_title'         => get_the_title( $wp_post_id ),
            'post_preview'       => $preview,
            'featured_image_url' => $thumb ? $thumb : '',
            'review_url'         => $review_url,
            'settings_url'       => admin_url( 'admin.php?page=sitestaffr-settings' ),
        );

        $result = $this->send_via_middleware(
            SiteStaffr_Public_Middleware_Client::path_send_blog_ready_email(),
            $to,
            $email_data,
            $cc_recipients
        );

        if ( $result ) {
            update_post_meta( $wp_post_id, '_sitestaffr_blog_review_emailed', 1 );
        }

        $recipient_log = $to;
        if ( ! empty( $cc_recipients ) ) {
            $recipient_log .= ' +' . count( $cc_recipients ) . ' CC';
        }
        SiteStaffr_Logger::legacy( sprintf( 'SiteStaffr Email: blog-ready notice for post #%d to %s — %s', $wp_post_id, $recipient_log, $result ? 'SUCCESS' : 'FAILED' ) );

        return $result;
    }

    /**
     * Send a "post published" email with a preview when Autopilot auto-publishes.
     *
     * Mirrors send_blog_ready_notification() but for a live post: the CTA links to
     * the published permalink rather than the no-login review surface. Send-once
     * guarded so an idempotent re-delivery can't double-email.
     *
     * @since 1.28.0
     * @param int $wp_post_id Published post ID.
     * @return bool True if the middleware accepted the request.
     */
    public function send_blog_published_notification( $wp_post_id ) {
        $wp_post_id = absint( $wp_post_id );
        $post       = $wp_post_id ? get_post( $wp_post_id ) : null;
        if ( ! $post ) {
            return false;
        }

        if ( get_post_meta( $wp_post_id, '_sitestaffr_blog_published_emailed', true ) ) {
            return true;
        }

        // Blog agent's own enable toggle + recipient list (dedicated option).
        if ( ! $this->blog_notifications_enabled() ) {
            return false;
        }
        list( $to, $cc_recipients ) = $this->resolve_blog_recipients();
        if ( empty( $to ) || ! is_email( $to ) ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: No valid recipient for blog-published notice' );
            return false;
        }

        $settings = get_option( 'sitestaffr_settings', array() );
        $preview  = wp_trim_words( wp_strip_all_tags( $post->post_content ), 40, '…' );
        $thumb    = get_the_post_thumbnail_url( $wp_post_id, 'large' );

        $email_data = array(
            'business_name'      => ! empty( $settings['business_name'] ) ? stripslashes( $settings['business_name'] ) : 'SiteStaffr',
            'post_title'         => get_the_title( $wp_post_id ),
            'post_preview'       => $preview,
            'featured_image_url' => $thumb ? $thumb : '',
            'view_url'           => get_permalink( $wp_post_id ),
            'settings_url'       => admin_url( 'admin.php?page=sitestaffr-my-agent' ),
        );

        $result = $this->send_via_middleware(
            SiteStaffr_Public_Middleware_Client::path_send_blog_published_email(),
            $to,
            $email_data,
            $cc_recipients
        );

        if ( $result ) {
            update_post_meta( $wp_post_id, '_sitestaffr_blog_published_emailed', 1 );
        }

        $recipient_log = $to;
        if ( ! empty( $cc_recipients ) ) {
            $recipient_log .= ' +' . count( $cc_recipients ) . ' CC';
        }
        SiteStaffr_Logger::legacy( sprintf( 'SiteStaffr Email: blog-published notice for post #%d to %s — %s', $wp_post_id, $recipient_log, $result ? 'SUCCESS' : 'FAILED' ) );

        return $result;
    }

    /**
     * Send the monthly "AI visibility" report email for a completed run.
     *
     * Recipients are the Blog Agent's own email list (resolve_blog_recipients(),
     * NO fallback to conversation notifications — same rule as the blog-ready
     * notice). The middleware's /send-visibility-report route is per-recipient
     * with no CC support, so each configured address (primary + CC) gets its
     * own signed POST.
     *
     * Called by SiteStaffr_Visibility::run_tick() after record_run() has
     * already appended this run to history — so the "previous" trend point is
     * the second-to-last history entry, not the last.
     *
     * @since 1.36.0
     * @param array $run Completed visibility run (score/prompts/date/byPlatform).
     * @return bool True if at least one recipient's email was sent successfully.
     */
    public function send_visibility_report( array $run ) {
        list( $primary, $cc ) = $this->resolve_blog_recipients();

        $recipients = array();
        $seen       = array();
        if ( ! empty( $primary ) ) {
            $recipients[] = $primary;
            $seen[ strtolower( $primary ) ] = true;
        }
        foreach ( $cc as $cc_email ) {
            $key = strtolower( $cc_email );
            if ( isset( $seen[ $key ] ) ) {
                continue;
            }
            $seen[ $key ] = true;
            $recipients[] = $cc_email;
        }

        if ( empty( $recipients ) ) {
            return false;
        }

        $settings      = get_option( 'sitestaffr_settings', array() );
        $business_name = ! empty( $settings['business_name'] ) ? stripslashes( $settings['business_name'] ) : 'SiteStaffr';

        $score_source = ( isset( $run['score'] ) && is_array( $run['score'] ) ) ? $run['score'] : array();
        $score        = array(
            'cited' => isset( $score_source['cited'] ) ? (int) $score_source['cited'] : 0,
            'total' => isset( $score_source['total'] ) ? (int) $score_source['total'] : 0,
        );

        // Per-engine breakdown for the email, mirroring the dashboard tiles.
        $by_platform    = ( isset( $run['byPlatform'] ) && is_array( $run['byPlatform'] ) ) ? $run['byPlatform'] : array();
        $email_platform = array();
        foreach ( array( 'perplexity', 'chatgpt', 'gemini' ) as $platform_key ) {
            $p = ( isset( $by_platform[ $platform_key ] ) && is_array( $by_platform[ $platform_key ] ) ) ? $by_platform[ $platform_key ] : array();
            $email_platform[ $platform_key ] = array(
                'cited'     => isset( $p['cited'] ) ? max( 0, (int) $p['cited'] ) : 0,
                'total'     => isset( $p['total'] ) ? max( 0, (int) $p['total'] ) : 0,
                'available' => ! empty( $p['available'] ),
            );
        }

        // "Who's showing up instead" — top competitors from the aggregate roll-up.
        $aggregate         = ( isset( $run['competitorsAggregate'] ) && is_array( $run['competitorsAggregate'] ) ) ? $run['competitorsAggregate'] : array();
        $email_competitors = array();
        foreach ( array_slice( $aggregate, 0, 5 ) as $row ) {
            if ( ! is_array( $row ) || empty( $row['domain'] ) ) {
                continue;
            }
            $email_competitors[] = array(
                'domain' => (string) $row['domain'],
                'count'  => isset( $row['count'] ) ? max( 0, (int) $row['count'] ) : 0,
            );
        }

        $email_data = array(
            'business_name' => $business_name,
            'score'         => $score,
            'trend_delta'   => $this->calculate_visibility_trend_delta( $score['cited'] ),
            'by_platform'   => $email_platform,
            'competitors'   => $email_competitors,
            'top_gap'       => $this->find_visibility_top_gap( $run ),
            'dashboard_url' => admin_url( 'admin.php?page=sitestaffr-my-agent&agent=blog&tab=visibility' ),
        );

        $sent_any = false;
        foreach ( $recipients as $recipient ) {
            $result = $this->send_via_middleware(
                SiteStaffr_Public_Middleware_Client::path_send_visibility_report_email(),
                $recipient,
                $email_data
            );
            if ( $result ) {
                $sent_any = true;
            }
            SiteStaffr_Logger::legacy( sprintf( 'SiteStaffr Email: visibility report to %s — %s', $recipient, $result ? 'SUCCESS' : 'FAILED' ) );
        }

        return $sent_any;
    }

    /**
     * Trend delta vs. the previous history point.
     *
     * record_run() has already appended the current run to
     * SiteStaffr_Visibility::get_state()['history'] by the time this runs
     * (see run_tick()), so the "previous" point is the second-to-last entry,
     * not the last. Returns 0 when there is no prior point.
     *
     * @since 1.36.0
     * @param int $current_cited Current run's score.cited.
     * @return int
     */
    private function calculate_visibility_trend_delta( $current_cited ) {
        if ( ! class_exists( 'SiteStaffr_Visibility' ) ) {
            return 0;
        }

        $state   = SiteStaffr_Visibility::get_state();
        $history = is_array( $state['history'] ) ? $state['history'] : array();
        if ( count( $history ) < 2 ) {
            return 0;
        }

        $previous       = $history[ count( $history ) - 2 ];
        $previous_cited = ( is_array( $previous ) && isset( $previous['score']['cited'] ) ) ? (int) $previous['score']['cited'] : 0;

        return (int) $current_cited - $previous_cited;
    }

    /**
     * The single biggest visibility gap: the first tracked prompt that was
     * NOT cited, plus its first listed competitor. Null when every tracked
     * prompt was cited (or there are no prompts).
     *
     * @since 1.36.0
     * @param array $run Completed visibility run.
     * @return array|null { text, competitor }
     */
    private function find_visibility_top_gap( array $run ) {
        $prompts = ( isset( $run['prompts'] ) && is_array( $run['prompts'] ) ) ? $run['prompts'] : array();

        foreach ( $prompts as $prompt ) {
            if ( ! is_array( $prompt ) || ! empty( $prompt['cited'] ) ) {
                continue;
            }

            $competitors = ( isset( $prompt['competitors'] ) && is_array( $prompt['competitors'] ) ) ? $prompt['competitors'] : array();

            return array(
                'text'       => isset( $prompt['text'] ) ? (string) $prompt['text'] : '',
                'competitor' => ! empty( $competitors[0] ) ? (string) $competitors[0] : '',
            );
        }

        return null;
    }

    /**
     * Send a one-time welcome email after the setup wizard completes.
     *
     * Fires on the sitestaffr_setup_wizard_completed action. Send-once guarded
     * by an option so re-running the wizard never re-sends; a failed send leaves
     * the guard unset so the next wizard completion retries.
     *
     * @since 1.31.0
     * @return bool True if the middleware accepted the request.
     */
    public function send_welcome_email() {
        if ( get_option( 'sitestaffr_welcome_email_sent' ) ) {
            return true;
        }

        $settings = get_option( 'sitestaffr_settings', array() );

        $to = ! empty( $settings['notification_email'] ) ? $settings['notification_email'] : get_option( 'admin_email' );
        if ( empty( $to ) || ! is_email( $to ) ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: No valid recipient for welcome email' );
            return false;
        }

        $email_data = array(
            'business_name'      => ! empty( $settings['business_name'] ) ? stripslashes( $settings['business_name'] ) : 'SiteStaffr',
            'notification_email' => $to,
            'dashboard_url'      => admin_url( 'admin.php?page=sitestaffr' ),
            'settings_url'       => admin_url( 'admin.php?page=sitestaffr-settings' ),
            'leads_url'          => class_exists( 'SiteStaffr_Leads' ) ? SiteStaffr_Leads::get_page_url() : '',
        );

        $result = $this->send_via_middleware(
            SiteStaffr_Public_Middleware_Client::path_send_welcome_email(),
            $to,
            $email_data
        );

        if ( $result ) {
            update_option( 'sitestaffr_welcome_email_sent', 1, false );
        }

        SiteStaffr_Logger::legacy( sprintf( 'SiteStaffr Email: welcome email to %s — %s', $to, $result ? 'SUCCESS' : 'FAILED' ) );

        return $result;
    }

    /**
     * Send test email to verify settings work via middleware.
     *
     * @since    1.0.0
     * @param    string|null    $to    Optional email address to send to.
     * @param    string         $agent_label Which agent the test is for ("Voice & Text
     *                                  Agent" / "Blog Agent"), shown in the email so the
     *                                  recipient knows which test arrived.
     * @return   bool                  True if email sent successfully, false otherwise.
     */
    public function send_test_email( $to = null, $agent_label = '' ) {
        $this->last_mail_error = '';
        $settings = get_option( 'sitestaffr_settings', array() );

        $cc_recipients = array();
        if ( ! $to ) {
            $to = ! empty( $settings['notification_email'] ) ? $settings['notification_email'] : get_option( 'admin_email' );
            $cc_recipients = $this->get_additional_notification_recipients( $settings, $to );
        }

        if ( empty( $to ) || ! is_email( $to ) ) {
            $this->last_mail_error = 'Invalid recipient email address.';
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: Cannot send test email - invalid recipient' );
            return false;
        }

        $email_data = array(
            'business_name'      => ! empty( $settings['business_name'] ) ? stripslashes( $settings['business_name'] ) : 'Your Business',
            'notification_email' => ! empty( $settings['notification_email'] ) ? $settings['notification_email'] : '',
            'agent_label'        => (string) $agent_label,
            'dashboard_url'      => admin_url( 'admin.php?page=sitestaffr' ),
            'settings_url'       => admin_url( 'admin.php?page=sitestaffr-settings' ),
            'current_utc'        => gmdate( 'c' ),
        );

        $result = $this->send_via_middleware(
            SiteStaffr_Public_Middleware_Client::path_send_test_email(),
            $to,
            $email_data,
            $cc_recipients
        );

        $recipient_log = $to;
        if ( ! empty( $cc_recipients ) ) {
            $recipient_log .= ' +' . count( $cc_recipients ) . ' CC';
        }

        if ( $result ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: Test email sent successfully to ' . $recipient_log );
        } else {
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: Test email FAILED to send to ' . $recipient_log . ( $this->last_mail_error ? ' — ' . $this->last_mail_error : '' ) );
        }

        return $result;
    }

    /**
     * Send an agent-health alert to the owner.
     *
     * Deliberately ignores `email_notifications_enabled`. That toggle means "stop
     * emailing me about conversations"; a paused agent is a service failure, not a
     * routine update — and the owner most likely to mute conversation email is the
     * one least likely to notice the outage in wp-admin.
     *
     * @since    1.40.0
     * @param    string $variant One of payment|trial|minutes|all_clear.
     * @return   bool            True if the middleware accepted the send.
     */
    public function send_health_alert( $variant ) {
        $settings = get_option( 'sitestaffr_settings', array() );
        $to       = ! empty( $settings['notification_email'] ) ? $settings['notification_email'] : get_option( 'admin_email' );

        if ( empty( $to ) || ! is_email( $to ) ) {
            SiteStaffr_Logger::legacy( 'SiteStaffr Health: No valid notification email configured - health alert skipped' );
            return false;
        }

        $email_data = array(
            'variant'       => (string) $variant,
            'business_name' => ! empty( $settings['business_name'] ) ? stripslashes( $settings['business_name'] ) : 'SiteStaffr',
            'manage_url'    => 'https://sitestaffr.com/manage/',
        );

        return (bool) $this->send_via_middleware(
            SiteStaffr_Public_Middleware_Client::path_send_health_alert_email(),
            $to,
            $email_data,
            $this->get_additional_notification_recipients( $settings, $to )
        );
    }

    /**
     * Send email via middleware (SendGrid).
     *
     * Makes an HMAC-signed POST request to the middleware email endpoint.
     *
     * @since    0.14.269
     * @param    string    $path        Middleware API path.
     * @param    string    $to          Recipient email address.
     * @param    array     $email_data  Structured email data.
     * @param    array     $cc_recipients Optional CC recipients array.
     * @return   bool                   True if middleware accepted the request.
     */
    private function send_via_middleware( $path, $to, $email_data, $cc_recipients = array() ) {
        $api_secret = $this->get_api_secret();
        $preflight  = SiteStaffr_Public_Middleware_Client::get_public_preflight_config( $api_secret );

        if ( $preflight['missing_api_secret'] || $preflight['missing_middleware_url'] ) {
            $this->last_mail_error = 'Email service not configured. Please complete plugin setup.';
            SiteStaffr_Logger::legacy( 'SiteStaffr Email: Missing API secret or middleware URL' );
            return false;
        }

        $site_token = get_option( 'sitestaffr_site_token', '' );

        $payload = array(
            'site_token' => $site_token,
            'site_url'   => site_url(),
            'to'         => $to,
            'email_data' => $email_data,
        );
        $normalized_cc = $this->normalize_cc_recipients( $cc_recipients, $to );
        if ( ! empty( $normalized_cc ) ) {
            // Include CC at both top-level and in email_data for middleware compatibility.
            $payload['cc'] = $normalized_cc;
            $payload['email_data']['cc'] = $normalized_cc;
        }

        $payload_json = wp_json_encode( $payload );
        $endpoint     = SiteStaffr_Public_Middleware_Client::build_middleware_endpoint(
            $preflight['middleware_url'],
            $path
        );

        $response = SiteStaffr_Public_Middleware_Client::post_signed_json_with_json_response(
            $endpoint,
            $payload_json,
            $api_secret,
            null,
            '',
            SiteStaffr_Public_Middleware_Client::timeout_email(),
            SiteStaffr_Public_Middleware_Client::extra_headers_json()
        );

        if ( is_wp_error( $response ) ) {
            $this->last_mail_error = 'Failed to connect to email service: ' . $response->get_error_message();
            return false;
        }

        $status_code  = isset( $response['status_code'] ) ? (int) $response['status_code'] : 0;
        $decoded_body = isset( $response['decoded_body'] ) ? $response['decoded_body'] : null;

        if ( 200 === $status_code && ! empty( $decoded_body['success'] ) ) {
            return true;
        }

        // Extract error detail from middleware response
        $error_detail = '';
        if ( is_array( $decoded_body ) ) {
            if ( ! empty( $decoded_body['detail'] ) ) {
                $error_detail = $decoded_body['detail'];
            } elseif ( ! empty( $decoded_body['error'] ) ) {
                $error_detail = $decoded_body['error'];
            }
        }

        $this->last_mail_error = ! empty( $error_detail )
            ? 'Email service error: ' . $error_detail
            : 'Email service returned status ' . $status_code;

        SiteStaffr_Logger::legacy(
            sprintf(
                'SiteStaffr Email: Middleware returned %s',
                SiteStaffr_Public_Middleware_Client::get_status_body_for_log(
                    $status_code,
                    isset( $response['body'] ) ? $response['body'] : ''
                )
            )
        );

        return false;
    }

    /**
     * Get the API secret for HMAC signing.
     *
     * @since    0.14.269
     * @return   string
     */
    private function get_api_secret() {
        // Prefer per-site secret (v0.13.72+), fall back to global secret
        $site_api_secret = get_option( 'sitestaffr_site_api_secret' );
        if ( ! empty( $site_api_secret ) ) {
            return $site_api_secret;
        }

        $api_secret = get_option( 'sitestaffr_api_secret' );
        if ( empty( $api_secret ) ) {
            $api_secret = bin2hex( random_bytes( 32 ) );
            update_option( 'sitestaffr_api_secret', $api_secret );
        }

        return $api_secret;
    }

    /**
     * Get additional notification recipients from settings.
     *
     * @since 0.14.269
     * @param array  $settings      Plugin settings.
     * @param string $primary_email Primary recipient to exclude from CC list.
     * @return array
     */
    private function get_additional_notification_recipients( $settings, $primary_email = '' ) {
        $max_cc_recipients = 5;
        if ( empty( $settings['notification_cc_emails'] ) || ! is_string( $settings['notification_cc_emails'] ) ) {
            return array();
        }

        $emails = $this->parse_notification_email_list( $settings['notification_cc_emails'] );
        if ( empty( $emails ) ) {
            return array();
        }

        if ( empty( $primary_email ) ) {
            return array_slice( $emails, 0, $max_cc_recipients );
        }

        $primary_normalized = strtolower( sanitize_email( $primary_email ) );
        $filtered_emails = array_values(
            array_filter(
                $emails,
                function ( $email ) use ( $primary_normalized ) {
                    return strtolower( $email ) !== $primary_normalized;
                }
            )
        );
        return array_slice( $filtered_emails, 0, $max_cc_recipients );
    }

    /**
     * Parse comma/semicolon/newline-separated email list.
     *
     * @since 0.14.269
     * @param string $raw_emails Raw email list string.
     * @return array
     */
    private function parse_notification_email_list( $raw_emails ) {
        if ( ! is_string( $raw_emails ) || '' === trim( $raw_emails ) ) {
            return array();
        }

        $parts = preg_split( '/[,;\r\n]+/', $raw_emails );
        if ( false === $parts || empty( $parts ) ) {
            return array();
        }

        $emails = array();
        $seen   = array();

        foreach ( $parts as $part ) {
            $candidate = trim( (string) $part );
            if ( '' === $candidate ) {
                continue;
            }

            $email = sanitize_email( $candidate );
            if ( empty( $email ) || ! is_email( $email ) ) {
                continue;
            }

            $key = strtolower( $email );
            if ( isset( $seen[ $key ] ) ) {
                continue;
            }

            $seen[ $key ] = true;
            $emails[] = $email;
        }

        return $emails;
    }

    /**
     * Normalize CC recipient list to unique valid emails and exclude primary recipient.
     *
     * @since 0.14.269
     * @param array  $cc_recipients Raw CC recipients.
     * @param string $primary_email Primary recipient email.
     * @return array
     */
    private function normalize_cc_recipients( $cc_recipients, $primary_email = '' ) {
        if ( ! is_array( $cc_recipients ) || empty( $cc_recipients ) ) {
            return array();
        }

        $normalized = array();
        $seen = array();
        $primary_normalized = strtolower( sanitize_email( $primary_email ) );

        foreach ( $cc_recipients as $cc_recipient ) {
            $email = sanitize_email( (string) $cc_recipient );
            if ( empty( $email ) || ! is_email( $email ) ) {
                continue;
            }

            $email_key = strtolower( $email );
            if ( '' !== $primary_normalized && $email_key === $primary_normalized ) {
                continue;
            }
            if ( isset( $seen[ $email_key ] ) ) {
                continue;
            }

            $seen[ $email_key ] = true;
            $normalized[] = $email;
        }

        return $normalized;
    }

    /**
     * Resolve the Blog agent's own recipients into [primary $to, $cc[]].
     *
     * Reads the dedicated `sitestaffr_blog_notifications` option — fully separate
     * from conversation notifications, with NO fallback. If no blog email is
     * configured, $to is empty and no blog email is sent anywhere.
     *
     * @since 1.28.0
     * @return array { 0: string $to, 1: array $cc }
     */
    private function resolve_blog_recipients() {
        $blog = get_option( 'sitestaffr_blog_notifications', array() );
        $to   = ! empty( $blog['email'] ) ? sanitize_email( $blog['email'] ) : '';
        if ( empty( $to ) || ! is_email( $to ) ) {
            return array( '', array() );
        }

        $cc = array();
        if ( ! empty( $blog['cc'] ) ) {
            foreach ( $this->parse_notification_email_list( $blog['cc'] ) as $cc_email ) {
                if ( strtolower( $cc_email ) !== strtolower( $to ) ) {
                    $cc[] = $cc_email;
                }
            }
            $cc = array_slice( $cc, 0, 5 );
        }
        return array( $to, $cc );
    }

    /**
     * Whether blog email notifications are enabled (dedicated option).
     *
     * @since 1.28.0
     * @return bool
     */
    private function blog_notifications_enabled() {
        $blog = get_option( 'sitestaffr_blog_notifications', array() );
        return ! isset( $blog['enabled'] ) || $blog['enabled'];
    }

    /**
     * Split an explicit recipient list into [primary $to, $cc[]].
     *
     * Used by the manual "Email Conversation" / "Email Review Link" actions: the
     * first valid address becomes the primary recipient, the rest (deduped, capped
     * at 5) become CC.
     *
     * @since 1.28.0
     * @param array $recipients Raw recipient emails.
     * @return array { 0: string $to, 1: array $cc }
     */
    private function split_recipients( $recipients ) {
        $valid = array();
        $seen  = array();
        foreach ( (array) $recipients as $candidate ) {
            $email = sanitize_email( (string) $candidate );
            if ( empty( $email ) || ! is_email( $email ) ) {
                continue;
            }
            $key = strtolower( $email );
            if ( isset( $seen[ $key ] ) ) {
                continue;
            }
            $seen[ $key ] = true;
            $valid[]      = $email;
        }

        if ( empty( $valid ) ) {
            return array( '', array() );
        }

        $to = array_shift( $valid );
        return array( $to, array_slice( $valid, 0, 5 ) );
    }

    /**
     * Get call data from database.
     *
     * @since    1.0.0
     * @param    int    $call_id    The call ID to retrieve.
     * @return   object|null        Call object or null if not found.
     */
    private function get_call_data( $call_id ) {
        global $wpdb;

        $table = $wpdb->prefix . 'phoneease_calls';

        return $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM $table WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- trusted plugin table name.
                $call_id
            )
        );
    }

    /**
     * Log email sending result.
     *
     * @since    1.0.0
     * @param    int       $call_id    Call ID the notification was for.
     * @param    string    $to         Recipient email address.
     * @param    bool      $sent       Whether email was sent successfully.
     */
    private function log_email_result( $call_id, $to, $sent ) {
        $status = $sent ? 'SUCCESS' : 'FAILED';
        SiteStaffr_Logger::legacy( sprintf( 'SiteStaffr Email: %s - Call #%d notification to %s', $status, $call_id, $to ) );
    }

    /**
     * Format phone number for display.
     *
     * @since    1.0.0
     * @param    string    $number    Phone number to format.
     * @return   string               Formatted phone number.
     */
    public function format_phone_number( $number ) {
        if ( class_exists( 'SiteStaffr_Call_Manager' ) && method_exists( 'SiteStaffr_Call_Manager', 'format_phone_number' ) ) {
            return SiteStaffr_Call_Manager::format_phone_number( $number );
        }

        $clean = preg_replace( '/[^0-9]/', '', $number );

        if ( strlen( $clean ) === 11 && substr( $clean, 0, 1 ) === '1' ) {
            $area_code = substr( $clean, 1, 3 );
            $prefix = substr( $clean, 4, 3 );
            $line = substr( $clean, 7, 4 );
            return "($area_code) $prefix-$line";
        }

        if ( strlen( $clean ) === 10 ) {
            $area_code = substr( $clean, 0, 3 );
            $prefix = substr( $clean, 3, 3 );
            $line = substr( $clean, 6, 4 );
            return "($area_code) $prefix-$line";
        }

        return $number;
    }

    /**
     * Format call duration for display.
     *
     * @since    1.0.0
     * @param    int    $seconds    Duration in seconds.
     * @return   string             Formatted duration string.
     */
    public function format_duration( $seconds ) {
        if ( empty( $seconds ) || $seconds <= 0 ) {
            return __( '0 seconds', 'sitestaffr' );
        }

        $minutes = floor( $seconds / 60 );
        $remaining_seconds = $seconds % 60;

        if ( $minutes > 0 ) {
            return sprintf(
                /* translators: 1: Number of minutes, 2: Number of seconds */
                __( '%1$d min %2$d sec', 'sitestaffr' ),
                $minutes,
                $remaining_seconds
            );
        }

        return sprintf(
            /* translators: %d: Number of seconds */
            __( '%d seconds', 'sitestaffr' ),
            $seconds
        );
    }

    /**
     * Format datetime for display.
     *
     * @since    1.0.0
     * @param    string    $datetime    MySQL datetime string.
     * @return   string                 Formatted datetime string.
     */
    public function format_datetime( $datetime ) {
        if ( empty( $datetime ) ) {
            return __( 'Unknown', 'sitestaffr' );
        }

        $timestamp = strtotime( $datetime );
        $date_format = get_option( 'date_format', 'F j, Y' );
        $time_format = get_option( 'time_format', 'g:i a' );

        return date_i18n( $date_format . ' ' . $time_format, $timestamp );
    }

    /**
     * Get dashboard URL for a specific call.
     *
     * @since    1.0.0
     * @param    int    $call_id    The call ID.
     * @return   string             URL to view call in dashboard.
     */
    public function get_call_url( $call_id ) {
        return SiteStaffr_Magic_Link::generate_transcript_url( $call_id );
    }

    /**
     * Format started_at timestamp in Eastern Time as ISO 8601.
     *
     * @since  1.2.0
     * @param  mixed $started_at Unix timestamp (seconds or milliseconds) or empty.
     * @return string             ISO 8601 string in Eastern Time, or empty string.
     */
    private function format_started_at_eastern( $started_at ) {
        if ( empty( $started_at ) ) {
            return '';
        }

        $ts = (int) $started_at;
        if ( $ts > 9999999999 ) {
            $ts = (int) floor( $ts / 1000 );
        }

        if ( $ts <= 0 ) {
            return '';
        }

        try {
            $dt = new DateTime( '@' . $ts );
            $dt->setTimezone( new DateTimeZone( 'America/New_York' ) );
            return $dt->format( 'c' );
        } catch ( Exception $e ) {
            return '';
        }
    }

    /**
     * Get human-readable status label.
     *
     * @since    1.0.0
     * @param    string    $status    Call status from database.
     * @return   string               Human-readable status label.
     */
    private function get_status_label( $status ) {
        $labels = array(
            'completed'  => __( 'Completed', 'sitestaffr' ),
            'no-answer'  => __( 'Missed', 'sitestaffr' ),
            'busy'       => __( 'Busy', 'sitestaffr' ),
            'failed'     => __( 'Failed', 'sitestaffr' ),
            'canceled'   => __( 'Canceled', 'sitestaffr' ),
            'in-progress' => __( 'In Progress', 'sitestaffr' ),
            'ringing'    => __( 'Ringing', 'sitestaffr' ),
            'queued'     => __( 'Queued', 'sitestaffr' ),
        );

        return isset( $labels[ $status ] ) ? $labels[ $status ] : __( 'New', 'sitestaffr' );
    }
}
// phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
