<?php
/**
 * Pricing resolver + ConvertPlus buy-link builder.
 *
 * Two sources: 'mock' (illustrative, no API needed) and 'live' (2Checkout API — wired once
 * account API access is granted). Source is chosen by the 'pricing_source' setting.
 *
 * @package BW_2Checkout_Pricing
 */

defined( 'ABSPATH' ) || exit;

class BW_2Checkout_Pricing_Pricing {

	/** Illustrative FX multipliers for mock mode only. Real rates come from the API. */
	const MOCK_FX = array(
		'USD' => 1.0,
		'EUR' => 0.92,
		'GBP' => 0.79,
		'CAD' => 1.37,
		'AUD' => 1.52,
		'JPY' => 157.0,
	);

	/**
	 * Resolve a price for a product + selection + currency.
	 *
	 * @param string $product_id Local product id (key in the products config).
	 * @param array  $options    Map of option-group code => selected value.
	 * @param string $currency   ISO 4217 code.
	 * @return array|WP_Error
	 */
	public function get_price( $product_id, array $options, $currency ) {
		$products = bw_2checkout_pricing_get_products();
		if ( ! isset( $products[ $product_id ] ) ) {
			return new WP_Error( 'bw_2checkout_pricing_unknown_product', __( 'Unknown product.', 'bw-2checkout-pricing' ) );
		}

		$currency = strtoupper( preg_replace( '/[^A-Za-z]/', '', (string) $currency ) );
		if ( '' === $currency ) {
			$currency = (string) bw_2checkout_pricing_get_setting( 'default_currency', 'USD' );
		}

		$source = ( 'live' === bw_2checkout_pricing_get_setting( 'pricing_source', 'mock' ) )
			? 'live'
			: 'mock';

		if ( 'live' === $source ) {
			$live = $this->get_live_price( $products[ $product_id ], $options, $currency );
			if ( ! is_wp_error( $live ) ) {
				return $live;
			}
			// Fall through to mock on error so the widget still renders something.
		}

		return $this->get_mock_price( $options, $currency );
	}

	/**
	 * Illustrative pricing so the widget is fully clickable before API access exists.
	 * Per-seat rate for the tier, multiplied by the user count. Clearly labelled 'mock'.
	 *
	 * @param array  $options  Option-group => value.
	 * @param string $currency ISO code.
	 * @return array
	 */
	protected function get_mock_price( array $options, $currency ) {
		$edition = isset( $options['EDITION_2'] ) ? (string) $options['EDITION_2'] : 'advanced';
		$term    = isset( $options['TERM'] ) ? (string) $options['TERM'] : '1Y';
		$users   = isset( $options['users'] ) ? max( 1, (int) $options['users'] ) : 1;

		$per_seat_base = array(
			'basic'    => 34.99,
			'advanced' => 44.99,
			'elite'    => 54.99,
		);
		$term_mult = array(
			'1Y' => 1.0,
			'2Y' => 1.8,
			'3Y' => 2.5,
		);

		$base  = isset( $per_seat_base[ $edition ] ) ? $per_seat_base[ $edition ] : 44.99;
		$tmult = isset( $term_mult[ $term ] ) ? $term_mult[ $term ] : 1.0;
		$vol   = $this->mock_volume_factor( $users );
		$fx    = isset( self::MOCK_FX[ $currency ] ) ? self::MOCK_FX[ $currency ] : 1.0;

		$unit  = round( $base * $tmult * $vol * $fx, 2 );
		$total = round( $unit * $users, 2 );

		return array(
			'source'         => 'mock',
			'currency'       => $currency,
			'symbol'         => bw_2checkout_pricing_currency_symbol( $currency ),
			'symbol_side'    => bw_2checkout_pricing_symbol_side( $currency ),
			'users'          => $users,
			'unit'           => $unit,
			'amount'         => $total,
			'term_months'    => $this->term_months( $options ),
			'net'            => true,
			'unit_formatted' => $this->format( $unit, $currency ),
			'formatted'      => $this->format( $total, $currency ),
		);
	}

	/**
	 * Per-seat volume discount curve for mock mode (matches the shape seen in discovery).
	 *
	 * @param int $users User count.
	 * @return float
	 */
	protected function mock_volume_factor( $users ) {
		if ( $users <= 4 ) {
			return 1.0;
		}
		if ( $users <= 25 ) {
			return 0.978;
		}
		if ( $users <= 50 ) {
			return 0.955;
		}
		if ( $users <= 100 ) {
			return 0.933;
		}
		if ( $users <= 500 ) {
			return 0.911;
		}
		if ( $users <= 1000 ) {
			return 0.889;
		}
		return 0.867;
	}

	/**
	 * Billing length of the selected term, in months. Derived by scanning the selection for a
	 * term-style code ("1Y", "2Y", "3Y"), so no option-group name is hardcoded. Used to present
	 * the price monthly: monthly = total / (12 x term years).
	 *
	 * @param array $options Option-group => value.
	 * @return int Months (defaults to 12).
	 */
	protected function term_months( array $options ) {
		foreach ( $options as $value ) {
			if ( preg_match( '/^\s*(\d+)\s*Y\s*$/i', (string) $value, $m ) ) {
				return max( 1, (int) $m[1] ) * 12;
			}
		}
		return 12;
	}

	/**
	 * Convert a raw "number of users" count into its Scale option bucket code, e.g. 5 => 'users-5-25'.
	 * Matches the 7-tier CDS "Users" scale confirmed in the live pricingconfigurations response.
	 *
	 * @param int $count Raw user count.
	 * @return string
	 */
	protected function bucket_for_users( $count ) {
		$count = max( 1, (int) $count );
		if ( $count <= 4 ) {
			return 'users-1-4';
		}
		if ( $count <= 25 ) {
			return 'users-5-25';
		}
		if ( $count <= 50 ) {
			return 'users-26-50';
		}
		if ( $count <= 100 ) {
			return 'users-51-100';
		}
		if ( $count <= 500 ) {
			return 'users-101-500';
		}
		if ( $count <= 1000 ) {
			return 'users-501-1000';
		}
		return 'users-1001-2000';
	}

	/**
	 * Replace a raw numeric "users" value with its bucket code. Leaves already-coded values
	 * (from <select> options like 'basic', '1Y') untouched.
	 *
	 * @param array $options Option-group => value.
	 * @return array
	 */
	protected function normalize_options( array $options ) {
		if ( isset( $options['users'] ) && ctype_digit( (string) $options['users'] ) ) {
			$options['users'] = $this->bucket_for_users( (int) $options['users'] );
		}
		return $options;
	}

	/**
	 * Live price via the 2Checkout API.
	 *
	 * Reads the product's eStore-DEFAULT pricing configuration (Default === true) and matches
	 * the row whose OptionCodes equal the selection and whose Currency equals the request.
	 * The matched Amount is the PER-USER rate for that volume bucket — confirmed per-seat
	 * (not flat-per-tier) by rian's live checkout test (2026-07-13): incrementing 1→2 users
	 * increases the total by exactly the annual per-user rate. Total = Amount × user count.
	 *
	 * @param array  $product  Product config.
	 * @param array  $options  Option-group => value.
	 * @param string $currency ISO code.
	 * @return array|WP_Error
	 */
	protected function get_live_price( $product, array $options, $currency ) {
		$client = new BW_2Checkout_Pricing_Client();
		if ( ! $client->is_configured() ) {
			return new WP_Error( 'bw_2checkout_pricing_not_configured', __( 'API credentials not configured.', 'bw-2checkout-pricing' ) );
		}

		$configs = $client->get_pricing_configurations( isset( $product['product_code'] ) ? $product['product_code'] : '' );
		if ( is_wp_error( $configs ) ) {
			return $configs;
		}

		$default = null;
		foreach ( (array) $configs as $cfg ) {
			if ( ! empty( $cfg['Default'] ) ) {
				$default = $cfg;
				break;
			}
		}
		if ( ! $default ) {
			return new WP_Error( 'bw_2checkout_pricing_no_default', __( 'No eStore-default pricing configuration found.', 'bw-2checkout-pricing' ) );
		}

		$wanted = $this->normalize_options( $options );

		// The config stores USD rows (plus rare per-country overrides). Find the USD row matching
		// this option combination — that is the exact per-user rate for the selected volume tier.
		$usd_unit = null;
		foreach ( isset( $default['Prices']['Regular'] ) ? (array) $default['Prices']['Regular'] : array() as $row ) {
			if ( strtoupper( $row['Currency'] ?? '' ) !== 'USD' ) {
				continue;
			}
			$row_opts = array();
			foreach ( (array) ( $row['OptionCodes'] ?? array() ) as $oc ) {
				$row_opts[ $oc['Code'] ] = isset( $oc['Options'][0] ) ? $oc['Options'][0] : null;
			}
			$ok = true;
			foreach ( $wanted as $group => $value ) {
				if ( ! isset( $row_opts[ $group ] ) || (string) $row_opts[ $group ] !== (string) $value ) {
					$ok = false;
					break;
				}
			}
			if ( $ok ) {
				$usd_unit = (float) $row['Amount'];
				break;
			}
		}

		if ( null === $usd_unit ) {
			return new WP_Error( 'bw_2checkout_pricing_no_match', __( 'No live price found for that selection.', 'bw-2checkout-pricing' ) );
		}

		$currency = strtoupper( $currency );
		$users    = isset( $options['users'] ) ? max( 1, (int) $options['users'] ) : 1;
		$code     = isset( $product['product_code'] ) ? (string) $product['product_code'] : '';

		if ( 'USD' === $currency ) {
			$unit = $usd_unit;
		} else {
			// Non-USD prices are 2Checkout's runtime FX conversion of the USD price. Derive the
			// exact multiplier from an order-preview of this product in both currencies (the FX
			// rate is global, so the base-tier preview is sufficient) and apply it to the tier's
			// USD rate — this reproduces what checkout will charge.
			$cur_base = $client->order_preview_unit_net( $code, $wanted, $currency );
			$usd_base = $client->order_preview_unit_net( $code, $wanted, 'USD' );
			if ( is_wp_error( $cur_base ) ) {
				return $cur_base;
			}
			if ( is_wp_error( $usd_base ) || $usd_base <= 0 ) {
				return new WP_Error( 'bw_2checkout_pricing_fx_unavailable', __( 'Currency conversion unavailable.', 'bw-2checkout-pricing' ) );
			}
			$unit = round( $usd_unit * ( $cur_base / $usd_base ), 2 );
		}

		$total = round( $unit * $users, 2 );

		return array(
			'source'         => 'live',
			'currency'       => $currency,
			'symbol'         => bw_2checkout_pricing_currency_symbol( $currency ),
			'symbol_side'    => bw_2checkout_pricing_symbol_side( $currency ),
			'users'          => $users,
			'unit'           => $unit,
			'amount'         => $total,
			'term_months'    => $this->term_months( $options ),
			'net'            => ( 'NET' === strtoupper( $default['PriceType'] ?? 'NET' ) ),
			'unit_formatted' => $this->format( $unit, $currency ),
			'formatted'      => $this->format( $total, $currency ),
		);
	}

	/**
	 * Build a ConvertPlus buy-link from a selection.
	 *
	 * @param string $product_id Local product id.
	 * @param array  $options    Option-group => value.
	 * @param string $currency   ISO code.
	 * @return string
	 */
	public function build_buy_link( $product_id, array $options, $currency, $tpl = null ) {
		$products = bw_2checkout_pricing_get_products();
		if ( ! isset( $products[ $product_id ]['product_code'] ) ) {
			return '';
		}

		$base     = (string) bw_2checkout_pricing_get_setting( 'checkout_base', 'https://secure.2checkout.com/checkout/buy' );
		$merchant = (string) bw_2checkout_pricing_get_setting( 'checkout_merchant', '' );
		if ( '' === $merchant ) {
			$merchant = bw_2checkout_pricing_get_merchant_code();
		}
		// Per-block template override (e.g. COPERNIC-two-columns) falls back to the plugin setting.
		$tpl = ( null !== $tpl && '' !== $tpl )
			? (string) $tpl
			: (string) bw_2checkout_pricing_get_setting( 'checkout_tpl', 'default' );
		$prod = (string) $products[ $product_id ]['product_code'];

		// The "users" Scale option takes the RAW user count (e.g. users:5); the cart both selects
		// the volume tier and multiplies by it, so product qty stays 1. Mirrors the buy-link
		// construct Copernic provided (qty=1 & opt=...,users:5). NOTE: still to be confirmed with
		// one real test order that the resulting cart total matches the displayed price.
		$opt_parts = array();
		foreach ( $options as $group => $value ) {
			$opt_parts[] = rawurlencode( (string) $group ) . ':' . rawurlencode( (string) $value );
		}

		$query = 'merchant=' . rawurlencode( $merchant )
			. '&prod=' . rawurlencode( $prod )
			. '&qty=1'
			. '&tpl=' . rawurlencode( $tpl )
			. '&currency=' . rawurlencode( strtoupper( $currency ) );

		if ( ! empty( $opt_parts ) ) {
			$query .= '&opt=' . implode( ',', $opt_parts );
		}
		if ( bw_2checkout_pricing_get_setting( 'test_mode', true ) ) {
			$query .= '&DOTEST=1';
		}

		return $base . '?' . $query;
	}

	/**
	 * Format an amount for display. Rough — a proper locale/symbol map comes with the polish pass.
	 *
	 * @param float  $amount   Amount.
	 * @param string $currency ISO code.
	 * @return string
	 */
	protected function format( $amount, $currency ) {
		return number_format( (float) $amount, 2 ) . ' ' . strtoupper( $currency );
	}
}
