<?php
/**
 * This file is part of the Wisewand WordPress plugin.
 *
 * @package Wisewand
 * @since 2.8.2
 * @license GPL-2.0-or-later
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
namespace Wisewand\Content;

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use WP_Error;
use Wisewand\Utils\Options;
use Wisewand\Logging\Logger;

// phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- error_log() used within WP_DEBUG conditionals; dynamic placeholder generation for IN clauses; table names derived from $wpdb->prefix are safe to interpolate; direct database calls required for custom plugin table operations

/**
 * Handles content pull operations and the retry queue for async content retrieval.
 *
 * Manages fetching article content from the Wisewand API, scheduling WP-Cron
 * retries when content is not yet ready, and maintaining queue state in the
 * custom wisewand_pull_queue database table.
 *
 * @package Wisewand
 * @since 2.8.2
 */
class Pull_Content_Handler {

	/**
	 * Plugin options wrapper.
	 *
	 * @since 2.8.2
	 * @var Options
	 */
	private $options;

	/**
	 * Logger instance.
	 *
	 * @since 2.8.2
	 * @var Logger
	 */
	private $logger;

	/**
	 * Fully-qualified name of the pull queue database table.
	 *
	 * @since 2.8.2
	 * @var string
	 */
	private $pull_queue_table;

	/**
	 * Reference to Content_Manager for delegating process_content() calls.
	 *
	 * Set via set_content_manager() after construction to avoid circular
	 * constructor dependencies.
	 *
	 * @since 2.8.2
	 * @var Content_Manager|null
	 */
	private $content_manager = null;

	/**
	 * Constructor.
	 *
	 * @since 2.8.2
	 *
	 * @param Options $options         Plugin options wrapper.
	 * @param Logger  $logger          Logger instance.
	 * @param string  $pull_queue_table Fully-qualified pull queue table name.
	 */
	public function __construct( Options $options, Logger $logger, $pull_queue_table ) {
		$this->options          = $options;
		$this->logger           = $logger;
		$this->pull_queue_table = $pull_queue_table;
	}

	/**
	 * Injects the Content_Manager reference for process_content() delegation.
	 *
	 * Called after both objects are constructed to avoid circular dependencies.
	 *
	 * @since 2.8.2
	 *
	 * @param Content_Manager $manager The parent Content_Manager instance.
	 */
	public function set_content_manager( Content_Manager $manager ) {
		$this->content_manager = $manager;
	}

	/**
	 * Initiates or checks the status of a pull operation for a given article.
	 *
	 * If the article is already in the queue as completed, returns an error.
	 * If it is pending, returns the current queue status. Otherwise, fetches
	 * from the API and either processes immediately or schedules a retry.
	 *
	 * @since 2.8.2
	 *
	 * @param string $article_id   External article ID to pull.
	 * @param int    $output_index Index of the output variant to use. Default 0.
	 * @return array|WP_Error Result array with status key, or WP_Error on failure.
	 */
	public function pull_content( $article_id, $output_index = 0 ) {
		$this->logger->info(
			'Starting pull operation',
			array(
				'article_id'   => $article_id,
				'output_index' => $output_index,
			)
		);

		// Check if already in queue
		$existing_queue_item = $this->get_queue_item( $article_id );

		if ( $existing_queue_item ) {
			if ( 'completed' === $existing_queue_item->status ) {
				return new WP_Error(
					'wisewand_already_processed',
					__( 'Article has already been processed successfully.', 'wisewand-ai-content-writer-seo-content-generator' ),
					array( 'status' => 409 )
				);
			}

			if ( 'pending' === $existing_queue_item->status ) {
				return array(
					'status'      => 'pending',
					'retry_after' => $this->calculate_retry_after( $existing_queue_item ),
					'attempts'    => $existing_queue_item->attempts,
				);
			}
		}

		// Fetch content from Wisewand API
		$content_data = $this->fetch_from_wisewand_api( $article_id );

		if ( is_wp_error( $content_data ) ) {
			return $content_data;
		}

		if ( 'completed' !== $content_data['status'] ) {
			// Article not ready yet, schedule retry
			$this->schedule_pull_retry( $article_id, $output_index );

			return array(
				'status'      => 'pending',
				'retry_after' => $this->get_next_retry_delay( 1 ),
				'attempts'    => 1,
			);
		}

		// Process the content
		$output_data = $this->extract_output_data( $content_data, $output_index );
		if ( is_wp_error( $output_data ) ) {
			return $output_data;
		}

		$process_result = $this->content_manager->process_content( $output_data );
		if ( is_wp_error( $process_result ) ) {
			$this->update_queue_item( $article_id, 'failed', $process_result->get_error_message() );
			return $process_result;
		}

		// Mark as completed
		$this->update_queue_item( $article_id, 'completed', null, $process_result['post_id'] );

		return array_merge( $process_result, array( 'status' => 'completed' ) );
	}

	/**
	 * Processes a scheduled pull retry for a queued article.
	 *
	 * Called via WP-Cron on the wisewand_pull_retry hook. Checks queue state,
	 * re-fetches from the API, and either processes the content or schedules
	 * another retry if the content is still not ready.
	 *
	 * @since 2.8.2
	 *
	 * @param string $article_id External article ID to retry.
	 */
	public function process_pull_retry( $article_id ) {
		$this->logger->debug( 'Processing pull retry', array( 'article_id' => $article_id ) );

		$queue_item = $this->get_queue_item( $article_id );
		if ( ! $queue_item || 'pending' !== $queue_item->status ) {
			return;
		}

		if ( $queue_item->attempts >= $queue_item->max_attempts ) {
			$this->update_queue_item( $article_id, 'failed', 'Maximum retry attempts exceeded' );
			$this->logger->warning(
				'Pull retry failed - max attempts exceeded',
				array(
					'article_id' => $article_id,
					'attempts'   => $queue_item->attempts,
				)
			);
			return;
		}

		// Try to fetch content again
		$content_data = $this->fetch_from_wisewand_api( $article_id );

		if ( is_wp_error( $content_data ) ) {
			$this->increment_queue_attempts( $article_id, $content_data->get_error_message() );
			$this->schedule_next_retry( $article_id );
			return;
		}

		if ( 'completed' !== $content_data['status'] ) {
			// Still not ready, schedule another retry
			$this->increment_queue_attempts( $article_id );
			$this->schedule_next_retry( $article_id );
			return;
		}

		// Content is ready, process it
		$meta         = json_decode( $queue_item->meta, true ) ?: array();
		$output_index = $meta['output_index'] ?? 0;

		$output_data = $this->extract_output_data( $content_data, $output_index );
		if ( is_wp_error( $output_data ) ) {
			$this->update_queue_item( $article_id, 'failed', $output_data->get_error_message() );
			return;
		}

		$process_result = $this->content_manager->process_content( $output_data );
		if ( is_wp_error( $process_result ) ) {
			$this->update_queue_item( $article_id, 'failed', $process_result->get_error_message() );
			return;
		}

		// Success!
		$this->update_queue_item( $article_id, 'completed', null, $process_result['post_id'] );

		$this->logger->info(
			'Pull retry completed successfully',
			array(
				'article_id' => $article_id,
				'post_id'    => $process_result['post_id'],
				'attempts'   => $queue_item->attempts + 1,
			)
		);
	}

	/**
	 * Retrieves a single queue row for the given article ID.
	 *
	 * @since 2.8.2
	 *
	 * @param string $article_id External article ID.
	 * @return object|null Database row object, or null if not found.
	 */
	public function get_queue_item( $article_id ) {
		global $wpdb;
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table query
		// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix is safe
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$this->pull_queue_table} WHERE article_id = %s",
				$article_id
			)
		);
	}

	/**
	 * Inserts or updates a pull queue row and schedules the first WP-Cron retry.
	 *
	 * @since 2.8.2
	 *
	 * @param string $article_id   External article ID.
	 * @param int    $output_index Output variant index. Default 0.
	 * @param int    $attempt      Attempt number to record. Default 1.
	 */
	public function schedule_pull_retry( $article_id, $output_index = 0, $attempt = 1 ) {
		global $wpdb;

		$next_attempt = gmdate( 'Y-m-d H:i:s', time() + $this->get_next_retry_delay( $attempt ) );

		$data = array(
			'article_id'   => $article_id,
			'status'       => 'pending',
			'attempts'     => $attempt,
			'next_attempt' => $next_attempt,
			'meta'         => wp_json_encode( array( 'output_index' => $output_index ) ),
		);

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table operations
		$existing = $this->get_queue_item( $article_id );
		if ( $existing ) {
			$wpdb->update( $this->pull_queue_table, $data, array( 'article_id' => $article_id ) );
		} else {
			$wpdb->insert( $this->pull_queue_table, $data );
		}
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		// Schedule WP-Cron event
		wp_schedule_single_event( strtotime( $next_attempt ), 'wisewand_pull_retry', array( $article_id ) );
	}

	/**
	 * Updates the status, error message, and optional post_id in a queue row.
	 *
	 * @since 2.8.2
	 *
	 * @param string      $article_id    External article ID.
	 * @param string      $status        New status value (pending, completed, failed).
	 * @param string|null $error_message Optional error message to store.
	 * @param int|null    $post_id       Optional WordPress post ID to store in meta.
	 */
	public function update_queue_item( $article_id, $status, $error_message = null, $post_id = null ) {
		global $wpdb;

		$data = array(
			'status'     => $status,
			'updated_at' => current_time( 'mysql' ),
		);

		if ( $error_message ) {
			$data['error_message'] = $error_message;
		}

		if ( $post_id ) {
			$meta            = json_decode( $this->get_queue_item( $article_id )->meta ?? '{}', true );
			$meta['post_id'] = $post_id;
			$data['meta']    = wp_json_encode( $meta );
		}

		$wpdb->update( $this->pull_queue_table, $data, array( 'article_id' => $article_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table update
	}

	/**
	 * Increments the attempt counter for a queue row and optionally stores an error.
	 *
	 * @since 2.8.2
	 *
	 * @param string      $article_id    External article ID.
	 * @param string|null $error_message Optional error message to record.
	 */
	public function increment_queue_attempts( $article_id, $error_message = null ) {
		global $wpdb;

		$queue_item   = $this->get_queue_item( $article_id );
		$new_attempts = $queue_item ? $queue_item->attempts + 1 : 1;

		$data = array(
			'attempts'   => $new_attempts,
			'updated_at' => current_time( 'mysql' ),
		);

		if ( $error_message ) {
			$data['error_message'] = $error_message;
		}

		$wpdb->update( $this->pull_queue_table, $data, array( 'article_id' => $article_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table update
	}

	/**
	 * Schedules the next WP-Cron retry event for a pending queue item.
	 *
	 * Marks the item as failed if max_attempts would be exceeded.
	 *
	 * @since 2.8.2
	 *
	 * @param string $article_id External article ID.
	 */
	public function schedule_next_retry( $article_id ) {
		$queue_item = $this->get_queue_item( $article_id );
		if ( ! $queue_item ) {
			return;
		}

		$next_attempt = $queue_item->attempts + 1;
		if ( $next_attempt > $queue_item->max_attempts ) {
			$this->update_queue_item( $article_id, 'failed', 'Maximum retry attempts exceeded' );
			return;
		}

		$delay             = $this->get_next_retry_delay( $next_attempt );
		$next_attempt_time = gmdate( 'Y-m-d H:i:s', time() + $delay );

		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table update
		$wpdb->update(
			$this->pull_queue_table,
			array( 'next_attempt' => $next_attempt_time ),
			array( 'article_id' => $article_id )
		);

		wp_schedule_single_event( time() + $delay, 'wisewand_pull_retry', array( $article_id ) );
	}

	/**
	 * Returns the retry delay in seconds for a given attempt number.
	 *
	 * Uses exponential backoff: 1 min, 2 min, 5 min, 10 min, 20 min,
	 * then 30 min for all subsequent attempts.
	 *
	 * @since 2.8.2
	 *
	 * @param int $attempt Current attempt number (1-based).
	 * @return int Delay in seconds.
	 */
	public function get_next_retry_delay( $attempt ) {
		// Exponential backoff: 1min, 2min, 5min, 10min, 20min, then 30min for remaining attempts
		$delays = array( 60, 120, 300, 600, 1200 ); // in seconds
		return $delays[ min( $attempt - 1, count( $delays ) - 1 ) ] ?? 1800;
	}

	/**
	 * Calculates seconds until the next scheduled retry for a queue item.
	 *
	 * @since 2.8.2
	 *
	 * @param object $queue_item Queue row object with next_attempt property.
	 * @return int|null Seconds until next retry, 0 if overdue, or null if not scheduled.
	 */
	public function calculate_retry_after( $queue_item ) {
		if ( ! $queue_item->next_attempt ) {
			return null;
		}

		$next_time = strtotime( $queue_item->next_attempt );
		$now       = time();

		return max( 0, $next_time - $now );
	}

	/**
	 * Fetches article output data from the Wisewand API.
	 *
	 * Returns the decoded JSON response array, or a WP_Error on any failure
	 * (network error, non-200 HTTP status, or invalid JSON).
	 *
	 * @since 2.8.2
	 *
	 * @param string $article_id External article ID.
	 * @return array|WP_Error Decoded API response array, or WP_Error on failure.
	 */
	public function fetch_from_wisewand_api( $article_id ) {
		$pull_settings = $this->options->get_pull_settings();

		$url = trailingslashit( $pull_settings['base_url'] ) . 'articles/' . urlencode( $article_id ) . '/output';

		$args = array(
			'timeout' => 30,
			'headers' => array(
				'User-Agent' => 'Wisewand-plugin/' . WISEWAND_PLUGIN_VERSION,
				'Accept'     => 'application/json',
			),
		);

		if ( ! empty( $pull_settings['api_key'] ) ) {
			$args['headers']['Authorization'] = 'Bearer ' . $pull_settings['api_key'];
		}

		$response = wp_remote_get( $url, $args );

		if ( is_wp_error( $response ) ) {
			return new WP_Error(
				'wisewand_api_error',
				sprintf(
					/* translators: %s: Error message from the API request */
					__( 'API request failed: %s', 'wisewand-ai-content-writer-seo-content-generator' ),
					$response->get_error_message()
				)
			);
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $response_code ) {
			return new WP_Error(
				'wisewand_api_http_error',
				sprintf(
					/* translators: %d: HTTP response code (e.g., 404, 500) */
					__( 'API returned HTTP %d', 'wisewand-ai-content-writer-seo-content-generator' ),
					$response_code
				),
				array( 'status' => $response_code )
			);
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( JSON_ERROR_NONE !== json_last_error() ) {
			return new WP_Error(
				'wisewand_api_invalid_json',
				__( 'API returned invalid JSON', 'wisewand-ai-content-writer-seo-content-generator' )
			);
		}

		return $data;
	}

	/**
	 * Extracts and normalises a specific output variant from the raw API response.
	 *
	 * Transforms the API response shape into the internal payload format expected
	 * by Content_Manager::process_content().
	 *
	 * @since 2.8.2
	 *
	 * @param array $api_data     Decoded API response array.
	 * @param int   $output_index Index of the output variant to extract. Default 0.
	 * @return array|WP_Error Normalised payload array, or WP_Error on failure.
	 */
	public function extract_output_data( $api_data, $output_index ) {
		if ( isset( $api_data['all_outputs'] ) && is_array( $api_data['all_outputs'] ) ) {
			if ( ! isset( $api_data['all_outputs'][ $output_index ] ) ) {
				return new WP_Error(
					'wisewand_invalid_output_index',
					sprintf(
						/* translators: %d: Output index number (e.g., 0, 1, 2) */
						__( 'Output index %d not found', 'wisewand-ai-content-writer-seo-content-generator' ),
						$output_index
					)
				);
			}
			$output_data = $api_data['all_outputs'][ $output_index ];
		} else {
			$output_data = $api_data;
		}

		// Ensure we have required data
		if ( ! isset( $output_data['id'] ) ) {
			return new WP_Error(
				'wisewand_missing_id',
				__( 'Article ID missing from API response', 'wisewand-ai-content-writer-seo-content-generator' )
			);
		}

		// Transform API data to internal format
		return array(
			'external_id'        => $output_data['id'],
			'title'              => $output_data['output']['h1'] ?? $output_data['input']['title'] ?? '',
			'content'            => $output_data['output']['content'] ?? '',
			'excerpt'            => $output_data['output']['meta_description'] ?? '',
			'post_status'        => $output_data['output']['publishwordpress_status'] ?? 'draft',
			'featured_image_url' => $output_data['output']['cover_image'] ?? null,
			'cover_images'       => $output_data['output']['cover_images'] ?? array(),
			'audio'              => $output_data['output']['audio'] ?? null,
			'categories'         => isset( $output_data['output']['publishwordpress_category'] ) ?
				array( $output_data['output']['publishwordpress_category'] ) : array(),
			'seo'                => array(
				'title'         => $output_data['output']['meta_title'] ?? '',
				'description'   => $output_data['output']['meta_description'] ?? '',
				'canonical_url' => $output_data['output']['url'] ?? '',
			),
			'input'              => $output_data['input'] ?? array(),
			'output'             => $output_data['output'] ?? array(),
		);
	}
}
