<?php

/**
 * SocialPromoter Metabox
 *
 * Registers and renders the "Promote This Post" metabox on Posts and
 * supported post types (integrations add theirs by filter). Handles Post Now and
 * Schedule AJAX actions.
 *
 * @package PublisherKits_SocialPromoter
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
/**
 * Class PKITS_SP_Metabox
 */
class PKITS_SP_Metabox {
    /**
     * Register the metabox and AJAX hooks.
     *
     * @return void
     */
    /**
     * Register metabox on add_meta_boxes hook.
     *
     * @return void
     */
    public function register() {
        /**
         * Post types that get the SocialPromoter metabox. Integrations (e.g. the
         * Pro WP Job Manager module, shipped separately) append theirs here.
         *
         * @param string[] $post_types Post type slugs.
         */
        $post_types = apply_filters( 'pkits_sp_metabox_post_types', array('post') );
        foreach ( $post_types as $post_type ) {
            add_meta_box(
                'pkits-sp-promote',
                esc_html__( 'SocialPromoter', 'publisherkits-socialpromoter' ),
                array($this, 'render'),
                $post_type,
                'side',
                'default'
            );
        }
    }

    /**
     * Register AJAX handlers.
     *
     * Must be hooked to admin_init (or init) so handlers are available
     * on admin-ajax.php requests, which do not fire add_meta_boxes.
     *
     * @return void
     */
    public function register_ajax_handlers() {
        add_action( 'wp_ajax_pkits_sp_post_now', array($this, 'ajax_post_now') );
        add_action( 'wp_ajax_pkits_sp_schedule_post', array($this, 'ajax_schedule_post') );
        add_action( 'wp_ajax_pkits_sp_cancel_queue_item', array($this, 'ajax_cancel_queue_item') );
        add_action( 'wp_ajax_pkits_sp_clear_failed', array($this, 'ajax_clear_failed') );
        // History management (delete a row, clear all) is a free feature.
        add_action( 'wp_ajax_pkits_sp_delete_queue_item', array($this, 'ajax_delete_queue_item') );
        add_action( 'wp_ajax_pkits_sp_clear_all_history', array($this, 'ajax_clear_all_history') );
    }

    /**
     * Build the default social copy for a post from the configured template,
     * resolving {post_title}, {post_url} and {site_name} against the post as it
     * stands now. Shared by the metabox prefill and the send-time rebuild, so a
     * post sent with untouched default copy is never left with stale auto-draft
     * text.
     *
     * @param int $post_id Post ID.
     * @return string
     */
    public static function default_copy_for( $post_id ) {
        $settings = get_option( 'pkits_sp_settings', array() );
        $settings = ( is_array( $settings ) ? $settings : array() );
        $template = ( isset( $settings['copy_template'] ) && '' !== $settings['copy_template'] ? $settings['copy_template'] : '{post_title} {post_url}' );
        $post_url = (string) (( get_permalink( (int) $post_id ) ?: '' ));
        $post_title = html_entity_decode( (string) get_the_title( (int) $post_id ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
        $site_name = (string) get_bloginfo( 'name' );
        return str_replace( array('{post_title}', '{post_url}', '{site_name}'), array($post_title, $post_url, $site_name), (string) $template );
    }

    /**
     * Render the metabox HTML.
     *
     * @param WP_Post $post Current post object.
     * @return void
     */
    public function render( $post ) {
        $accounts = PKITS_SP_Accounts::get_active();
        $queue = new PKITS_SP_Queue();
        $items = $queue->get_by_post( $post->ID );
        // Resolve copy template from settings.
        $post_url = (string) (( get_permalink( $post->ID ) ?: '' ));
        $default_copy = self::default_copy_for( $post->ID );
        // Gather post tags for hashtag helper.
        $tags = array();
        if ( 'post' === $post->post_type ) {
            $post_tags = get_the_tags( $post->ID );
            if ( $post_tags && !is_wp_error( $post_tags ) ) {
                foreach ( $post_tags as $tag ) {
                    $tags[] = str_replace( array(' ', '-'), '', $tag->name );
                }
            }
        }
        wp_nonce_field( 'pkits_sp_metabox', 'pkits_sp_nonce' );
        ?>
		<div id="pkits-sp-metabox" data-post-id="<?php 
        echo esc_attr( $post->ID );
        ?>" data-post-url="<?php 
        echo esc_url( $post_url );
        ?>">

			<?php 
        if ( !empty( $items ) ) {
            // Group queue rows into sends. A send fans one story to every
            // connected channel at once, so all its per-platform rows share
            // a scheduled time and campaign. The panel shows a single status
            // line for the most relevant send; full detail is on the History page.
            $groups = array();
            foreach ( $items as $it ) {
                $key = substr( (string) $it->scheduled_at, 0, 16 ) . '|' . (int) ($it->campaign_id ?? 0);
                if ( !isset( $groups[$key] ) ) {
                    $groups[$key] = array(
                        'ts'   => strtotime( $it->scheduled_at . ' UTC' ),
                        'rows' => array(),
                    );
                }
                $groups[$key]['rows'][] = $it;
            }
            $queued = array();
            $history = array();
            foreach ( $groups as $g ) {
                $has_pending = false;
                foreach ( $g['rows'] as $r ) {
                    if ( 'pending' === $r->status ) {
                        $has_pending = true;
                        break;
                    }
                }
                if ( $has_pending ) {
                    $queued[] = $g;
                    continue;
                }
                // A send with no pending row is history - but only if it has
                // something to report. The status line below is built from the
                // sent, pending and failed rows in a send and ignores every
                // other state, so a send whose rows are all (say) cancelled
                // describes to nothing: it printed an empty line, and because
                // history is shown newest first it also hid the most recent
                // send that did have something to say.
                foreach ( $g['rows'] as $r ) {
                    if ( 'sent' === $r->status || 'failed' === $r->status ) {
                        $history[] = $g;
                        break;
                    }
                }
            }
            usort( $queued, function ( $a, $b ) {
                return $a['ts'] <=> $b['ts'];
            } );
            usort( $history, function ( $a, $b ) {
                return $b['ts'] <=> $a['ts'];
            } );
            $failed_count = count( array_filter( $items, function ( $i ) {
                return 'failed' === $i->status;
            } ) );
            $now_ts = time();
            $names = array(
                'x'        => 'X',
                'linkedin' => 'LinkedIn',
                'facebook' => 'Facebook',
                'bluesky'  => 'Bluesky',
            );
            $order = array(
                'x',
                'linkedin',
                'facebook',
                'bluesky'
            );
            // Channel-name lists for a send, grouped by status in a stable order.
            $describe = function ( $g ) use($names, $order) {
                $by = array(
                    'sent'    => array(),
                    'pending' => array(),
                    'failed'  => array(),
                );
                foreach ( $g['rows'] as $r ) {
                    if ( !isset( $by[$r->status] ) ) {
                        continue;
                    }
                    $by[$r->status][$r->platform] = $names[$r->platform] ?? ucfirst( (string) $r->platform );
                }
                $sortf = function ( $arr ) use($order) {
                    $out = array();
                    foreach ( $order as $p ) {
                        if ( isset( $arr[$p] ) ) {
                            $out[] = $arr[$p];
                        }
                    }
                    foreach ( $arr as $p => $n ) {
                        if ( !in_array( $p, $order, true ) ) {
                            $out[] = $n;
                        }
                    }
                    return $out;
                };
                return array(
                    'sent'    => $sortf( $by['sent'] ),
                    'pending' => $sortf( $by['pending'] ),
                    'failed'  => $sortf( $by['failed'] ),
                );
            };
            // Queued is sorted earliest-first, history latest-first. Show the
            // next queued send if there is one, otherwise the last posted send.
            $primary = ( !empty( $queued ) ? $queued[0] : (( !empty( $history ) ? $history[0] : null )) );
            ?>
				<div class="pkits-sp-history pkits-sp-history-top">
					<div class="pkits-sp-history-header">
						<strong><?php 
            esc_html_e( 'Social', 'publisherkits-socialpromoter' );
            ?></strong>
						<div class="pkits-sp-history-actions">
							<?php 
            if ( $failed_count > 0 ) {
                ?>
								<button type="button" class="pkits-sp-clear-failed-btn" data-post-id="<?php 
                echo esc_attr( $post->ID );
                ?>">
									<?php 
                /* translators: %d: number of failed posts */
                printf( esc_html__( 'Clear %d failed', 'publisherkits-socialpromoter' ), (int) $failed_count );
                ?>
								</button>
							<?php 
            }
            ?>
							<a href="<?php 
            echo esc_url( admin_url( 'admin.php?page=publisherkits-socialpromoter-history' ) );
            ?>" class="pkits-sp-view-all-link"><?php 
            esc_html_e( 'Full history', 'publisherkits-socialpromoter' );
            ?></a>
						</div>
					</div>

					<?php 
            if ( null === $primary ) {
                ?>
						<p class="pkits-sp-status-line pkits-sp-muted"><?php 
                esc_html_e( 'Not yet posted to social.', 'publisherkits-socialpromoter' );
                ?></p>
					<?php 
            } else {
                $d = $describe( $primary );
                $pending_ids = array();
                foreach ( $primary['rows'] as $r ) {
                    if ( 'pending' === $r->status ) {
                        $pending_ids[] = (int) $r->id;
                    }
                }
                if ( !empty( $pending_ids ) ) {
                    $chans = implode( ', ', $d['pending'] );
                    // Statement form, not a ternary: the free-build code printer drops
                    // comments inside expressions, which loses the translators comment.
                    if ( $primary['ts'] > $now_ts ) {
                        /* translators: %s: human-readable time difference */
                        $rel = sprintf( __( 'due in %s', 'publisherkits-socialpromoter' ), human_time_diff( $now_ts, $primary['ts'] ) );
                    } else {
                        $rel = __( 'due now', 'publisherkits-socialpromoter' );
                    }
                    ?>
							<p class="pkits-sp-status-line">
								<?php 
                    /* translators: 1: channel list, 2: relative time */
                    printf( esc_html__( 'Queued for %1$s, %2$s.', 'publisherkits-socialpromoter' ), esc_html( $chans ), esc_html( $rel ) );
                    ?>
								<button type="button" class="pkits-sp-send-cancel" data-queue-ids="<?php 
                    echo esc_attr( implode( ',', $pending_ids ) );
                    ?>"><?php 
                    esc_html_e( 'Cancel', 'publisherkits-socialpromoter' );
                    ?></button>
							</p>
						<?php 
                } else {
                    /* translators: %s: human-readable time difference */
                    $rel = sprintf( __( '%s ago', 'publisherkits-socialpromoter' ), human_time_diff( $primary['ts'], $now_ts ) );
                    $sent = implode( ', ', $d['sent'] );
                    $fail = implode( ', ', $d['failed'] );
                    ?>
							<p class="pkits-sp-status-line">
								<?php 
                    if ( '' !== $sent ) {
                        /* translators: 1: channel list, 2: relative time */
                        printf( esc_html__( 'Posted to %1$s, %2$s.', 'publisherkits-socialpromoter' ), esc_html( $sent ), esc_html( $rel ) );
                    }
                    if ( '' !== $fail ) {
                        echo ( '' !== $sent ? ' ' : '' );
                        /* translators: %s: channel list */
                        printf( esc_html__( 'Failed on %s.', 'publisherkits-socialpromoter' ), esc_html( $fail ) );
                    }
                    ?>
							</p>
						<?php 
                }
                ?>
					<?php 
            }
            ?>
				</div>
			<?php 
        }
        ?>

			<?php 
        if ( empty( $accounts ) ) {
            ?>
				<p class="pkits-sp-notice">
					<?php 
            printf( 
                /* translators: %s: URL to accounts settings page */
                esc_html__( 'No social accounts connected. %s', 'publisherkits-socialpromoter' ),
                '<a href="' . esc_url( admin_url( 'admin.php?page=publisherkits-socialpromoter-accounts' ) ) . '">' . esc_html__( 'Connect an account', 'publisherkits-socialpromoter' ) . '</a>'
             );
            ?>
				</p>
			<?php 
        } else {
            ?>

				<div class="pkits-sp-field">
					<label for="pkits-sp-copy"><strong><?php 
            esc_html_e( 'Social copy', 'publisherkits-socialpromoter' );
            ?></strong></label>
					<textarea
						id="pkits-sp-copy"
						name="pkits_sp_copy"
						rows="4"
						maxlength="3000"
						placeholder="<?php 
            esc_attr_e( 'Write your social post copy here...', 'publisherkits-socialpromoter' );
            ?>"
					><?php 
            echo esc_textarea( $default_copy );
            ?></textarea>
					<input type="hidden" id="pkits-sp-copy-default" value="<?php 
            echo esc_attr( $default_copy );
            ?>" />
					<div class="pkits-sp-char-row">
						<span class="pkits-sp-char-counter"><?php 
            echo esc_html( strlen( $default_copy ) );
            ?></span>
						<span class="pkits-sp-char-hint"><?php 
            esc_html_e( '(X limit: 280)', 'publisherkits-socialpromoter' );
            ?></span>
					</div>
				</div>

				<?php 
            if ( !empty( $tags ) ) {
                ?>
				<div class="pkits-sp-field pkits-sp-tags">
					<span class="pkits-sp-label"><?php 
                esc_html_e( 'Tags as hashtags:', 'publisherkits-socialpromoter' );
                ?></span>
					<?php 
                foreach ( $tags as $tag ) {
                    ?>
						<button type="button" class="pkits-sp-tag-pill" data-tag="<?php 
                    echo esc_attr( $tag );
                    ?>">#<?php 
                    echo esc_html( $tag );
                    ?></button>
					<?php 
                }
                ?>
				</div>
				<?php 
            }
            ?>

				<div class="pkits-sp-field">
					<strong><?php 
            esc_html_e( 'Post to:', 'publisherkits-socialpromoter' );
            ?></strong>
					<?php 
            foreach ( $accounts as $storage_id => $account ) {
                ?>
						<?php 
                $platform_labels = array(
                    'x'        => 'X',
                    'facebook' => 'FB',
                    'linkedin' => 'in',
                );
                $platform_label = ( isset( $platform_labels[$account['platform']] ) ? $platform_labels[$account['platform']] : strtoupper( $account['platform'] ) );
                ?>
						<label class="pkits-sp-account-check">
							<input
								type="checkbox"
								name="pkits_sp_accounts[]"
								class="pkits-sp-account-checkbox"
								value="<?php 
                echo esc_attr( $storage_id );
                ?>"
								data-platform="<?php 
                echo esc_attr( $account['platform'] );
                ?>"
							/>
							<span class="pkits-sp-platform-badge pkits-sp-badge-<?php 
                echo esc_attr( $account['platform'] );
                ?>"><?php 
                echo esc_html( $platform_label );
                ?></span>
							<span class="pkits-sp-account-label"><?php 
                echo esc_html( $account['label'] );
                ?></span>
						</label>
					<?php 
            }
            ?>
				</div>

				<?php 
            $featured_image_url = PKITS_SP_Image_Handler::get_featured_image_url( $post->ID );
            $thumb_id = get_post_thumbnail_id( $post->ID );
            $thumb_src = ( $thumb_id ? wp_get_attachment_image_url( $thumb_id, 'thumbnail' ) : '' );
            ?>
			<?php 
            if ( $featured_image_url ) {
                ?>
			<div class="pkits-sp-field pkits-sp-image-field">
				<label class="pkits-sp-account-check">
					<input
						type="checkbox"
						id="pkits-sp-include-image"
						name="pkits_sp_include_image"
						value="<?php 
                echo esc_url( $featured_image_url );
                ?>"
						checked
					/>
					<?php 
                if ( $thumb_src ) {
                    ?>
						<img src="<?php 
                    echo esc_url( $thumb_src );
                    ?>" alt="" class="pkits-sp-thumb-preview" />
					<?php 
                }
                ?>
					<span class="pkits-sp-account-label"><?php 
                esc_html_e( 'Include featured image', 'publisherkits-socialpromoter' );
                ?></span>
				</label>
			</div>
			<?php 
            }
            ?>

			<?php 
            ?>

			<div class="pkits-sp-actions">
					<button type="button" id="pkits-sp-post-now" class="button button-primary pkits-sp-full-width">
						<?php 
            esc_html_e( 'Post Now', 'publisherkits-socialpromoter' );
            ?>
					</button>

					<div class="pkits-sp-schedule-row">
						<input
							type="datetime-local"
							id="pkits-sp-schedule-time"
							class="pkits-sp-datetime"
						/>
						<button type="button" id="pkits-sp-schedule" class="button">
							<?php 
            esc_html_e( 'Schedule', 'publisherkits-socialpromoter' );
            ?>
						</button>
					</div>
				</div>

				<div id="pkits-sp-result" class="pkits-sp-result" style="display:none;"></div>

			<?php 
        }
        ?>

			<?php 
        ?>

		</div>
		<?php 
    }

    /**
     * AJAX handler: post immediately to selected accounts.
     *
     * @return void
     */
    public function ajax_post_now() {
        check_ajax_referer( 'pkits_sp_metabox', 'nonce' );
        // Posting speaks as the publisher's official social accounts, so require
        // publish capability (not merely edit_posts, which includes Contributors);
        // the per-post edit_post check is applied once $post_id is known below.
        if ( !current_user_can( 'publish_posts' ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Permission denied.', 'publisherkits-socialpromoter' ),
            ) );
        }
        $post_id = absint( wp_unslash( $_POST['post_id'] ?? 0 ) );
        $copy = sanitize_textarea_field( wp_unslash( $_POST['copy'] ?? '' ) );
        $accounts = ( isset( $_POST['accounts'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['accounts'] ) ) : array() );
        $image_url = ( isset( $_POST['image_url'] ) ? esc_url_raw( wp_unslash( $_POST['image_url'] ) ) : '' );
        if ( !$post_id || empty( $copy ) || empty( $accounts ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Missing required fields.', 'publisherkits-socialpromoter' ),
            ) );
        }
        if ( !get_post( $post_id ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Post not found.', 'publisherkits-socialpromoter' ),
            ) );
        }
        if ( !current_user_can( 'edit_post', $post_id ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Permission denied.', 'publisherkits-socialpromoter' ),
            ) );
        }
        if ( 'publish' !== get_post_status( $post_id ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Publish the post before posting it to social. Social posts link to the live article and take their branding from it.', 'publisherkits-socialpromoter' ),
            ) );
        }
        // If the editor sent the untouched default copy, it may have been
        // pre-filled while the post was still an auto-draft. Rebuild it from the
        // now-published post so the title and link are correct.
        if ( !empty( $_POST['copy_is_default'] ) ) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
            $copy = self::default_copy_for( $post_id );
        }
        $results = array();
        $poster = new PKITS_SP_Poster();
        $queue = new PKITS_SP_Queue();
        // Posting now takes over the story. Cancel any pending send already
        // queued for this post (for example an auto-scheduled one created when
        // the post was published) so it cannot fire as a duplicate.
        $queue->cancel_pending_for_post( $post_id );
        // One timestamp for the whole batch so the channels of a single Post Now
        // group together in history rather than drifting a second apart.
        $batch_ts = current_time( 'mysql', 1 );
        foreach ( $accounts as $account_storage_id ) {
            $account = PKITS_SP_Accounts::get( $account_storage_id );
            if ( empty( $account ) ) {
                $results[] = array(
                    'account' => esc_html( $account_storage_id ),
                    'success' => false,
                    'message' => esc_html__( 'Account not found.', 'publisherkits-socialpromoter' ),
                );
                continue;
            }
            // Insert as a sent item directly.
            $queue_id = $queue->insert( array(
                'post_id'      => $post_id,
                'platform'     => $account['platform'],
                'account_id'   => $account_storage_id,
                'copy_text'    => $copy,
                'image_url'    => $image_url,
                'scheduled_at' => $batch_ts,
            ) );
            if ( is_wp_error( $queue_id ) ) {
                $results[] = array(
                    'account' => esc_html( $account['label'] ),
                    'success' => false,
                    'message' => esc_html( $queue_id->get_error_message() ),
                );
                continue;
            }
            $item = $queue->get( $queue_id );
            $success = $poster->execute( $item );
            $results[] = array(
                'account' => esc_html( $account['label'] ),
                'success' => $success,
                'message' => ( $success ? esc_html__( 'Posted successfully.', 'publisherkits-socialpromoter' ) : esc_html__( 'Post failed. Check queue for details.', 'publisherkits-socialpromoter' ) ),
            );
        }
        wp_send_json_success( array(
            'results' => $results,
        ) );
    }

    /**
     * AJAX handler: schedule a post for a future date/time.
     *
     * @return void
     */
    public function ajax_schedule_post() {
        check_ajax_referer( 'pkits_sp_metabox', 'nonce' );
        // Scheduling publishes to the official social accounts; require publish
        // capability. Per-post edit_post is checked once $post_id is known below.
        if ( !current_user_can( 'publish_posts' ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Permission denied.', 'publisherkits-socialpromoter' ),
            ) );
        }
        $post_id = absint( wp_unslash( $_POST['post_id'] ?? 0 ) );
        $copy = sanitize_textarea_field( wp_unslash( $_POST['copy'] ?? '' ) );
        $accounts = ( isset( $_POST['accounts'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['accounts'] ) ) : array() );
        $scheduled_at = sanitize_text_field( wp_unslash( $_POST['scheduled_at'] ?? '' ) );
        $image_url = ( isset( $_POST['image_url'] ) ? esc_url_raw( wp_unslash( $_POST['image_url'] ) ) : '' );
        if ( !$post_id || empty( $copy ) || empty( $accounts ) || empty( $scheduled_at ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Missing required fields.', 'publisherkits-socialpromoter' ),
            ) );
        }
        if ( !get_post( $post_id ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Post not found.', 'publisherkits-socialpromoter' ),
            ) );
        }
        if ( !current_user_can( 'edit_post', $post_id ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Permission denied.', 'publisherkits-socialpromoter' ),
            ) );
        }
        $post_status = get_post_status( $post_id );
        if ( 'publish' !== $post_status && 'future' !== $post_status ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Publish or schedule the post before scheduling it to social.', 'publisherkits-socialpromoter' ),
            ) );
        }
        // Rebuild untouched default copy from the post, in case it was pre-filled
        // while the post was still an auto-draft.
        if ( !empty( $_POST['copy_is_default'] ) ) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
            $copy = self::default_copy_for( $post_id );
        }
        // The browser sends a datetime-local value: the user's SITE-LOCAL wall
        // time, with no timezone. WordPress runs PHP in UTC, so interpret it as
        // site-local and convert, otherwise the send is offset by the site's UTC
        // offset (e.g. an hour late during BST).
        $timestamp = strtotime( get_gmt_from_date( str_replace( 'T', ' ', $scheduled_at ) ) );
        if ( !$timestamp || $timestamp <= time() ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Schedule time must be in the future.', 'publisherkits-socialpromoter' ),
            ) );
        }
        $scheduled_utc = gmdate( 'Y-m-d H:i:s', $timestamp );
        $queue = new PKITS_SP_Queue();
        $scheduled = array();
        // A manual schedule takes over the story. Cancel any pending send already
        // queued for this post so the manual one does not run as a duplicate.
        $queue->cancel_pending_for_post( $post_id );
        foreach ( $accounts as $account_storage_id ) {
            $account = PKITS_SP_Accounts::get( $account_storage_id );
            if ( empty( $account ) ) {
                continue;
            }
            $queue_id = $queue->insert( array(
                'post_id'      => $post_id,
                'platform'     => $account['platform'],
                'account_id'   => $account_storage_id,
                'copy_text'    => $copy,
                'image_url'    => $image_url,
                'scheduled_at' => $scheduled_utc,
            ) );
            if ( !is_wp_error( $queue_id ) ) {
                $scheduled[] = esc_html( $account['label'] );
            }
        }
        if ( empty( $scheduled ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Could not schedule any posts.', 'publisherkits-socialpromoter' ),
            ) );
        }
        wp_send_json_success( array(
            'message' => sprintf( 
                /* translators: %s: comma-separated list of account labels */
                esc_html__( 'Scheduled for %1$s to: %2$s', 'publisherkits-socialpromoter' ),
                esc_html( gmdate( 'd M Y H:i', $timestamp ) . ' UTC' ),
                esc_html( implode( ', ', $scheduled ) )
             ),
        ) );
    }

    /**
     * AJAX handler: cancel a pending queue item.
     *
     * @return void
     */
    public function ajax_cancel_queue_item() {
        check_ajax_referer( 'pkits_sp_metabox', 'nonce' );
        // Queue rows are not post-scoped (any queue_id is accepted), so gate
        // destructive queue actions on manage_options rather than edit_posts,
        // which would let a Contributor cancel other users' scheduled sends.
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Permission denied.', 'publisherkits-socialpromoter' ),
            ) );
        }
        $queue_id = absint( wp_unslash( $_POST['queue_id'] ?? 0 ) );
        if ( !$queue_id ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Invalid queue ID.', 'publisherkits-socialpromoter' ),
            ) );
        }
        $queue = new PKITS_SP_Queue();
        $result = $queue->cancel( $queue_id );
        if ( $result ) {
            wp_send_json_success( array(
                'message' => esc_html__( 'Scheduled post cancelled.', 'publisherkits-socialpromoter' ),
            ) );
        } else {
            wp_send_json_error( array(
                'message' => esc_html__( 'Could not cancel this item.', 'publisherkits-socialpromoter' ),
            ) );
        }
    }

    /**
     * AJAX: delete a single queue item.
     *
     * @return void
     */
    public function ajax_delete_queue_item() {
        check_ajax_referer( 'pkits_sp_metabox', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Permission denied.', 'publisherkits-socialpromoter' ),
            ) );
        }
        $queue_id = absint( ( isset( $_POST['queue_id'] ) ? wp_unslash( $_POST['queue_id'] ) : 0 ) );
        if ( !$queue_id ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Invalid queue ID.', 'publisherkits-socialpromoter' ),
            ) );
        }
        $queue = new PKITS_SP_Queue();
        $result = $queue->delete( $queue_id );
        if ( $result ) {
            wp_send_json_success();
        } else {
            wp_send_json_error( array(
                'message' => esc_html__( 'Could not delete item.', 'publisherkits-socialpromoter' ),
            ) );
        }
    }

    /**
     * AJAX: clear all failed queue items for a specific post.
     *
     * @return void
     */
    public function ajax_clear_failed() {
        check_ajax_referer( 'pkits_sp_metabox', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Permission denied.', 'publisherkits-socialpromoter' ),
            ) );
        }
        // post_id = 0 is a deliberate sentinel meaning "delete ALL failed
        // across all posts" (the queue screen's Delete-all button sends 0), so
        // it must not be rejected as invalid.
        $post_id = absint( ( isset( $_POST['post_id'] ) ? wp_unslash( $_POST['post_id'] ) : 0 ) );
        $queue = new PKITS_SP_Queue();
        $deleted = ( 0 === $post_id ? $queue->delete_all_failed() : $queue->delete_failed_for_post( $post_id ) );
        wp_send_json_success( array(
            'deleted' => $deleted,
        ) );
    }

    /**
     * AJAX: clear all history (sent, failed, cancelled) across all posts.
     *
     * @return void
     */
    public function ajax_clear_all_history() {
        check_ajax_referer( 'pkits_sp_metabox', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Permission denied.', 'publisherkits-socialpromoter' ),
            ) );
        }
        $queue = new PKITS_SP_Queue();
        $deleted = $queue->delete_all_history();
        wp_send_json_success( array(
            'deleted' => $deleted,
        ) );
    }

}
