<?php
/**
 * A Terms of Service class for Jetpack.
 *
 * @package automattic/jetpack-licensing
 */

namespace Automattic\Jetpack;

use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Jetpack;
use Jetpack_IXR_ClientMulticall;
use Jetpack_Options;
use WP_Error;

/**
 * Class Licensing.
 * Helper class that is responsible for attaching licenses to the current site.
 *
 * @since 9.0.0
 */
class Licensing {
	/**
	 * Name of the WordPress option that holds all known Jetpack licenses.
	 *
	 * @const string
	 */
	const LICENSES_OPTION_NAME = 'jetpack_licenses';

	/**
	 * Name of the WordPress transient that holds the last license attaching error, if any.
	 *
	 * @const string
	 */
	const ERROR_TRANSIENT_NAME = 'jetpack_licenses_error';

	/**
	 * Holds the singleton instance of this class.
	 *
	 * @var self
	 */
	protected static $instance = false;

	/**
	 * Singleton.
	 *
	 * @static
	 */
	public static function instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Initialize.
	 *
	 * @return void
	 */
	public function initialize() {
		add_action( 'update_option_' . self::LICENSES_OPTION_NAME, array( $this, 'attach_stored_licenses' ) );
		add_action( 'jetpack_authorize_ending_authorized', array( $this, 'attach_stored_licenses_on_connection' ) );
	}

	/**
	 * Get Jetpack connection manager instance.
	 *
	 * @return Connection_Manager
	 */
	protected function connection() {
		static $connection;

		if ( null === $connection ) {
			$connection = new Connection_Manager();
		}

		return $connection;
	}

	/**
	 * Get the last license attach request error that has occurred, if any.
	 *
	 * @return string Human-readable error message or an empty string.
	 */
	public function last_error() {
		return Jetpack_Options::get_option( 'licensing_error', '' );
	}

	/**
	 * Log an error to be surfaced to the user at a later time.
	 *
	 * @param string $error Human-readable error message.
	 * @return void
	 */
	public function log_error( $error ) {
		$substr = function_exists( 'mb_substr' ) ? 'mb_substr' : 'substr';
		Jetpack_Options::update_option( 'licensing_error', $substr( $error, 0, 1024 ) );
	}

	/**
	 * Get all stored licenses.
	 *
	 * @return string[] License keys.
	 */
	public function stored_licenses() {
		$licenses = (array) get_option( self::LICENSES_OPTION_NAME, array() );
		$licenses = array_filter( $licenses, 'is_scalar' );
		$licenses = array_map( 'strval', $licenses );
		$licenses = array_filter( $licenses );

		return $licenses;
	}

	/**
	 * Make an authenticated WP.com XMLRPC multicall request to attach the provided license keys.
	 *
	 * @param string[] $licenses License keys to attach.
	 * @return Jetpack_IXR_ClientMulticall
	 */
	protected function attach_licenses_request( array $licenses ) {
		$xml = new Jetpack_IXR_ClientMulticall();

		foreach ( $licenses as $license ) {
			$xml->addCall( 'jetpack.attachLicense', $license );
		}

		$xml->query();

		return $xml;
	}

	/**
	 * Attach the given licenses.
	 *
	 * @param string[] $licenses Licenses to attach.
	 * @return array|WP_Error Results for each license (which may include WP_Error instances) or a WP_Error instance.
	 */
	public function attach_licenses( array $licenses ) {
		if ( ! $this->connection()->is_active() ) {
			return new WP_Error( 'not_connected', __( 'Jetpack is not connected.', 'jetpack' ) );
		}

		if ( empty( $licenses ) ) {
			return array();
		}

		$xml = $this->attach_licenses_request( $licenses );

		if ( $xml->isError() ) {
			$error = new WP_Error( 'request_failed', __( 'License attach request failed.', 'jetpack' ) );
			$error->add( $xml->getErrorCode(), $xml->getErrorMessage() );
			return $error;
		}

		$results = array_map(
			function ( $response ) {
				if ( isset( $response['faultCode'] ) || isset( $response['faultString'] ) ) {
					return new WP_Error( $response['faultCode'], $response['faultString'] );
				}

				return $response;
			},
			(array) $xml->getResponse()
		);

		return $results;
	}

	/**
	 * Attach all stored licenses.
	 *
	 * @return array|WP_Error Results for each license (which may include WP_Error instances) or a WP_Error instance.
	 */
	public function attach_stored_licenses() {
		$licenses = $this->stored_licenses();
		$results  = $this->attach_licenses( $licenses );

		if ( is_wp_error( $results ) ) {
			if ( 'request_failed' === $results->get_error_code() ) {
				$this->log_error(
					__( 'Failed to attach your Jetpack license(s). Please try reconnecting Jetpack.', 'jetpack' )
				);
			}

			return $results;
		}

		$failed = array();

		foreach ( $results as $index => $result ) {
			if ( isset( $licenses[ $index ] ) && is_wp_error( $result ) ) {
				$failed[] = $licenses[ $index ];
			}
		}

		if ( ! empty( $failed ) ) {
			$this->log_error(
				sprintf(
					/* translators: %s is a comma-separated list of license keys. */
					__( 'The following Jetpack licenses are invalid, already in use or revoked: %s', 'jetpack' ),
					implode( ', ', $failed )
				)
			);
		}

		return $results;
	}

	/**
	 * Attach all stored licenses during connection flow for the connection owner.
	 *
	 * @return void
	 */
	public function attach_stored_licenses_on_connection() {
		if ( $this->connection()->is_connection_owner() ) {
			$this->attach_stored_licenses();
		}
	}
}
