<?php
/**
 * The7 remote api.
 *
 * @package The7
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class The7_Remote_API
 */
class The7_Remote_API {

	const THEME_AVAILABLE_VERSIONS_TRANSIENT_KEY = 'the7_remote_api_theme_available_versions';

	/**
	 * @var string
	 */
	protected $api_register_url = 'https://repo.the7.io/register.php';

	/**
	 * @var string
	 */
	protected $api_de_register_url = 'https://repo.the7.io/de_register.php';

	/**
	 * @var string
	 */
	protected $api_theme_info_url = 'https://repo.the7.io/theme/info.json';

	/**
	 * @var string
	 */
	protected $api_download_theme_url = 'https://repo.the7.io/theme/download.php';

	/**
	 * Upgrade Channel info endpoint (newer line: version + requirements).
	 *
	 * @var string
	 */
	protected $api_upgrade_theme_info_url = 'https://repo.the7.io/api/v2/theme/info.json';

	/**
	 * Upgrade Channel download endpoint (newer line package).
	 *
	 * @var string
	 */
	protected $api_upgrade_download_theme_url = 'https://repo.the7.io/api/v2/theme/download';

	/**
	 * @var string
	 */
	protected $api_plugins_list_url = 'https://repo.the7.io/plugins/list.json';

	/**
	 * @var string
	 */
	protected $api_download_plugin_url = 'https://repo.the7.io/plugins/download.php';

	/**
	 * @var string
	 */
	protected $api_verify_purchase_code = 'https://repo.the7.io/verify-code.php';

	/**
	 * @var string
	 */
	protected $api_critical_alert = 'https://repo.the7.io/get-alert/';

	/**
	 * @var string
	 */
	protected $api_banners_url = 'https://repo.the7.io/banners/';

	/**
	 * @var string
	 */
	protected $api_feature_access_url = 'https://repo.the7.io/api/v2/feature-access';

	/**
	 * @var string
	 */
	protected $api_demo_content_list_url = 'https://repo.the7.io/demo-content/list.json';

	/**
	 * @var string
	 */
	protected $api_demo_content_download_url = 'https://repo.the7.io/demo-content/download.php';

	/**
	 * @var string
	 */
	protected $api_elementor_templates_list_url = 'https://repo.the7.io/elementor-templates/list.json';

	/**
	 * @var string
	 */
	protected $api_elementor_templates_download_url = 'https://repo.the7.io/elementor-templates/download.php';

	/**
	 * @var array
	 */
	protected $strings = [];

	/**
	 * @var string
	 */
	protected $code = '';

	/**
	 * The7_Remote_API constructor.
	 *
	 * @param $code
	 */
	public function __construct( $code ) {
		$props = array_keys( get_object_vars( $this ) );
		$props = array_filter(
			$props,
			function ( $prop ) {
				return strpos( $prop, 'api_' ) === 0;
			}
		);
		foreach ( $props as $prop ) {
			$constant = strtoupper( "dt_remote_{$prop}" );
			if ( defined( $constant ) && constant( $constant ) ) {
				$this->$prop = constant( $constant );
			}
		}

		$this->code = $code;
	}

	/**
	 * @return array|WP_Error
	 */
	public function register_purchase_code() {
		$args                    = $this->get_purchase_code_post_args();
		$args['body']['version'] = THE7_VERSION;

		$response = wp_remote_post( $this->api_register_url, $args );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		if ( '200' != $response_code ) {
			return new WP_Error( 'bad_request', $response_code . ': ' . $this->get_string( 'bad_request' ) );
		}

		$code_check = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( isset( $code_check['errors'] ) ) {
			return new WP_Error( 'remote_api_error', $code_check['errors'] );
		}

		if ( empty( $code_check['success'] ) ) {
			return new WP_Error( 'invalid_response', $this->get_string( 'invalid_response' ) );
		}

		return $code_check;
	}

	/**
	 * @return array|bool|WP_Error
	 */
	public function de_register_purchase_code() {
		$response = wp_remote_post( $this->api_de_register_url, $this->get_purchase_code_post_args() );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		if ( '200' != wp_remote_retrieve_response_code( $response ) ) {
			return new WP_Error( 'bad_request', $this->get_string( 'bad_request' ) );
		}

		$code_check = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( isset( $code_check['errors'] ) ) {
			return new WP_Error( 'remote_api_error', $code_check['errors'] );
		}

		if ( empty( $code_check['success'] ) ) {
			return new WP_Error( 'invalid_response', $this->get_string( 'invalid_response' ) );
		}

		return true;
	}

	/**
	 * @return array
	 */
	protected function get_purchase_code_post_args() {
		return [
			'timeout' => 30,
			'body'    => [
				'code' => $this->code,
			],
		];
	}

	/**
	 * Verify current purchase code against repo.
	 *
	 * @return array|WP_Error
	 */
	public function get_verify_code_response() {
		$request_data = $this->get_verify_code_request_data();
		$response     = wp_remote_get( $request_data['url'], [ 'timeout' => 30 ] );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		$response_body = wp_remote_retrieve_body( $response );
		if ( '200' != $response_code ) {
			return new WP_Error(
				'bad_request',
				$response_code . ': ' . $this->get_string( 'bad_request' ),
				[
					'body'          => $response_body,
					'response_code' => $response_code,
				]
			);
		}

		$json = json_decode( $response_body, true );

		if ( empty( $json ) || ! is_array( $json ) ) {
			return new WP_Error(
				'invalid_response',
				$this->get_string( 'invalid_response' ),
				[
					'body'          => $response_body,
					'response_code' => $response_code,
				]
			);
		}

		return $json;
	}

	/**
	 * Return request data used for purchase code verification.
	 *
	 * @return array
	 */
	public function get_verify_code_request_data() {
		return [
			'method' => 'GET',
			'url'    => $this->build_api_url(
				$this->api_verify_purchase_code,
				[
					'code' => $this->code,
				]
			),
		];
	}

	public function is_api_url( $url ) {
		$host = @parse_url( $url, PHP_URL_HOST );

		return strpos( $this->api_download_plugin_url, $host ) !== false;
	}

	/**
	 * Check theme update info.
	 *
	 * Reports the license `code` and the Installed Build (`client_version`) so the repo can
	 * channel-resolve the advertised payload (Legacy vs Current, or the blocked-license
	 * nudge) and record the Live Sites heartbeat. The response therefore varies by
	 * code/client_version and must not be wrapped in an unkeyed cache.
	 *
	 * The legacy `version` arg is deliberately not sent: it meant *requested build* through
	 * 14.4.2 and *installed build* from 14.4.3, so the server cannot trust it. See
	 * docs/adr/0010-client-version-and-requested-version.md.
	 *
	 * @return array|WP_Error
	 */
	public function check_theme_update() {
		return $this->remote_get_json(
			$this->build_api_url( $this->api_theme_info_url, $this->get_base_query_args() )
		);
	}

	/**
	 * The pair of args that travels on every licensed theme endpoint call: the licence and
	 * the Installed Build.
	 *
	 * Single-sourced deliberately — the whole point of the `client_version` /
	 * `requested_version` split is that these two never drift apart again across call sites.
	 * See docs/adr/0010-client-version-and-requested-version.md.
	 *
	 * @return array
	 */
	protected function get_base_query_args() {
		return [
			'code'           => $this->code,
			'client_version' => THE7_VERSION,
		];
	}

	/**
	 * Archived builds the repo is willing to serve, for the Dev Mode Re-Install selector.
	 *
	 * Called only from Dev Mode (the Re-Install tab view and its POST handler), so the
	 * request is unconditionally marked `dev_reinstall` — this info read is never part of
	 * the ordinary update poll, which goes through `check_theme_update()`.
	 *
	 * @return array
	 */
	public function get_available_theme_versions() {
		$versions = get_transient( self::THEME_AVAILABLE_VERSIONS_TRANSIENT_KEY );
		if ( $versions === false ) {
			$versions = [];
			$data     = $this->remote_get_json(
				$this->build_api_url(
					$this->api_theme_info_url,
					$this->get_base_query_args() + [ 'dev_reinstall' => '1' ]
				)
			);
			if ( ! is_wp_error( $data ) && isset( $data['available_versions'] ) && is_array( $data['available_versions'] ) ) {
				$versions = $data['available_versions'];
			}
			set_transient( self::THEME_AVAILABLE_VERSIONS_TRANSIENT_KEY, $versions, MINUTE_IN_SECONDS * 15 );
		}

		return array_filter( (array) $versions );
	}

	/**
	 * Flush cached repo lists fetched through The7 remote API.
	 */
	public static function flush_repo_lists_cache() {
		delete_transient( self::THEME_AVAILABLE_VERSIONS_TRANSIENT_KEY );
		delete_site_transient( 'update_themes' );

		do_action( 'the7_remote_api_flush_repo_lists_cache' );
	}

	/**
	 * Return theme download url.
	 *
	 * Two distinct builds travel on this URL and must never share a name again:
	 *  - `client_version` — the Installed Build (`THE7_VERSION`). The server treats this as an
	 *    entitlement input (Legacy Maintenance Allowance, v15 cohort), so it is always sent.
	 *  - `requested_version` — the Requested Build, the business input the server resolves.
	 *
	 * Omitting `requested_version` fails open to the latest build of the current line. The
	 * legacy `version` / `v` args are no longer sent; see
	 * docs/adr/0010-client-version-and-requested-version.md.
	 *
	 * @param string $requested_version Optional exact build to be served.
	 * @param bool   $dev_reinstall     Whether this is a Dev Mode Re-Install request.
	 *
	 * @return string
	 */
	public function get_theme_download_url( $requested_version = '', $dev_reinstall = false ) {
		$query_args = $this->get_base_query_args();

		if ( $requested_version ) {
			$query_args['requested_version'] = $requested_version;
		}

		if ( $dev_reinstall ) {
			$query_args['dev_reinstall'] = '1';
		}

		return $this->build_api_url( $this->api_download_theme_url, $query_args );
	}

	/**
	 * Return the Upgrade Channel download url (always latest build of the newer line).
	 *
	 * `client_version` is the Installed Build (the currently running `THE7_VERSION`), which the
	 * server reads as an entitlement input rather than plain telemetry.
	 * There is deliberately no `requested_version`: this channel always serves the latest
	 * build of the newer line and the client does not choose.
	 *
	 * @return string
	 */
	public function get_theme_upgrade_download_url() {
		return $this->build_api_url( $this->api_upgrade_download_theme_url, $this->get_base_query_args() );
	}

	/**
	 * Fetch the Upgrade Channel info document (newer line version + requirements).
	 *
	 * The endpoint selects the current upgrade line when no source version is provided.
	 *
	 * @return array|WP_Error
	 */
	public function get_theme_upgrade_info() {
		return $this->remote_get_json( $this->api_upgrade_theme_info_url );
	}

	/**
	 * Get plugins list.
	 *
	 * @return array|WP_Error
	 */
	public function check_plugins_list() {
		return $this->remote_get_json( $this->api_plugins_list_url );
	}

	/**
	 * @param string $slug
	 *
	 * @return string
	 */
	public function get_plugin_download_url( $slug ) {
		return $this->build_api_url(
			$this->api_download_plugin_url,
			[
				'code' => $this->code,
				'item' => $slug,
			]
		);
	}

	/**
	 * Return critical alert body as array or WP_Error on error.
	 *
	 * @return array|WP_Error
	 */
	public function get_critical_alert() {
		return $this->remote_get_json(
			$this->build_api_url(
				$this->api_critical_alert,
				[
					'code' => $this->code,
				]
			)
		);
	}

	/**
	 * Fetch remote banner payload for the held banner id.
	 *
	 * @param string $id Banner id currently held by the site (empty when none).
	 *
	 * @return array|WP_Error
	 */
	public function get_banner( $id = '' ) {
		return $this->remote_get_json( $this->get_banner_request_url( $id ) );
	}

	/**
	 * Fetch the Feature Access verdict document for the current license.
	 *
	 * @return array|WP_Error
	 */
	public function get_feature_access() {
		return $this->remote_get_json(
			$this->build_api_url(
				$this->api_feature_access_url,
				[
					'code'    => $this->code,
					'version' => THE7_VERSION,
				]
			)
		);
	}

	/**
	 * Return banners API request URL.
	 *
	 * @param string $id Banner id currently held by the site (empty when none).
	 *
	 * @return string
	 */
	public function get_banner_request_url( $id = '' ) {
		$query_args = [
			'version'  => THE7_VERSION,
			'platform' => the7_get_license_platform(),
			'code'     => $this->code,
		];

		if ( '' !== (string) $id ) {
			$query_args['id'] = $id;
		}

		return $this->build_api_url( $this->api_banners_url, $query_args );
	}

	/**
	 * Build API request URL with encoded query args.
	 *
	 * @param string $url        API endpoint URL.
	 * @param array  $query_args Query args.
	 *
	 * @return string
	 */
	protected function build_api_url( $url, array $query_args = [] ) {
		if ( ! $query_args ) {
			return $url;
		}

		return add_query_arg( urlencode_deep( $query_args ), $url );
	}

	/**
	 * @param string $url
	 *
	 * @return array|WP_Error
	 */
	protected function remote_get_json( $url ) {
		$response = wp_remote_get( $url, [ 'timeout' => 30 ] );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		if ( '200' != wp_remote_retrieve_response_code( $response ) ) {
			return new WP_Error( 'bad_request', $this->get_string( 'bad_request' ) );
		}

		$json = json_decode( wp_remote_retrieve_body( $response ), true );

		// A legit, fully-granted response can be an empty JSON object/array (e.g. `{}`),
		// which decodes to `array()` and is falsy under `empty()`. Only a non-array decode
		// (invalid JSON, or JSON that isn't an object/array) indicates a real failure.
		if ( ! is_array( $json ) ) {
			return new WP_Error( 'invalid_response', $this->get_string( 'invalid_response' ) );
		}

		return $json;
	}

	/**
	 * @return array|bool
	 */
	public function get_demos_list() {
		$response = wp_remote_get(
			$this->api_demo_content_list_url,
			[
				'timeout'    => 30,
				'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . network_site_url(),
			]
		);

		$response_code = wp_remote_retrieve_response_code( $response );

		if ( 200 !== $response_code || is_wp_error( $response ) ) {
			return false;
		}

		$response_body = wp_remote_retrieve_body( $response );

		return json_decode( $response_body, true );
	}

	/**
	 * This method try to download demo content with $id and put it to $target_path.
	 * Creates $target_path dir if it is not exists. Dummy content zip archive will be unzipped.
	 *
	 * @param string $id
	 * @param string $target_dir
	 * @param string $req_url
	 *
	 * @return string|WP_Error Path where dummy content files is located on success or WP_Error on failure.
	 */
	public function download_demo( $id, $target_dir, $req_url = '' ) {
		/**
		 * @var $wp_filesystem WP_Filesystem_Base
		 */ global $wp_filesystem;

		if ( ! $wp_filesystem && ! WP_Filesystem() ) {
			return new WP_Error( 'fs_unavailable', $this->get_string( 'fs_unavailable' ) );
		}

		if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) {
			return $wp_filesystem->errors;
		}

		$request_url_args = [
			'item'    => $id,
			'code'    => $this->code,
			'req_url' => $req_url,
		];
		$request_url      = $this->build_api_url( $this->api_demo_content_download_url, $request_url_args );
		$remote_response  = wp_safe_remote_get(
			$request_url,
			[
				'timeout'    => 300,
				'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . network_site_url(),
			]
		);

		if ( is_wp_error( $remote_response ) ) {
			return $remote_response;
		}

		$response_code = (int) wp_remote_retrieve_response_code( $remote_response );

		if ( 402 === $response_code ) {
			return $this->feature_locked_error( $remote_response );
		}

		if ( ! is_array( $remote_response ) || 200 !== $response_code ) {
			return new WP_Error(
				'download_failed',
				sprintf( $this->get_string( 'download_failed' ), $response_code )
			);
		}

		wp_mkdir_p( $target_dir );

		$file_content  = wp_remote_retrieve_body( $remote_response );
		$zip_file_name = trailingslashit( $target_dir ) . "{$id}.zip";
		$wp_filesystem->put_contents( $zip_file_name, $file_content );

		$unzip_result = unzip_file( $zip_file_name, $target_dir );
		if ( is_wp_error( $unzip_result ) ) {
			return $unzip_result;
		}

		$dummy_dir = trailingslashit( $target_dir ) . $id;

		if ( ! is_dir( $dummy_dir ) ) {
			return new WP_Error( 'fs_no_folder', sprintf( $this->get_string( 'fs_no_folder' ), $dummy_dir ) );
		}

		return $dummy_dir;
	}

	/**
	 * Build the WP_Error for a 402 Feature Lock: surface the server message inline and
	 * schedule a non-blocking ambient refetch so the Feature Gate corrects itself.
	 *
	 * @param array $remote_response Raw wp_remote_get response.
	 *
	 * @return WP_Error
	 */
	protected function feature_locked_error( $remote_response ) {
		$body    = json_decode( wp_remote_retrieve_body( $remote_response ), true );
		$message = '';

		if ( is_array( $body ) && isset( $body['message'] ) ) {
			$message = wp_kses_post( (string) $body['message'] );
		}

		if ( class_exists( 'The7_Feature_Access' ) ) {
			The7_Feature_Access::schedule_single_fetch();
		}

		return new WP_Error( 'the7_feature_locked', $message );
	}

	/**
	 * Returns Elementor templates list url.
	 *
	 * @return string
	 */
	public function get_elementor_templates_list_url(): string {
		return $this->api_elementor_templates_list_url;
	}

	/**
	 * Returns Elementor template download url.
	 *
	 * @param string $id Template id.
	 *
	 * @return string
	 */
	public function get_elementor_template_download_url( $id ): string {
		return $this->build_api_url(
			$this->api_elementor_templates_download_url,
			[
				'code' => $this->code,
				'item' => $id,
			]
		);
	}

	/**
	 * @param string $key String key.
	 *
	 * @return string
	 */
	protected function get_string( $key ) {
		if ( empty( $this->strings ) ) {
			$this->strings['fs_unavailable'] = __(
				'Failed to access the file system. You may try adjusting the permissions for the uploads folder.',
				'the7mk2'
			);
			/* translators: %s: directory name */
			$this->strings['fs_no_folder'] = __( 'Unable to find cache folder (%s).', 'the7mk2' );
			/* translators: %s: the7 server http responce code */
			$this->strings['download_failed']  = __( 'Download failed. The7 server http responce code is %s.', 'the7mk2' );
			$this->strings['bad_request']      = __( 'Bad request.', 'the7mk2' );
			$this->strings['invalid_response'] = __( 'Invalid response.', 'the7mk2' );
		}

		return $this->strings[ $key ] ?? '';
	}
}
