<?php
/**
 * FinalPOS Stock API
 *
 * REST API endpoint for handling stock delta updates.
 *
 * @package FinalPOS
 * @since 1.3.2
 */

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

use Automattic\WooCommerce\Internal\Admin\Settings\Exceptions\ApiException;
use Automattic\WooCommerce\Internal\RestApiControllerBase;


/**
 * Class FinalPOS_Stock_API
 *
 * Handles REST API endpoints for stock management.
 */
class FinalPOS_Stock_API extends RestApiControllerBase {

		/**
		 * Route base.
		 *
		 * @var string
		 */
	protected string $rest_base = 'finalpos';

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'finalpos_stock';


	/**
	 * Get the WooCommerce REST API namespace for the class.
	 *
	 * @return string
	 */
	protected function get_rest_api_namespace(): string {
		return $this->namespace;
	}

	/**
	 * Initialize the REST API endpoints.
	 *
	 * @return void
	 */
	public static function init() {
		$instance = new self();
		add_action( 'rest_api_init', array( $instance, 'register_routes' ) );

		// Register webhook delivery action hook.
		add_action( 'finalpos_trigger_product_webhooks', array( __CLASS__, 'process_webhook_delivery' ) );
	}

	/**
	 * Register REST API routes.
	 *
	 * Request body example:
	 * {
	 *   "items": [
	 *     {
	 *       "product_id": 177,
	 *       "stock_delta": 33,
	 *       "timestamp": 1704067200
	 *     },
	 *     {
	 *       "product_id": 178,
	 *       "stock_delta": -10,
	 *       "timestamp": 1704067201
	 *     }
	 *   ]
	 * }
	 *
	 * @return void
	 */
	public function register_routes(): void {
		register_rest_route(
			$this->route_namespace,
			'/' . $this->rest_base . '/stock/delta',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'handle_stock_delta' ),
					'permission_callback' => fn( $request ): bool|WP_Error => $this->check_permission( $request, 'manage_woocommerce' ),
					'args'                => array(
						'items' => array(
							'required'          => true,
							'type'              => 'array',
							'validate_callback' => array( $this, 'validate_stock_delta_array' ),
						),
					),
					'schema'              => $this->get_stock_delta_schema(),
				),
			)
		);
	}

	/**
	 * Check permissions for the REST API endpoint.
	 *
	 * @param WP_REST_Request $request                  Full details about the request.
	 * @param string          $required_capability_name Required capability name.
	 * @param mixed           ...$extra_args            Extra arguments.
	 * @return bool|WP_Error
	 */
	public function check_permissions( WP_REST_Request $request, string $required_capability_name, ...$extra_args ): bool|WP_Error {

		if ( ! current_user_can( $required_capability_name ) ) {
			return new WP_Error(
				'rest_forbidden',
				__( 'You do not have permission to manage stock.', 'finalpos' ),
				array( 'status' => WP_Http::FORBIDDEN )
			);
		}

		return $this->check_permission( $request, $required_capability_name, ...$extra_args );
	}

	/**
	 * Validate product_id parameter.
	 *
	 * @param int             $value   The value to validate.
	 * @param WP_REST_Request $request Full details about the request.
	 * @param string          $param   The parameter name.
	 * @return bool|WP_Error
	 */
	public function validate_product_id( $value, $request, $param ): bool|WP_Error {
		if ( ! is_numeric( $value ) || $value <= 0 ) {
			return new WP_Error(
				'rest_invalid_param',
				/* translators: %s: Parameter name */
				sprintf( __( '%s must be a positive integer.', 'finalpos' ), $param ),
				array( 'status' => WP_Http::BAD_REQUEST )
			);
		}

		return true;
	}

	/**
	 * Validate stock_delta parameter.
	 *
	 * @param mixed           $value   The value to validate.
	 * @param WP_REST_Request $request Full details about the request.
	 * @param string          $param   The parameter name.
	 * @return bool|WP_Error
	 */
	public function validate_stock_delta( $value, $request, string $param ): bool|WP_Error {
		if ( ! is_numeric( $value ) ) {
			return new WP_Error(
				'rest_invalid_param',
				/* translators: %s: Parameter name */
				sprintf( __( '%s must be a non-zero number.', 'finalpos' ), $param ),
				array( 'status' => WP_Http::BAD_REQUEST )
			);
		}

		return true;
	}

	/**
	 * Validate stock delta array parameter.
	 *
	 * @param mixed           $value   The value to validate.
	 * @param WP_REST_Request $request Full details about the request.
	 * @param string          $param   The parameter name.
	 * @return bool|WP_Error
	 */
	public function validate_stock_delta_array( $value, $request, $param ): bool|WP_Error {
		if ( ! is_array( $value ) ) {
			return new WP_Error(
				'rest_invalid_param',
				/* translators: %s: Parameter name */
				sprintf( __( 'Must be an array: ', 'finalpos' ) . '%s', $param ),
				array( 'status' => WP_Http::BAD_REQUEST )
			);
		}

		if ( empty( $value ) ) {
			return new WP_Error(
				'rest_invalid_param',
				/* translators: %s: Parameter name */
				sprintf( __( 'Cannot be empty.', 'finalpos' ) . '%s', $param ),
				array( 'status' => WP_Http::BAD_REQUEST )
			);
		}

		foreach ( $value as $index => $item ) {
			if ( ! is_array( $item ) ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: %d: Item index */
					sprintf( __( 'Item at index %d must be an object.', 'finalpos' ), $index ),
					array( 'status' => WP_Http::BAD_REQUEST )
				);
			}

			// Validate product_id.
			if ( ! isset( $item['product_id'] ) || ! is_numeric( $item['product_id'] ) || $item['product_id'] <= 0 ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: %d: Item index */
					sprintf( __( 'Item at index %d: product_id must be a positive integer.', 'finalpos' ), $index ),
					array( 'status' => WP_Http::BAD_REQUEST )
				);
			}

			// Validate stock_delta.
			if ( ! isset( $item['stock_delta'] ) || ! is_numeric( $item['stock_delta'] ) ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: %d: Item index */
					sprintf( __( 'Item at index %d: stock_delta must be a number.', 'finalpos' ), $index ),
					array( 'status' => WP_Http::BAD_REQUEST )
				);
			}

			// Validate timestamp (optional but if present must be numeric).
			if ( isset( $item['timestamp'] ) && ! is_numeric( $item['timestamp'] ) ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: %d: Item index */
					sprintf( __( 'Item at index %d: timestamp must be a number.', 'finalpos' ), $index ),
					array( 'status' => WP_Http::BAD_REQUEST )
				);
			}
		}

		return true;
	}

	/**
	 * Handle stock delta update request.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response
	 */
	public function handle_stock_delta( $request ): WP_REST_Response|WP_Error {
		global $wpdb;

		// Get the array of items.
		$items = $request->get_param( 'items' );

		if ( ! is_array( $items ) || empty( $items ) ) {
			return $this->prepare_error_response(
				'invalid_items',
				__( 'Items must be a non-empty array.', 'finalpos' ),
				WP_Http::BAD_REQUEST
			);
		}

		$results             = array();
		$has_errors          = false;
		$updated_product_ids = array();

		// TODO: Consider possibility to use batch update or queue instead of one by one updates.
		// Start database transaction for all updates.
		$wpdb->query( 'START TRANSACTION' );

		try {
			foreach ( $items as $index => $item ) {
				// Sanitize parameters.
				$product_id  = absint( $item['product_id'] );
				$stock_delta = floatval( $item['stock_delta'] );
				$timestamp   = isset( $item['timestamp'] ) ? absint( $item['timestamp'] ) : null;

				// Load the product.
				$product = wc_get_product( $product_id );

				if ( ! $product ) {
					$results[]  = array(
						'success'    => false,
						'index'      => $index,
						'product_id' => $product_id,
						'error'      => array(
							'code'    => 'product_not_found',
							'message' => __( 'Product not found.', 'finalpos' ),
						),
					);
					$has_errors = true;
					continue;
				}

				// Check if product manages stock.
				if ( ! $product->managing_stock() ) {
					$results[]  = array(
						'success'    => false,
						'index'      => $index,
						'product_id' => $product_id,
						'error'      => array(
							'code'    => 'stock_not_managed',
							'message' => __( 'This product does not manage stock.', 'finalpos' ),
						),
					);
					$has_errors = true;
					continue;
				}

				// Determine operation based on stock_delta sign.
				$operation = $stock_delta < 0 ? 'decrease' : 'increase';
				$quantity  = abs( $stock_delta );

				// Update product stock.
				$new_stock = wc_update_product_stock( $product, $quantity, $operation );

				if ( is_wp_error( $new_stock ) ) {
					// If it has an error just skip it and continue.
					// TODO: Consider possibility to log the error and return it to the caller.
					/** @var WP_Error $new_stock */
					$results[]  = array(
						'success'    => false,
						'index'      => $index,
						'product_id' => $product_id,
						'error'      => array(
							'code'    => $new_stock->get_error_code(),
							'message' => $new_stock->get_error_message(),
						),
					);
					$has_errors = true;
					continue;
				}

				// Success result.
				$result = array(
					'success'    => true,
					'index'      => $index,
					'product_id' => $product_id,
					'operation'  => $operation,
					'quantity'   => $quantity,
					'new_stock'  => $new_stock,
				);

				if ( null !== $timestamp ) {
					$result['timestamp'] = $timestamp;
				}

				$results[ $product_id ] = $result;

				// Collect successfully updated product ID for webhook triggering.
				$updated_product_ids[] = $product_id;
			}

			// Commit transaction if no errors occurred, otherwise rollback.
			if ( $has_errors ) {
				$wpdb->query( 'ROLLBACK' );
			} else {
				$wpdb->query( 'COMMIT' );
			}

			// Schedule webhook delivery for all successfully updated products (fire-and-forget).
			if ( ! empty( $updated_product_ids ) ) {
				$this->schedule_webhook_delivery( $updated_product_ids );
			}

			// Return response with results.
			$status_code = $has_errors ? WP_Http::MULTI_STATUS : WP_Http::OK;
			$message     = $has_errors
				? __( 'Some stock updates failed.', 'finalpos' )
				: __( 'All stock updates completed successfully.', 'finalpos' );

			return new WP_REST_Response(
				array(
					'success' => ! $has_errors,
					'message' => $message,
					'results' => $results,
				),
				$status_code
			);

		} catch ( ApiException $ex ) {
			// Rollback transaction on error.
			$wpdb->query( 'ROLLBACK' );

			/**
			 * Exception caught during stock update.
			 *
			 * @var ApiException $ex
			 */
			return $this->prepare_error_response(
				$ex->getErrorCode(),
				$ex->getMessage(),
				WP_Http::BAD_REQUEST
			);

		} catch ( \Exception $e ) {
			// Rollback transaction on error.
			$wpdb->query( 'ROLLBACK' );

			$error_code = $e->getCode();
			if ( empty( $error_code ) ) {
				$error_code = 'stock_update_failed';
			}

			return $this->prepare_error_response(
				$error_code,
				$e->getMessage(),
				WP_Http::INTERNAL_SERVER_ERROR
			);
		}
	}

	/**
	 * Get the schema for the stock delta endpoint.
	 *
	 * @return array
	 */
	private function get_stock_delta_schema(): array {
		return array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'stock-delta-response',
			'type'       => 'object',
			'properties' => array(
				'success' => array(
					'description' => __( 'Whether all stock updates were successful.', 'finalpos' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'message' => array(
					'description' => __( 'A message indicating the result of the stock updates.', 'finalpos' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'results' => array(
					'description' => __( 'Array of results for each stock update item.', 'finalpos' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'success'    => array(
								'description' => __( 'Whether this item was processed successfully.', 'finalpos' ),
								'type'        => 'boolean',
							),
							'index'      => array(
								'description' => __( 'The index of the item in the request array.', 'finalpos' ),
								'type'        => 'integer',
							),
							'product_id' => array(
								'description' => __( 'The product ID that was updated.', 'finalpos' ),
								'type'        => 'integer',
							),
							'operation'  => array(
								'description' => __( 'The operation performed (increase or decrease).', 'finalpos' ),
								'type'        => 'string',
								'enum'        => array( 'increase', 'decrease' ),
							),
							'quantity'   => array(
								'description' => __( 'The quantity that was changed.', 'finalpos' ),
								'type'        => 'number',
							),
							'new_stock'  => array(
								'description' => __( 'The new stock level after the update.', 'finalpos' ),
								'type'        => 'number',
							),
							'timestamp'  => array(
								'description' => __( 'The timestamp from the request, if provided.', 'finalpos' ),
								'type'        => 'integer',
							),
							'error'      => array(
								'description' => __( 'Error details if the update failed.', 'finalpos' ),
								'type'        => 'object',
								'properties'  => array(
									'code'    => array(
										'description' => __( 'Error code.', 'finalpos' ),
										'type'        => 'string',
									),
									'message' => array(
										'description' => __( 'Error message.', 'finalpos' ),
										'type'        => 'string',
									),
								),
							),
						),
					),
				),
			),
		);
	}

	/**
	 * Prepare error response in WooCommerce style.
	 *
	 * @param string $code    Error code.
	 * @param string $message Error message.
	 * @param int    $status  HTTP status code.
	 * @return WP_REST_Response
	 */
	private function prepare_error_response( $code, $message, $status ): WP_REST_Response {
		return new WP_REST_Response(
			array(
				'code'    => $code,
				'message' => $message,
				'data'    => array( 'status' => $status ),
			),
			$status
		);
	}

	/**
	 * Manually trigger all active webhooks for product.updated topic.
	 *
	 * @param array $product_ids Array of product IDs that were updated.
	 * @return void
	 */
	private static function trigger_product_updated_webhooks( array $product_ids ): void {
		if ( empty( $product_ids ) || ! class_exists( 'WC_Webhook' ) ) {
			return;
		}

		// Get all active webhook IDs using WooCommerce data store.
		$data_store  = WC_Data_Store::load( 'webhook' );
		$webhook_ids = $data_store->get_webhooks_ids( 'active' );

		if ( empty( $webhook_ids ) ) {
			return;
		}

		// Trigger webhooks for each product.
		foreach ( $product_ids as $product_id ) {
			foreach ( $webhook_ids as $webhook_id ) {
				try {
					$webhook = new WC_Webhook( $webhook_id );

					// Verify webhook is active and matches topic.
					if ( 'active' === $webhook->get_status() && 'product.updated' === $webhook->get_topic() ) {
						// Deliver webhook with the product ID.
						$webhook->set_pending_delivery( false );
						$webhook->deliver( $product_id );
						$webhook->save();
					}
				} catch ( \Exception $e ) {
					// Log error but continue with other webhooks.
					error_log(
						sprintf(
							'FinalPOS: Failed to trigger webhook %d for product %d: %s',
							$webhook_id,
							$product_id,
							$e->getMessage()
						)
					);
				}
			}
		}
	}

	/**
	 * Schedule webhook delivery as fire-and-forget background task.
	 *
	 * Uses WordPress cron with immediate spawn to trigger webhooks asynchronously
	 * without blocking the API response.
	 *
	 * @param array $product_ids Array of product IDs that were successfully updated.
	 * @return void
	 */
	private function schedule_webhook_delivery( array $product_ids ): void {
		if ( empty( $product_ids ) ) {
			return;
		}

		// Schedule immediate action with product IDs.
		$scheduled = wp_schedule_single_event(
			time(),
			'finalpos_trigger_product_webhooks',
			array( $product_ids )
		);

		if ( false !== $scheduled ) {
			// Trigger immediate cron execution (fire-and-forget).
			spawn_cron();
		}
	}

	/**
	 * Process webhook delivery (static method called by cron).
	 *
	 * This method is called asynchronously by WordPress cron to trigger
	 * webhooks for all updated products without blocking the API response.
	 *
	 * @param array $product_ids Array of product IDs to trigger webhooks for.
	 * @return void
	 */
	public static function process_webhook_delivery( array $product_ids ): void {
		// Call trigger_product_updated_webhooks with the array of product IDs.
		self::trigger_product_updated_webhooks( $product_ids );
	}
}
