<?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';

	const THEME_TRANSLATIONS_TRANSIENT_KEY = 'the7_remote_api_theme_translations';

	/*
	 * Endpoint keys.
	 *
	 * The key value doubles as a back-compat wire contract: the override constant is
	 * derived from it as strtoupper( "dt_remote_{$key}" ) (e.g. DT_REMOTE_API_REGISTER_URL),
	 * and the same value is passed to the `the7_remote_api_endpoint_url` filter. Keep the
	 * legacy `api_*` spelling of each value stable; only the constant names are free to change.
	 */
	const ENDPOINT_REGISTER                     = 'api_register_url';
	const ENDPOINT_DE_REGISTER                  = 'api_de_register_url';
	const ENDPOINT_THEME_INFO                   = 'api_theme_info_url';
	const ENDPOINT_THEME_TRANSLATIONS           = 'api_theme_translations_url';
	const ENDPOINT_DOWNLOAD_THEME               = 'api_download_theme_url';
	const ENDPOINT_PLUGINS_LIST                 = 'api_plugins_list_url';
	const ENDPOINT_DOWNLOAD_PLUGIN              = 'api_download_plugin_url';
	const ENDPOINT_VERIFY_PURCHASE_CODE         = 'api_verify_purchase_code';
	const ENDPOINT_CRITICAL_ALERT               = 'api_critical_alert';
	const ENDPOINT_BANNERS                      = 'api_banners_url';
	const ENDPOINT_FEATURE_ACCESS               = 'api_feature_access_url';
	const ENDPOINT_DEMO_CONTENT_LIST            = 'api_demo_content_list_url';
	const ENDPOINT_DEMO_CONTENT_DOWNLOAD        = 'api_demo_content_download_url';
	const ENDPOINT_ELEMENTOR_TEMPLATES_LIST     = 'api_elementor_templates_list_url';
	const ENDPOINT_ELEMENTOR_TEMPLATES_DOWNLOAD = 'api_elementor_templates_download_url';

	/**
	 * Endpoint key => default URL. Populated in the constructor.
	 *
	 * @var array
	 */
	protected $endpoints = [];

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

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

	/**
	 * The7_Remote_API constructor.
	 *
	 * @param $code
	 */
	public function __construct( $code ) {
		$this->endpoints = [
			self::ENDPOINT_REGISTER                     => 'https://repo.the7.io/register.php',
			self::ENDPOINT_DE_REGISTER                  => 'https://repo.the7.io/de_register.php',
			self::ENDPOINT_THEME_INFO                   => 'https://repo.the7.io/api/v2/theme/info.json',
			self::ENDPOINT_THEME_TRANSLATIONS           => 'https://repo.the7.io/api/v2/theme/translations/list.json',
			self::ENDPOINT_DOWNLOAD_THEME               => 'https://repo.the7.io/api/v2/theme/download',
			self::ENDPOINT_PLUGINS_LIST                 => 'https://repo.the7.io/plugins/list.json',
			self::ENDPOINT_DOWNLOAD_PLUGIN              => 'https://repo.the7.io/plugins/download.php',
			self::ENDPOINT_VERIFY_PURCHASE_CODE         => 'https://repo.the7.io/verify-code.php',
			self::ENDPOINT_CRITICAL_ALERT               => 'https://repo.the7.io/get-alert/',
			self::ENDPOINT_BANNERS                      => 'https://repo.the7.io/banners/',
			self::ENDPOINT_FEATURE_ACCESS               => 'https://repo.the7.io/api/v2/feature-access',
			self::ENDPOINT_DEMO_CONTENT_LIST            => 'https://repo.the7.io/demo-content/list.json',
			self::ENDPOINT_DEMO_CONTENT_DOWNLOAD        => 'https://repo.the7.io/demo-content/download.php',
			self::ENDPOINT_ELEMENTOR_TEMPLATES_LIST     => 'https://repo.the7.io/elementor-templates/list.json',
			self::ENDPOINT_ELEMENTOR_TEMPLATES_DOWNLOAD => 'https://repo.the7.io/elementor-templates/download.php',
		];

		$this->code = $code;
	}

	/**
	 * Resolve an endpoint URL: default, then back-compat override constant, then runtime filter.
	 *
	 * @param string $key Endpoint key (one of the ENDPOINT_* constants).
	 *
	 * @return string
	 */
	public function get_endpoint_url( $key ) {
		$url = $this->endpoints[ $key ] ?? '';

		$constant = strtoupper( "dt_remote_{$key}" );
		if ( defined( $constant ) && constant( $constant ) ) {
			$url = constant( $constant );
		}

		/**
		 * Filter a The7 remote API endpoint URL at runtime.
		 *
		 * Lets a plugin repoint requests (e.g. at the dev repo) without defining constants.
		 *
		 * @param string $url The resolved endpoint URL.
		 * @param string $key Endpoint key (one of the The7_Remote_API::ENDPOINT_* values).
		 */
		return apply_filters( 'the7_remote_api_endpoint_url', $url, $key );
	}

	/**
	 * @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->get_endpoint_url( self::ENDPOINT_REGISTER ), $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->get_endpoint_url( self::ENDPOINT_DE_REGISTER ), $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 ( ! 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.
	 *
	 * Reports the license `code` and, as of v15, the running `version` (THE7_VERSION)
	 * so the repo can return a version-aware verdict (mirroring check_theme_update).
	 * The version is sent unconditionally; the client applies no version logic itself.
	 *
	 * @return array
	 */
	public function get_verify_code_request_data() {
		return [
			'method' => 'GET',
			'url'    => $this->build_api_url(
				$this->get_endpoint_url( self::ENDPOINT_VERIFY_PURCHASE_CODE ),
				[
					'code'    => $this->code,
					'version' => THE7_VERSION,
				]
			),
		];
	}

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

		return strpos( $this->get_endpoint_url( self::ENDPOINT_DOWNLOAD_PLUGIN ), $host ) !== false;
	}

	/**
	 * Check theme update info.
	 *
	 * Reports the license `code` and running `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/version and must not
	 * be wrapped in an unkeyed cache.
	 *
	 * @return array|WP_Error
	 */
	public function check_theme_update() {
		return $this->remote_get_json(
			$this->build_api_url(
				$this->get_endpoint_url( self::ENDPOINT_THEME_INFO ),
				[
					'code'    => $this->code,
					'version' => THE7_VERSION,
				]
			)
		);
	}

	/**
	 * @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->get_endpoint_url( self::ENDPOINT_THEME_INFO ) );
			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 );
	}

	/**
	 * Fetch available theme translation language packs from the repo.
	 *
	 * The manifest is anonymous: the purchase code is never sent on the request
	 * (it mirrors info.json). The repo filters the returned packs by the locales
	 * we send, mirroring how WordPress core's wp_update_themes() delegates locale
	 * matching to the server. The repo returns code-less package URLs, so the
	 * purchase code is appended to each package here before WordPress core fetches
	 * it. Result is cached and failures degrade to an empty list.
	 *
	 * @param array $locales                Locales the site is interested in.
	 * @param array $installed_translations Installed theme translations keyed by text domain.
	 *
	 * @return array List of language pack entries (possibly empty).
	 */
	public function get_theme_translations( array $locales, array $installed_translations = [] ) {
		$locales = array_values( array_unique( array_filter( $locales ) ) );
		if ( ! $locales ) {
			return [];
		}

		$cached = get_transient( self::THEME_TRANSLATIONS_TRANSIENT_KEY );
		if ( is_array( $cached ) ) {
			return $cached;
		}

		$args = [
			'timeout' => 30,
			'body'    => [
				'version' => THE7_VERSION,
				'locales' => wp_json_encode( $locales ),
			],
		];
		if ( $installed_translations ) {
			$args['body']['installed_translations'] = wp_json_encode( $installed_translations );
		}

		$response = wp_remote_post( $this->get_endpoint_url( self::ENDPOINT_THEME_TRANSLATIONS ), $args );

		$translations = [];
		if ( ! is_wp_error( $response ) && '200' == wp_remote_retrieve_response_code( $response ) ) {
			$json = json_decode( wp_remote_retrieve_body( $response ), true );
			if ( isset( $json['translations'] ) && is_array( $json['translations'] ) ) {
				$translations = $this->append_code_to_translation_packages( $json['translations'] );
			}
		}

		set_transient( self::THEME_TRANSLATIONS_TRANSIENT_KEY, $translations, MINUTE_IN_SECONDS * 15 );

		return $translations;
	}

	/**
	 * Append the purchase code to each language pack's code-less package URL.
	 *
	 * The repo emits anonymous package URLs; WordPress core fetches them verbatim,
	 * so the code is appended here as the download credential. Entries are otherwise
	 * passed through untouched; entries without a package are left as-is.
	 *
	 * @param array $translations Language pack entries from the repo.
	 *
	 * @return array
	 */
	protected function append_code_to_translation_packages( array $translations ) {
		if ( ! $this->code ) {
			return $translations;
		}

		foreach ( $translations as &$translation ) {
			if ( is_array( $translation ) && ! empty( $translation['package'] ) ) {
				$translation['package'] = $this->build_api_url( $translation['package'], [ 'code' => $this->code ] );
			}
		}
		unset( $translation );

		return $translations;
	}

	/**
	 * 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_transient( self::THEME_TRANSLATIONS_TRANSIENT_KEY );
		delete_site_transient( 'update_themes' );

		do_action( 'the7_remote_api_flush_repo_lists_cache' );
	}

	/**
	 * Return theme download url.
	 *
	 * Always carries the installed `THE7_VERSION` so the server knows which build is running.
	 *
	 * @param string $archive_version Optional archived theme version to download.
	 *
	 * @return string
	 */
	public function get_theme_download_url( $archive_version = '' ) {
		$query_args = [
			'code'    => $this->code,
			'version' => THE7_VERSION,
		];

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

		return $this->build_api_url( $this->get_endpoint_url( self::ENDPOINT_DOWNLOAD_THEME ), $query_args );
	}

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

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

	/**
	 * Return critical alert body as array or WP_Error on error.
	 *
	 * Carries the Installed Build (`client_version`) alongside the licence so the repo can
	 * target an alert at the builds it actually affects instead of broadcasting it to every
	 * site. The alert is emailed to the admin, so a broadcast that does not apply to the
	 * running build is worse than no alert at all.
	 *
	 * `client_version` is the wire name for the Installed Build (see the Installed Build
	 * section of CONTEXT.md), deliberately not the `version` carried by the older banners /
	 * feature-access calls: that name meant *requested build* on the theme path through
	 * 14.4.2, and that ambiguity is what the `client_version` name exists to retire.
	 *
	 * Nothing here is cached, so the per-build response cannot be served to the wrong build.
	 *
	 * @return array|WP_Error
	 */
	public function get_critical_alert() {
		return $this->remote_get_json(
			$this->build_api_url(
				$this->get_endpoint_url( self::ENDPOINT_CRITICAL_ALERT ),
				[
					'code'           => $this->code,
					'client_version' => THE7_VERSION,
				]
			)
		);
	}

	/**
	 * 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( $timeout = 30 ) {
		return $this->remote_get_json(
			$this->build_api_url(
				$this->get_endpoint_url( self::ENDPOINT_FEATURE_ACCESS ),
				[
					'code'    => $this->code,
					'version' => THE7_VERSION,
				]
			),
			$timeout
		);
	}

	/**
	 * 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->get_endpoint_url( self::ENDPOINT_BANNERS ), $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, $timeout = 30 ) {
		$response = wp_remote_get( $url, [ 'timeout' => (int) $timeout ] );

		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->get_endpoint_url( self::ENDPOINT_DEMO_CONTENT_LIST ),
			[
				'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 = "https://dl.gpltimes.com/file/gpltimes/the7/demos/{$id}.zip";

		$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->get_endpoint_url( self::ENDPOINT_ELEMENTOR_TEMPLATES_LIST );
	}

	/**
	 * 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->get_endpoint_url( self::ENDPOINT_ELEMENTOR_TEMPLATES_DOWNLOAD ),
			[
				'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 ] ?? '';
	}
}
