<?php

/**
 * SocialPromoter Queue
 *
 * Manages the scheduled post queue: inserting, retrieving, updating,
 * and cancelling queue items. Also owns the WP Cron callback.
 *
 * @package PublisherKits_SocialPromoter
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
/**
 * Class PKITS_SP_Queue
 */
class PKITS_SP_Queue {
    /**
     * WP Cron hook name for firing individual scheduled posts.
     *
     * @var string
     */
    const CRON_HOOK = 'pkits_sp_fire_post';

    /**
     * Insert a new item into the queue.
     *
     * @param array $data {
     *     @type int    $post_id       WordPress post ID.
     *     @type string $platform      Platform slug (x, facebook, linkedin).
     *     @type string $account_id    Storage key of the connected account.
     *     @type string $copy_text     Social copy to send.
     *     @type string $image_url     Optional image URL.
     *     @type string $scheduled_at  MySQL datetime string (UTC).
     * }
     * @return int|WP_Error Inserted queue item ID, or WP_Error.
     */
    public function insert( array $data ) {
        global $wpdb;
        $insert = array(
            'post_id'      => absint( $data['post_id'] ),
            'campaign_id'  => ( isset( $data['campaign_id'] ) ? absint( $data['campaign_id'] ) : null ),
            'platform'     => sanitize_key( $data['platform'] ),
            'account_id'   => sanitize_text_field( $data['account_id'] ),
            'copy_text'    => wp_kses_post( $data['copy_text'] ),
            'image_url'    => ( isset( $data['image_url'] ) ? esc_url_raw( $data['image_url'] ) : null ),
            'scheduled_at' => sanitize_text_field( $data['scheduled_at'] ),
            'status'       => 'pending',
            'created_at'   => current_time( 'mysql', 1 ),
        );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
        $result = $wpdb->insert( $wpdb->prefix . 'pkits_sp_queue', $insert, array(
            '%d',
            '%d',
            '%s',
            '%s',
            '%s',
            '%s',
            '%s',
            '%s',
            '%s'
        ) );
        if ( false === $result ) {
            return new WP_Error('queue_insert_failed', 'Failed to insert queue item: ' . $wpdb->last_error);
        }
        $queue_id = (int) $wpdb->insert_id;
        // Schedule the WP Cron event to fire this post. A past/now slot must still
        // be scheduled (fired shortly) rather than skipped, or the row would sit
        // 'pending' forever. Immediate-send callers run execute() synchronously;
        // this fallback cron then becomes a harmless no-op via the atomic claim.
        $timestamp = strtotime( $data['scheduled_at'] );
        if ( !$timestamp || $timestamp <= time() ) {
            $timestamp = time() + 60;
        }
        wp_schedule_single_event( $timestamp, self::CRON_HOOK, array($queue_id) );
        return $queue_id;
    }

    /**
     * Update a queue item.
     *
     * @param int   $id   Queue item ID.
     * @param array $data Fields to update.
     * @return bool
     */
    public function update( $id, array $data ) {
        global $wpdb;
        $allowed = array(
            'status',
            'platform_post_id',
            'error_message',
            'sent_at'
        );
        $update = array();
        $formats = array();
        foreach ( $allowed as $field ) {
            if ( array_key_exists( $field, $data ) ) {
                $update[$field] = sanitize_text_field( $data[$field] );
                $formats[] = '%s';
            }
        }
        if ( empty( $update ) ) {
            return false;
        }
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $result = $wpdb->update(
            $wpdb->prefix . 'pkits_sp_queue',
            $update,
            array(
                'id' => absint( $id ),
            ),
            $formats,
            array('%d')
        );
        return false !== $result;
    }

    /**
     * Cancel a pending queue item.
     *
     * Clears the scheduled WP Cron event and marks the record as cancelled.
     *
     * @param int $id Queue item ID.
     * @return bool
     */
    public function cancel( $id ) {
        $item = $this->get( $id );
        if ( !$item || 'pending' !== $item->status ) {
            return false;
        }
        // Clear the WP Cron event.
        $timestamp = strtotime( $item->scheduled_at );
        if ( $timestamp ) {
            wp_clear_scheduled_hook( self::CRON_HOOK, array((int) $id) );
        }
        return $this->update( $id, array(
            'status' => 'cancelled',
        ) );
    }

    /**
     * Cancel every pending send for a post. Used when a manual Post Now or
     * Schedule takes over a story, so an auto-scheduled send cannot fire as a
     * duplicate alongside the manual one.
     *
     * @param int $post_id Post ID.
     * @return int Number of sends cancelled.
     */
    public function cancel_pending_for_post( $post_id ) {
        $rows = $this->get_by_post( $post_id );
        $n = 0;
        foreach ( (array) $rows as $r ) {
            if ( 'pending' === $r->status && $this->cancel( (int) $r->id ) ) {
                $n++;
            }
        }
        return $n;
    }

    /**
     * Get a single queue item by ID.
     *
     * @param int $id Queue item ID.
     * @return object|null
     */
    public function get( $id ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pkits_sp_queue WHERE id = %d", absint( $id ) ) );
    }

    /**
     * Get all queue items for a specific post.
     *
     * @param int $post_id WordPress post ID.
     * @return array
     */
    public function get_by_post( $post_id ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pkits_sp_queue WHERE post_id = %d ORDER BY scheduled_at DESC", absint( $post_id ) ) );
    }

    /**
     * Get upcoming pending queue items for the queue admin page.
     *
     * @param int $limit  Max items to return.
     * @param int $offset Pagination offset.
     * @return array
     */
    public function get_upcoming( $limit = 50, $offset = 0 ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $wpdb->get_results( $wpdb->prepare( "SELECT q.*, p.post_title FROM {$wpdb->prefix}pkits_sp_queue q\n\t\t\t\tLEFT JOIN {$wpdb->posts} p ON q.post_id = p.ID\n\t\t\t\tWHERE q.status = 'pending'\n\t\t\t\tORDER BY q.scheduled_at ASC\n\t\t\t\tLIMIT %d OFFSET %d", absint( $limit ), absint( $offset ) ) );
    }

    /**
     * Get recent sent/failed items for the history page.
     *
     * @param int $limit  Max items to return.
     * @param int $offset Pagination offset.
     * @return array
     */
    /**
     * Count all sent/failed history rows (for pagination).
     *
     * @return int
     */
    public function count_history() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}pkits_sp_queue WHERE status IN ('sent', 'failed')" );
    }

    public function get_history( $limit = 50, $offset = 0 ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $wpdb->get_results( $wpdb->prepare( "SELECT q.*, p.post_title FROM {$wpdb->prefix}pkits_sp_queue q\n\t\t\t\tLEFT JOIN {$wpdb->posts} p ON q.post_id = p.ID\n\t\t\t\tWHERE q.status IN ('sent', 'failed')\n\t\t\t\tORDER BY q.sent_at DESC, q.scheduled_at DESC\n\t\t\t\tLIMIT %d OFFSET %d", absint( $limit ), absint( $offset ) ) );
    }

    /**
     * WP Cron callback: fire a single queued post.
     *
     * @param int $queue_id Queue item ID.
     * @return void
     */
    public function fire_post( $queue_id ) {
        $queue_id = absint( $queue_id );
        // Atomically claim the item so two concurrent cron runners (loopback
        // spawn plus a traffic-triggered wp-cron.php hit, or a system cron
        // alongside WP-Cron) cannot both send it. Only the worker that flips
        // pending -> sending proceeds; the loser bails.
        if ( !$this->claim_for_sending( $queue_id ) ) {
            return;
        }
        $item = $this->get( $queue_id );
        if ( !$item ) {
            return;
        }
        $poster = new PKITS_SP_Poster();
        $poster->execute( $item );
    }

    /**
     * Atomically transition a pending item to 'sending'.
     *
     * Compare-and-swap: the UPDATE only matches while the row is still
     * 'pending', so exactly one caller can win the claim. Returns true only for
     * that caller; a row already sending/sent/cancelled affects 0 rows.
     *
     * @param int $id Queue item ID.
     * @return bool True if this call claimed the item.
     */
    public function claim_for_sending( $id ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $affected = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}pkits_sp_queue SET status = 'sending' WHERE id = %d AND status = 'pending'", absint( $id ) ) );
        return 1 === (int) $affected;
    }

    /**
     * Register the WP Cron hook.
     *
     * @return void
     */
    public function register_hooks() {
        add_action( self::CRON_HOOK, array($this, 'fire_post') );
    }

    /**
     * Delete a single queue item permanently.
     *
     * @param int $id Queue item ID.
     * @return bool
     */
    public function delete( $id ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $result = $wpdb->delete( $wpdb->prefix . 'pkits_sp_queue', array(
            'id' => absint( $id ),
        ), array('%d') );
        return false !== $result;
    }

    /**
     * Delete all failed queue items for a specific post.
     *
     * @param int $post_id WordPress post ID.
     * @return int Number of rows deleted.
     */
    public function delete_failed_for_post( $post_id ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $result = $wpdb->delete( $wpdb->prefix . 'pkits_sp_queue', array(
            'post_id' => absint( $post_id ),
            'status'  => 'failed',
        ), array('%d', '%s') );
        return (int) $result;
    }

    /**
     * Delete all failed queue items across all posts.
     *
     * @return int Number of rows deleted.
     */
    public function delete_all_failed() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}pkits_sp_queue WHERE status = %s", 'failed' ) );
        return (int) $deleted;
    }

    /**
     * Delete all sent and failed history items (keep pending/scheduled).
     *
     * @return int Number of rows deleted.
     */
    public function delete_all_history() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $deleted = $wpdb->query( $wpdb->prepare(
            "DELETE FROM {$wpdb->prefix}pkits_sp_queue WHERE status IN (%s, %s, %s)",
            'sent',
            'failed',
            'cancelled'
        ) );
        return (int) $deleted;
    }

    /**
     * Count pending queue items for a specific post.
     *
     * @param int $post_id WordPress post ID.
     * @return int
     */
    public function count_pending_for_post( $post_id ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}pkits_sp_queue WHERE post_id = %d AND status = 'pending'", absint( $post_id ) ) );
    }

}
