<?php
/**
 * PostGate Stripe API Class
 *
 * Handles all Stripe API operations for products and prices.
 * Provides methods to fetch, process, and store product/price data from Stripe.
 *
 * This class follows the delegation pattern used elsewhere in PostGate (e.g., PostGate_Product_Utils
 * for currencies). It centralizes Stripe-specific API logic, improving code organization,
 * maintainability, and testability.
 *
 * @package PostGate
 * @subpackage Stripe
 * @since 1.0.1
 * @author QodeSphere
 * @license GPL-2.0+
 */

defined( 'ABSPATH' ) || exit;

/**
 * PostGate Stripe API Class
 *
 * Centralizes all Stripe API operations for products and prices:
 *
 * - Fetching active products from Stripe API
 * - Retrieving prices for each product
 * - Processing and formatting product/price data
 * - Managing stored product data in WordPress options
 * - Preserving default product selection across syncs
 *
 * Main entry point:
 * - fetch_and_process_products(): Orchestrates complete product sync workflow
 *
 * @since 1.0.1
 */
class PostGate_Stripe_API {

	/**
	 * Fetch and process products from Stripe API.
	 *
	 * Main entry point for product synchronization. This method:
	 * 1. Fetches active products from Stripe API
	 * 2. Retrieves prices for each product
	 * 3. Processes and formats the data
	 * 4. Preserves default product selection
	 * 5. Saves data to WordPress database
	 *
	 * Returns a structured response with products, save status, and stored data.
	 *
	 * @since 1.0.1
	 *
	 * @param string $access_token Stripe API access token.
	 * @param string $mode         Connection mode: 'live' or 'sandbox'.
	 * @param int    $limit        Maximum number of products to fetch (1-100).
	 * @return array{products: array, saved: bool, message?: string, stored: array|null, stored_all: array}|WP_Error
	 *                             Processed products data or WP_Error on failure.
	 */
	public static function fetch_and_process_products( string $access_token, string $mode, int $limit ): array|WP_Error {
		// Fetch products from Stripe API
		$data = self::fetch_stripe_products( $access_token, $limit );
		if ( is_wp_error( $data ) ) {
			return $data;
		}

		// Get stored products data and find previous default
		$all = self::get_stored_products_data();
		$prev_default_id = self::find_previous_default_product_id( $all, $mode );

		// Prepare HTTP args for price fetching (reused for all requests)
		$http_args = [
			'headers' => [ 'Authorization' => "Bearer {$access_token}" ],
			'timeout' => 20,
		];

		// Process each product and fetch associated prices
		$products = [];
		$default_assigned = false;

		if ( ! empty( $data['data'] ) && is_array( $data['data'] ) ) {
			foreach ( $data['data'] as $product ) {
				// Skip products without ID
				if ( empty( $product['id'] ) ) {
					continue;
				}

				// Fetch prices for this specific product
				$prices_data = self::fetch_stripe_prices( $product['id'], $http_args );

				// Determine if this product should be marked as default
				// Only one product can be default, assigned to first match
				$is_default = false;
				if ( $product['id'] === $prev_default_id && false === $default_assigned ) {
					$is_default = true;
					$default_assigned = true;
				}

				// Process and add product to results
				$products[] = self::process_stripe_product( $product, $prices_data, $is_default );
			}
		}

		// Do NOT save if Stripe returned no products - preserves existing data
		if ( empty( $products ) ) {
			return [
				'products' => [],
				'saved' => false,
				'message' => 'No active Stripe products found for this mode.',
				'stored' => isset( $all[ $mode ] ) ? $all[ $mode ] : null,
				'stored_all' => $all,
			];
		}

		// Save only current mode's data, preserving other mode's data
		$all[ $mode ] = [
			'updated_at' => time(),
			'products' => $products,
		];
		update_option( 'postgate_stripe_products', $all );

		return [
			'products' => $products,
			'saved' => true,
			'stored' => $all[ $mode ],
			'stored_all' => $all,
		];
	}

	/**
	 * Get stored products data from database.
	 *
	 * Retrieves the postgate_stripe_products option and ensures proper structure.
	 * Returns an array with 'live' and 'sandbox' keys, each potentially containing
	 * product data or null.
	 *
	 * @since 1.0.1
	 *
	 * @return array Stored products data with 'live' and 'sandbox' keys.
	 */
	private static function get_stored_products_data(): array {
		$all = get_option( 'postgate_stripe_products' ) ?? [ 'live' => null, 'sandbox' => null ];
		if ( ! is_array( $all ) ) {
			$all = [ 'live' => null, 'sandbox' => null ];
		}
		return $all;
	}

	/**
	 * Find the ID of the previously marked default product for a given mode.
	 *
	 * Searches through stored products to find which one was marked as default.
	 * Returns empty string if no default product is found.
	 *
	 * @since 1.0.1
	 *
	 * @param array  $stored_products Stored products data for the mode.
	 * @param string $mode            Connection mode: 'live' or 'sandbox'.
	 * @return string Product ID of previous default or empty string.
	 */
	private static function find_previous_default_product_id( array $stored_products, string $mode ): string {
		if ( ! isset( $stored_products[ $mode ]['products'] ) || ! is_array( $stored_products[ $mode ]['products'] ) ) {
			return '';
		}

		foreach ( $stored_products[ $mode ]['products'] as $product_item ) {
			if ( ! empty( $product_item['id'] ) && ! empty( $product_item['is_default'] ) ) {
				return $product_item['id'];
			}
		}

		return '';
	}

	/**
	 * Fetch products from Stripe API.
	 *
	 * Makes HTTP request to Stripe to retrieve active products.
	 * Returns parsed response data or WP_Error on failure.
	 *
	 * @since 1.0.1
	 *
	 * @param string $access_token Stripe API access token.
	 * @param int    $limit        Maximum number of products to fetch.
	 * @return array|WP_Error Array with response data or WP_Error on failure.
	 */
	private static function fetch_stripe_products( string $access_token, int $limit ): array|WP_Error {
		$url = "https://api.stripe.com/v1/products?active=true&limit={$limit}";
		$args = [
			'headers' => [ 'Authorization' => "Bearer {$access_token}" ],
			'timeout' => 20,
		];
		$response = wp_remote_get( $url, $args );

		if ( is_wp_error( $response ) ) {
			return new WP_Error( 'postgate_http_error', $response->get_error_message(), [ 'status' => 500 ] );
		}

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

		if ( $code < 200 || $code >= 300 || ! is_array( $data ) ) {
			return new WP_Error(
				'postgate_stripe_error',
				'Stripe API error while fetching products.',
				[ 'status' => 500, 'body' => $body, 'code' => $code ]
			);
		}

		return $data;
	}

	/**
	 * Fetch prices for a specific product from Stripe API.
	 *
	 * Makes HTTP request to retrieve active prices associated with a product.
	 * Returns array of processed price data. Returns empty array on error to maintain resilience.
	 *
	 * @since 1.0.1
	 *
	 * @param string $product_id Stripe product ID.
	 * @param array  $http_args  HTTP request arguments with authorization header.
	 * @return array Array of processed price data.
	 */
	private static function fetch_stripe_prices( string $product_id, array $http_args ): array {
		$prices_url = "https://api.stripe.com/v1/prices?product={$product_id}&active=true&limit=100";
		$prices_response = wp_remote_get( $prices_url, $http_args );
		$prices_data = [];

		if ( ! is_wp_error( $prices_response ) && 200 === wp_remote_retrieve_response_code( $prices_response ) ) {
			$prices_body = wp_remote_retrieve_body( $prices_response );
			$prices_parsed = json_decode( $prices_body, true );

			if ( ! empty( $prices_parsed['data'] ) && is_array( $prices_parsed['data'] ) ) {
				foreach ( $prices_parsed['data'] as $price ) {
					$prices_data[] = self::process_stripe_price( $price );
				}
			}
		}

		return $prices_data;
	}

	/**
	 * Process a single Stripe price object into standardized format.
	 *
	 * Extracts and formats price data including amount, currency, type, interval,
	 * interval_count, nickname, and metadata.
	 *
	 * @since 1.0.1
	 *
	 * @param array $price Raw Stripe price object.
	 * @return array Processed price data.
	 */
	private static function process_stripe_price( array $price ): array {
		$type = ( isset( $price['type'] ) && $price['type'] === 'recurring' ) ? 'recurring' : 'one_time';
		$interval = '';
		$interval_count = 1;

		if ( 'recurring' === $type && ! empty( $price['recurring']['interval'] ) ) {
			$interval = (string) $price['recurring']['interval'];
			$interval_count = isset( $price['recurring']['interval_count'] ) && is_numeric( $price['recurring']['interval_count'] )
				? max( 1, intval( $price['recurring']['interval_count'] ) )
				: 1;
		}

		return [
			'id' => (string) $price['id'],
			'amount' => isset( $price['unit_amount'] ) ? intval( $price['unit_amount'] ) : 0,
			'currency' => strtoupper( (string) ( $price['currency'] ?? 'USD' ) ),
			'interval' => $interval,
			'interval_count' => $interval_count,
			'type' => $type,
			'nickname' => ! empty( $price['nickname'] ) ? (string) $price['nickname'] : '',
			'metadata' => isset( $price['metadata'] ) && is_array( $price['metadata'] ) ? $price['metadata'] : [],
		];
	}

	/**
	 * Process a single Stripe product object into standardized format.
	 *
	 * Extracts and formats product data including ID, name, description, default_price,
	 * associated prices array, default status, and metadata.
	 *
	 * @since 1.0.1
	 *
	 * @param array $product     Raw Stripe product object.
	 * @param array $prices_data Processed prices array for this product.
	 * @param bool  $is_default  Whether this product should be marked as default.
	 * @return array Processed product data.
	 */
	private static function process_stripe_product( array $product, array $prices_data, bool $is_default ): array {
		return [
			'id' => (string) $product['id'],
			'name' => sanitize_text_field( (string) ( $product['name'] ?? $product['id'] ) ),
			'description' => sanitize_textarea_field( (string) ( $product['description'] ?? '' ) ),
			'default_price' => ! empty( $product['default_price'] ) ? (string) $product['default_price'] : '',
			'prices' => $prices_data,
			'is_default' => $is_default,
			'metadata' => isset( $product['metadata'] ) && is_array( $product['metadata'] ) ? $product['metadata'] : [],
		];
	}
}
