<?php
/**
 * Native WordPress updates for Wooden Banana Studio.
 *
 * The public release manifest is intentionally small and static. WordPress
 * performs its normal update check, while this class supplies the release
 * metadata and verifies the downloaded ZIP before installation.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

final class MPAB_Updater {
	const UPDATE_URI   = 'https://wooden-banana-studio-updates.pages.dev/';
	const MANIFEST_URL = 'https://wooden-banana-studio-updates.pages.dev/manifest.json';
	const SLUG         = 'mark-pacura-album-builder';

	/** @var array<string,mixed>|false|null */
	private $manifest = null;

	public static function register() {
		$updater = new self();
		add_filter( 'update_plugins_wooden-banana-studio-updates.pages.dev', array( $updater, 'check_for_update' ), 10, 4 );
		add_filter( 'plugins_api', array( $updater, 'plugin_information' ), 20, 3 );
		add_filter( 'upgrader_pre_download', array( $updater, 'verify_package' ), 20, 4 );
	}

	/**
	 * Supply update data to WordPress for this plugin only.
	 *
	 * @param array|false $update      Existing update data.
	 * @param array       $plugin_data Parsed plugin headers.
	 * @param string      $plugin_file Plugin basename.
	 * @param string[]    $locales     Requested locales.
	 * @return array|false
	 */
	public function check_for_update( $update, $plugin_data, $plugin_file, $locales ) {
		unset( $plugin_data, $locales );

		if ( plugin_basename( MPAB_FILE ) !== $plugin_file ) {
			return $update;
		}

		$manifest = $this->get_manifest();
		if ( ! $manifest || version_compare( $manifest['version'], MPAB_VERSION, '<=' ) ) {
			return false;
		}

		return array(
			'id'           => self::UPDATE_URI,
			'slug'         => self::SLUG,
			'version'      => $manifest['version'],
			'url'          => $manifest['details_url'],
			'package'      => $manifest['download_url'],
			'tested'       => $manifest['tested'],
			'requires_php' => $manifest['requires_php'],
			'autoupdate'   => false,
		);
	}

	/**
	 * Populate the native View details modal on the Plugins screen.
	 *
	 * @param false|object|array $result Existing API result.
	 * @param string             $action Requested API action.
	 * @param object             $args   API arguments.
	 * @return false|object|array
	 */
	public function plugin_information( $result, $action, $args ) {
		if ( false !== $result || 'plugin_information' !== $action || empty( $args->slug ) || self::SLUG !== $args->slug ) {
			return $result;
		}

		$manifest = $this->get_manifest();
		if ( ! $manifest ) {
			return $result;
		}

		return (object) array(
			'name'          => $manifest['name'],
			'slug'          => self::SLUG,
			'version'       => $manifest['version'],
			'author'        => '<a href="https://woodenbanana.com/">Wooden Banana</a>',
			'homepage'      => $manifest['details_url'],
			'requires'      => $manifest['requires'],
			'tested'        => $manifest['tested'],
			'requires_php'  => $manifest['requires_php'],
			'last_updated'  => $manifest['last_updated'],
			'download_link' => $manifest['download_url'],
			'sections'      => $manifest['sections'],
		);
	}

	/**
	 * Download our package and reject it unless it matches the published hash.
	 *
	 * @param false|string|WP_Error $reply      Existing short-circuit value.
	 * @param string                $package    Package URL.
	 * @param WP_Upgrader           $upgrader   Upgrader instance.
	 * @param array                 $hook_extra Upgrade context.
	 * @return false|string|WP_Error
	 */
	public function verify_package( $reply, $package, $upgrader, $hook_extra ) {
		unset( $upgrader, $hook_extra );

		if ( false !== $reply ) {
			return $reply;
		}

		$manifest = $this->get_manifest();
		if ( ! $manifest || $manifest['download_url'] !== $package ) {
			return $reply;
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		$temporary_file = download_url( $package, 300 );
		if ( is_wp_error( $temporary_file ) ) {
			return $temporary_file;
		}

		$actual_hash = hash_file( 'sha256', $temporary_file );
		if ( ! is_string( $actual_hash ) || ! hash_equals( $manifest['sha256'], strtolower( $actual_hash ) ) ) {
			wp_delete_file( $temporary_file );
			return new WP_Error( 'mpab_update_hash_mismatch', 'Wooden Banana Studio could not verify this update package. The current version has not been changed.' );
		}

		return $temporary_file;
	}

	/**
	 * Fetch and validate the release manifest once per request.
	 *
	 * @return array<string,mixed>|false
	 */
	private function get_manifest() {
		if ( null !== $this->manifest ) {
			return $this->manifest;
		}

		$this->manifest = false;
		$manifest_url  = apply_filters( 'mpab_update_manifest_url', self::MANIFEST_URL );
		if ( ! $this->is_allowed_https_url( $manifest_url, wp_parse_url( self::MANIFEST_URL, PHP_URL_HOST ) ) ) {
			return false;
		}

		$response = wp_safe_remote_get(
			$manifest_url,
			array(
				'timeout'             => 8,
				'redirection'         => 2,
				'limit_response_size' => 262144,
				'headers'             => array(
					'Accept'     => 'application/json',
					'User-Agent' => 'Wooden-Banana-Studio/' . MPAB_VERSION . '; ' . home_url( '/' ),
				),
			)
		);
		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return false;
		}

		$decoded = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $decoded ) || ! $this->validate_manifest( $decoded, $manifest_url ) ) {
			return false;
		}

		$this->manifest = $decoded;
		return $this->manifest;
	}

	/**
	 * Validate all fields used by the updater before trusting the manifest.
	 *
	 * @param array  $manifest    Decoded manifest.
	 * @param string $manifest_url Manifest URL used for this request.
	 * @return bool
	 */
	private function validate_manifest( $manifest, $manifest_url ) {
		$required = array( 'schema', 'name', 'slug', 'version', 'details_url', 'download_url', 'sha256', 'requires', 'tested', 'requires_php', 'last_updated', 'sections' );
		foreach ( $required as $key ) {
			if ( ! array_key_exists( $key, $manifest ) ) {
				return false;
			}
		}

		$manifest_host = wp_parse_url( $manifest_url, PHP_URL_HOST );
		return 1 === (int) $manifest['schema']
			&& self::SLUG === $manifest['slug']
			&& is_string( $manifest['name'] )
			&& is_string( $manifest['version'] )
			&& (bool) preg_match( '/^[0-9]+(?:\.[0-9A-Za-z-]+)+$/', $manifest['version'] )
			&& $this->is_allowed_https_url( $manifest['details_url'], $manifest_host )
			&& $this->is_allowed_https_url( $manifest['download_url'], $manifest_host )
			&& is_string( $manifest['sha256'] )
			&& (bool) preg_match( '/^[a-f0-9]{64}$/', $manifest['sha256'] )
			&& is_string( $manifest['requires'] )
			&& is_string( $manifest['tested'] )
			&& is_string( $manifest['requires_php'] )
			&& is_string( $manifest['last_updated'] )
			&& is_array( $manifest['sections'] );
	}

	/**
	 * Restrict update metadata and packages to HTTPS on the configured host.
	 *
	 * @param mixed  $url          Candidate URL.
	 * @param string $allowed_host Expected hostname.
	 * @return bool
	 */
	private function is_allowed_https_url( $url, $allowed_host ) {
		return is_string( $url )
			&& 'https' === wp_parse_url( $url, PHP_URL_SCHEME )
			&& $allowed_host === wp_parse_url( $url, PHP_URL_HOST );
	}
}
