<?php

namespace ImageOptimizer\Modules\Optimization\Classes;

use ImageOptimizer\Classes\Async_Operation\{
	Async_Operation,
	Async_Operation_Hook,
	Async_Operation_Queue,
	Exceptions\Async_Operation_Exception,
	Queries\Image_Optimization_Operation_Query
};
use ImageOptimizer\Classes\Image\{
	Exceptions\Invalid_Image_Exception,
	Image,
	Image_Meta,
	Image_Optimization_Error_Type,
	Image_Query_Builder,
	Image_Status,
	WP_Image_Meta
};
use ImageOptimizer\Classes\Utils;
use ImageOptimizer\Modules\Oauth\Classes\Data;
use ImageOptimizer\Modules\Oauth\Classes\Exceptions\Quota_Exceeded_Error;
use ImageOptimizer\Modules\Optimization\Classes\Exceptions\Bulk_Token_Obtaining_Error;
use ImageOptimizer\Modules\Optimization\Components\Exceptions\Bulk_Optimization_Exception;
use ImageOptimizer\Modules\Stats\Classes\Optimization_Stats;

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

class Bulk_Optimization {
	private const OBTAIN_TOKEN_ENDPOINT = 'image/bulk-token';

	/**
	 * Cancels pending bulk optimization operations.
	 *
	 * @return void
	 * @throws Async_Operation_Exception
	 */
	public static function cancel_bulk_optimization(): void {
		$query = ( new Image_Optimization_Operation_Query() )
			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
			// It's risky to cancel in-progress operations at that point, so we cancel only the pending ones.
			->set_status( Async_Operation::OPERATION_STATUS_PENDING )
			->set_limit( -1 );

		$operations = Async_Operation::get( $query );

		foreach ( $operations as $operation ) {
			$image_id = $operation->get_args()['attachment_id'];

			Async_Operation::cancel( $operation->get_id() );

			( new Image_Meta( $image_id ) )->delete();
		}
	}

	/**
	 * Looks for all non-optimized images and creates a bulk operation for each of them.
	 * Also, obtains bulk token and passes it to a newly created operation.
	 *
	 * @return void
	 * @throws Quota_Exceeded_Error|Invalid_Image_Exception
	 */
	public static function find_images_and_schedule_optimization(): void {
		$images = self::find_images(
			( new Image_Query_Builder() )
				->return_not_optimized_images(),
			true
		);

		if ( ! $images['total_images_count'] ) {
			return;
		}

		$operation_id = wp_generate_password( 10, false );

		try {
			$bulk_token = self::obtain_bulk_token( $images['total_images_count'] );
			self::set_bulk_operation_token( $operation_id, $bulk_token );
		} catch ( Bulk_Token_Obtaining_Error $e ) {
			$bulk_token = null;
		}

		foreach ( $images['attachments_in_quota'] as $attachment_id ) {
			$meta = new Image_Meta( $attachment_id );

			if ( null === $bulk_token ) {
				$meta
					->set_status( Image_Status::OPTIMIZATION_FAILED )
					->save();

				continue;
			}

			try {
				Async_Operation::create(
					Async_Operation_Hook::OPTIMIZE_BULK,
					[
						'attachment_id' => $attachment_id,
						'operation_id' => $operation_id,
					],
					Async_Operation_Queue::OPTIMIZE
				);

				$meta
					->set_status( Image_Status::OPTIMIZATION_IN_PROGRESS )
					->save();
			} catch ( Async_Operation_Exception $aoe ) {
				$meta
					->set_status( Image_Status::OPTIMIZATION_FAILED )
					->save();

				continue;
			}
		}
	}

	/**
	 * Looks for already optimized images with backups and creates a bulk operation for each of them.
	 * Also, obtains bulk token and passes it to a newly created operation.
	 *
	 * @return void
	 * @throws Quota_Exceeded_Error|Invalid_Image_Exception
	 */
	public static function find_optimized_images_and_schedule_reoptimization(): void {
		$images = self::find_images(
			( new Image_Query_Builder() )
				->return_optimized_images()
		);

		if ( ! $images['total_images_count'] ) {
			return;
		}

		$operation_id = wp_generate_password( 10, false );

		try {
			$bulk_token = self::obtain_bulk_token( $images['total_images_count'] );
			self::set_bulk_operation_token( $operation_id, $bulk_token );
		} catch ( Bulk_Token_Obtaining_Error $e ) {
			$bulk_token = null;
		}

		foreach ( $images['attachments_in_quota'] as $attachment_id ) {
			$meta = new Image_Meta( $attachment_id );

			if ( null === $bulk_token ) {
				$meta
					->set_status( Image_Status::REOPTIMIZING_FAILED )
					->save();

				continue;
			}

			try {
				Async_Operation::create(
					Async_Operation_Hook::REOPTIMIZE_BULK,
					[
						'attachment_id' => $attachment_id,
						'operation_id' => $operation_id,
					],
					Async_Operation_Queue::OPTIMIZE
				);

				$meta
					->set_status( Image_Status::REOPTIMIZING_IN_PROGRESS )
					->save();
			} catch ( Async_Operation_Exception $aoe ) {
				$meta
					->set_status( Image_Status::REOPTIMIZING_FAILED )
					->save();

				continue;
			}
		}

		foreach ( $images['attachments_out_of_quota'] as $attachment_id ) {
			( new Image_Meta( $attachment_id ) )
				->set_status( Image_Status::REOPTIMIZING_FAILED )
				->set_error_type( Image_Optimization_Error_Type::QUOTA_EXCEEDED )
				->save();
		}
	}

	/**
	 * Looks for images for bulk optimization operations based on a query passed and the quota left.
	 *
	 * @param Image_Query_Builder $query Image query to execute.
	 * @param bool $limit_to_quota If true, it limits image query to the quota left.
	 * @return array{total_images_count: int, attachments_in_quota: array, attachments_out_of_quota: array}
	 *
	 * @throws Invalid_Image_Exception
	 * @throws Quota_Exceeded_Error
	 */
	private static function find_images( Image_Query_Builder $query, bool $limit_to_quota = false ): array {
		$output = [
			'total_images_count' => 0,
			'attachments_in_quota' => [],
			'attachments_out_of_quota' => [],
		];
		$images_left = Data::images_left();

		if ( ! $images_left ) {
			throw new Quota_Exceeded_Error( __( 'Images quota exceeded', 'image-optimizer' ) );
		}

		if ( $limit_to_quota ) {
			$query->set_paging_size( $images_left );
		}

		$wp_query = $query->execute();

		if ( ! $wp_query->post_count ) {
			return $output;
		}

		foreach ( $wp_query->posts as $attachment_id ) {
			$wp_meta = new WP_Image_Meta( $attachment_id );
			$sizes_count = count( $wp_meta->get_size_keys() );

			if ( $output['total_images_count'] + $sizes_count <= $images_left ) {
				$output['total_images_count'] += $sizes_count;
				$output['attachments_in_quota'][] = $attachment_id;
			} else {
				break;
			}
		}

		$output['attachments_out_of_quota'] = array_diff( $wp_query->posts, $output['attachments_in_quota'] );

		return $output;
	}

	/**
	 * Looks for the bulk token in transients.
	 *
	 * @param string $operation_id Bulk optimization operation id
	 *
	 * @return string|null Bulk token.
	 * @throws Bulk_Optimization_Exception
	*/
	public static function get_bulk_operation_token( string $operation_id ): ?string {
		$bulk_token = get_transient( "image_optimizer_bulk_token_$operation_id" );

		if ( ! $bulk_token ) {
			throw new Bulk_Optimization_Exception( "There is no token found for the operation $operation_id" );
		}

		return $bulk_token;
	}

	/**
	 * Saves bulk optimization token to transients for a day.
	 *
	 * @param string $operation_id Bulk optimization operation id
	 * @param string $bulk_token Bulk optimization token
	 * @return void
	 */
	public static function set_bulk_operation_token( string $operation_id, string $bulk_token ): void {
		set_transient( "image_optimizer_bulk_token_$operation_id", $bulk_token, DAY_IN_SECONDS );
	}

	/**
	 * Sends a request to the BE to obtain bulk optimization token.
	 * It prevents obtaining a token for each and every optimization operation.
	 *
	 * @return string
	 * @throws Bulk_Token_Obtaining_Error
	 */
	private static function obtain_bulk_token( int $images_count ): ?string {
		$response = Utils::get_api_client()->make_request(
			'POST',
			self::OBTAIN_TOKEN_ENDPOINT,
			[
				'images_count' => $images_count,
			]
		);

		if ( is_wp_error( $response ) ) {
			throw new Bulk_Token_Obtaining_Error( $response->get_error_message() );
		}

		return $response->token ?? null;
	}

	/**
	 * Checks if there is a bulk optimization operation in progress.
	 * If there is at least a single active bulk optimization operation it returns true, otherwise false.
	 *
	 * @return bool
	 * @throws Async_Operation_Exception
	 */
	public static function is_optimization_in_progress(): bool {
		$query = ( new Image_Optimization_Operation_Query() )
			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
			->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
			->set_limit( 1 )
			->return_ids();

		return ! empty( Async_Operation::get( $query ) );
	}

	/**
	 * Checks if there is a bulk re-optimization operation in progress.
	 * If there is at least a single active bulk re-optimization operation it returns true, otherwise false.
	 *
	 * @return bool
	 * @throws Async_Operation_Exception
	 */
	public static function is_reoptimization_in_progress(): bool {
		$query = ( new Image_Optimization_Operation_Query() )
			->set_hook( Async_Operation_Hook::REOPTIMIZE_BULK )
			->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
			->set_limit( 1 )
			->return_ids();

		return ! empty( Async_Operation::get( $query ) );
	}

	/**
	 * Retrieves the bulk optimization process status.
	 *
	 * @return array{status: string, stats: array}
	 * @throws Async_Operation_Exception
	 */
	public static function get_status(): array {
		$stats = Optimization_Stats::get_image_stats();

		$output = [
			'status' => 'not-started',
			'percentage' => round( $stats['optimized_image_count'] / $stats['total_image_count'] * 100 ),
		];

		$active_query = ( new Image_Optimization_Operation_Query() )
			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
			->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
			->set_limit( -1 );

		if ( empty( Async_Operation::get( $active_query ) ) ) {
			return $output;
		}

		$output['status'] = 'in-progress';

		return $output;
	}

	/**
	 * Returns latest operations for the bulk optimization screen.
	 *
	 * @return array
	 * @throws Async_Operation_Exception
	 */
	public static function get_processed_images(): array {
		$output = [];

		$first_operation_query = ( new Image_Optimization_Operation_Query() )
			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
			->set_limit( 1 );

		$first_operation = Async_Operation::get( $first_operation_query )[0] ?? null;

		if ( empty( $first_operation ) ) {
			return [];
		}

		$query = ( new Image_Optimization_Operation_Query() )
			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
			->set_status([
				Async_Operation::OPERATION_STATUS_NOT_STARTED,
				Async_Operation::OPERATION_STATUS_PENDING,
				Async_Operation::OPERATION_STATUS_RUNNING,
				Async_Operation::OPERATION_STATUS_COMPLETE,
				Async_Operation::OPERATION_STATUS_FAILED,
				Async_Operation::OPERATION_STATUS_CANCELED,
			])
			->set_bulk_operation_id( $first_operation->get_args()['operation_id'] )
			->set_limit( 50 );

		$operations = Async_Operation::get( $query );

		if ( empty( $operations ) ) {
			return [];
		}

		foreach ( $operations as $operation ) {
			$image_id = $operation->get_args()['attachment_id'];

			try {
				$image = new Image( $image_id );
				$meta = new Image_Meta( $image_id );
				$wp_meta = new WP_Image_Meta( $image_id );
			} catch ( Invalid_Image_Exception $e ) {
				continue;
			}

			$output[] = [
				'operation_id' => $operation->get_id(),
				'status' => $operation->get_status() === Async_Operation::OPERATION_STATUS_COMPLETE
					? ( new Image_Meta( $image_id ) )->get_status()
					: $operation->get_status(),
				'image_name' => $image->get_attachment_object()->post_title,
				'image_id' => $image_id,
				'thumbnail_url' => $image->get_url( 'thumbnail' ),
				'original_file_size' => $meta->get_original_file_size( Image::SIZE_FULL ),
				'current_file_size' => $wp_meta->get_file_size( Image::SIZE_FULL ),
			];
		}

		return $output;
	}
}
