<?php
/**
 * VillaTheme_Plugin_Check_Update
 */

// no direct access allowed
if ( ! defined( 'ABSPATH' ) ) {
    die();
}

if ( ! class_exists( 'VillaTheme_Plugin_Check_Update' ) ) {

    /**
     * Class VillaTheme_Plugin_Check_Update
     * 1.1.0
     *
     * Package URL has no secrets. One-time opaque token is returned as `package_token`
     * and sent via Authorization: Bearer on download. Do not cache package_token.
     * Site is sent as POST `site` + X-VillaTheme-Site, never in User-Agent.
     */
    class VillaTheme_Plugin_Check_Update {
        /**
         * The plugin current version
         * @var string
         */
        public $current_version;

        /**
         * The plugin remote update path
         * @var string
         */
        public $update_path;

        /**
         * Plugin Slug (plugin_directory/plugin_file.php)
         * @var string
         */
        public $plugin_slug;

        /**
         * Plugin name (plugin_file)
         * @var string
         */
        public $slug;

        /**
         * The item name while requesting to update api
         * @var string
         */
        public $request_name;

        /**
         * The item ID in marketplace
         * @var string
         */
        public $plugin_id;


        /**
         * The item name while requesting to update api
         * @var string
         */
        public $plugin_file_path;

        /**
         * The item name while requesting to update api
         * @var string
         */
        public $banners;
        /**
         * To verify on server
         * @var
         */
        public $key;
        /**
         * This is product Id on VillaTheme
         * @var
         */
        public $item_id;
        public $setting_url;
        public $messages;
        /**
         * Guard against get_site_transient recursion on inject_cached_update.
         *
         * @var bool
         */
        protected $injecting = false;

        /**
         * Initialize a new instance of the WordPress Auto-Update class
         *
         * VillaTheme_Plugin_Check_Update constructor.
         *
         * @param $current_version
         * @param $update_path
         * @param $plugin_slug
         * @param $slug
         * @param $item_id
         * @param string $key
         * @param string $setting_url
         */
        public function __construct( $current_version, $update_path, $plugin_slug, $slug, $item_id, $key = '', $setting_url = '' ) {
            // Set the class public variables
            $this->current_version = $current_version;
            $this->update_path     = $update_path;
            $this->plugin_slug     = $plugin_slug;
            $this->slug            = $slug;
            $this->key             = $key;
            $this->item_id         = $item_id;
            $this->setting_url     = $setting_url;

            //Action hide notices
            if (isset($_GET['villatheme_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['villatheme_nonce'])),'villatheme_nonce')) {
                if ( isset( $_GET[ $this->slug . '_dismiss_notices' ] ) && sanitize_text_field( wp_unslash( $_GET[ $this->slug . '_dismiss_notices' ] ) ) ) {
                    update_option( $this->slug . '_' . $this->current_version . '_dismiss_notices', current_time( 'timestamp', true ) );
                }
                if ( isset( $_GET[ $this->slug . '_hide' ] ) && sanitize_text_field( wp_unslash( $_GET[ $this->slug . '_hide' ] ) ) ) {
                    set_transient( $this->slug . '_hide', 1, 2592000 );
                }
            }
            // Register before admin_init/_maybe_update_plugins so the check is not skipped.
            add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ), 1 );
            add_filter( 'site_transient_update_plugins', array( $this, 'inject_cached_update' ) );

            // Define the alternative response for information checking
            add_filter( 'plugins_api', array( $this, 'check_info' ), 5, 3 );

            // Mint a fresh one-time package token right before WordPress downloads the zip.
            add_filter( 'upgrader_pre_download', array( $this, 'refresh_package_before_download' ), 10, 4 );

            $this->messages = get_option( $this->slug . '_messages', array() );
            if ( ! get_option( $this->slug . '_' . $this->current_version . '_dismiss_notices' ) ) {
                if ( ! get_transient( $this->slug . '_hide' ) ) {
                    if ( ! $this->key ) {
                        add_action( 'admin_notices', array( $this, 'update_key_notice' ) );
                    } else {
                        if ( is_array( $this->messages ) && count( $this->messages ) ) {
                            if ( isset( $this->messages['update'] ) && $this->messages['update'] == 2 ) {
                                add_action( 'admin_notices', array( $this, 'renew_key_notice' ) );
                            }
                        }
                    }
                }
            }
            add_action( 'villatheme_save_and_check_key_' . $this->slug, array( $this, 'save_and_check_key' ) );
            add_action( $this->slug . '_key', array( $this, 'key_active' ) );
        }

        /**
         * This class must be initialized before a plugin saves its settings
         * $auto_update_key may be changed so we should pass it to the hook
         *
         * @param $auto_update_key
         */
        public function save_and_check_key( $auto_update_key ) {
            $this->key = $auto_update_key;

            $this->clear_update_cache();
            $this->request_remote_version();
            $this->messages = get_option( $this->slug . '_messages', array() );
            $this->flush_update_transient();
        }

        /**
         * Drop cached plugin update data so Save & Check Key always hits the API.
         */
        public function clear_update_cache() {
            delete_transient( 'villatheme_item_' . $this->item_id );
            delete_site_transient( 'villatheme_item_' . $this->item_id );

            if ( function_exists( 'wp_clean_plugins_cache' ) ) {
                wp_clean_plugins_cache( true );
            } else {
                delete_site_transient( 'update_plugins' );
                wp_cache_delete( 'plugins', 'plugins' );
            }
        }

        /**
         * Show key is active
         */
        public function key_active() {
            $this->messages = get_option( $this->slug . '_messages', array() );

            if ( $this->key && isset( $this->messages ) ) {
                if ( count( $this->messages ) ) {
                    if ( ! empty( $this->messages['message'] ) ) {
                        ?>
                        <p class="villatheme-key-active"><?php echo esc_html( $this->messages['message'] ) ?></p>
                        <?php
                    }
                }
            }
        }

        /**
         * Show update key wordpress
         */
        public function renew_key_notice() {
            $plugin = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $this->plugin_slug );
            ?>
            <div class="villatheme-dashboard notice notice-warning">
                <div class="villatheme-content">
                    <p><?php echo wp_kses_post( 'Hello! Your <strong>' . $plugin['Name'] . '</strong>  has expired so you no longer receive automatic updates as well as support. Please go to renew now.' ) ?>
                        <a class="button button-primary" target="_blank"
                           href="<?php echo esc_url( 'https://codecanyon.net/downloads' ) ?>"><?php echo esc_html( 'Renew' ) ?></a>
                        <a href="<?php echo esc_url( add_query_arg( array( $this->slug . '_hide' => 1 ,'villatheme_nonce'=> wp_create_nonce( 'villatheme_nonce' )) ) ) ?>"
                           class="button"><?php echo esc_html( 'Skip' ) ?></a>
                    </p>
                    <a target="_self"
                       href="<?php echo esc_url( add_query_arg( array( $this->slug . '_dismiss_notices' => 1 ,'villatheme_nonce'=> wp_create_nonce( 'villatheme_nonce' )) ) ); ?>"
                       class="button notice-dismiss vi-button-dismiss"><?php echo esc_html( 'Dismiss' ) ?></a>
                </div>
            </div>
            <?php
        }

        /**
         * Show update key wordpress
         */
        public function update_key_notice() {
            $plugin = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $this->plugin_slug );
            ?>
            <div class="villatheme-dashboard error">
                <div class="villatheme-content">
                    <p><?php echo wp_kses_post( 'Hello! Would you like to receive automatic update? Please activate your copy of  <strong>' . $plugin['Name'] . '</strong>' ) ?></p>
                    <p>
                        <a href="<?php echo esc_url( $this->setting_url ) ?>#update"
                           class="button button-primary"><?php echo esc_html( 'Activate' ) ?></a>
                        <a href="<?php echo esc_url( add_query_arg( array( $this->slug . '_hide' => 1 ,'villatheme_nonce'=> wp_create_nonce( 'villatheme_nonce' )) ) ) ?>"
                           class="button"><?php echo esc_html( 'Hide' ) ?></a>
                    </p>
                    <a target="_self"
                       href="<?php echo esc_url( add_query_arg( array( $this->slug . '_dismiss_notices' => 1,'villatheme_nonce'=> wp_create_nonce( 'villatheme_nonce' ) ) ) ); ?>"
                       class="button notice-dismiss vi-button-dismiss"><?php echo esc_html( 'Dismiss' ) ?></a>
                </div>
            </div>
            <?php
        }

        /**
         * Add our self-hosted autoupdate plugin to the filter transient
         *
         * @param $transient
         *
         * @return object $ transient
         */
        public function check_update( $transient ) {
            if ( ! is_object( $transient ) ) {
                return $transient;
            }

            $meta = $this->get_remote_meta();

            if ( ! is_array( $meta ) ) {
                return $transient;
            }

            return $this->apply_meta_to_transient( $transient, $meta );
        }

        /**
         * Inject a cached update when WordPress *reads* update_plugins (plugins.php).
         *
         * @param mixed $transient Transient value.
         *
         * @return mixed
         */
        public function inject_cached_update( $transient ) {
            if ( $this->injecting || ! is_object( $transient ) ) {
                return $transient;
            }

            $cached = get_transient( 'villatheme_item_' . $this->item_id );

            if ( ! is_array( $cached ) ) {
                return $transient;
            }

            $this->injecting = true;
            $transient       = $this->apply_meta_to_transient( $transient, $cached );
            $this->injecting = false;

            return $transient;
        }

        /**
         * After Save & Check Key, write this plugin into the update_plugins transient.
         */
        public function flush_update_transient() {
            $transient = get_site_transient( 'update_plugins' );

            if ( ! is_object( $transient ) ) {
                $transient = new stdClass();
            }

            $transient = $this->check_update( $transient );

            if ( empty( $transient->last_checked ) ) {
                $transient->last_checked = time();
            }

            set_site_transient( 'update_plugins', $transient );
        }

        /**
         * @param object $transient Update transient.
         * @param array  $meta      Remote metadata.
         *
         * @return object
         */
        protected function apply_meta_to_transient( $transient, $meta ) {
            $version = isset( $meta['version'] ) ? $meta['version'] : 0;
            $url     = isset( $meta['url'] ) ? $meta['url'] : '';

            if ( ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
                $transient->response = array();
            }

            if ( version_compare( $this->current_version, (string) $version, '<' ) ) {
                $obj              = new stdClass();
                $obj->id          = $this->plugin_slug;
                $obj->slug        = $this->slug;
                $obj->plugin      = $this->plugin_slug;
                $obj->new_version = $version;
                $obj->url         = $url;
                $obj->package     = $this->key ? $this->get_public_package_url() : '';

                $transient->response[ $this->plugin_slug ] = $obj;

                if ( isset( $transient->no_update[ $this->plugin_slug ] ) ) {
                    unset( $transient->no_update[ $this->plugin_slug ] );
                }
            } else {
                if ( isset( $transient->response[ $this->plugin_slug ] ) ) {
                    unset( $transient->response[ $this->plugin_slug ] );
                }

                $item              = new stdClass();
                $item->id          = $this->plugin_slug;
                $item->slug        = $this->slug;
                $item->plugin      = $this->plugin_slug;
                $item->new_version = $this->current_version;
                $item->url         = $url;
                $item->package     = '';

                $transient->no_update[ $this->plugin_slug ] = $item;
            }

            return $transient;
        }

        /**
         * Mint a fresh Bearer token and download the zip immediately before install.
         *
         * @param bool|string|WP_Error $reply      Short-circuit value.
         * @param string               $package    Package URL from the update transient.
         * @param WP_Upgrader          $upgrader   Upgrader instance.
         * @param array                $hook_extra Extra arguments passed by WordPress 5.5+.
         *
         * @return bool|string|WP_Error
         */
        public function refresh_package_before_download( $reply, $package, $upgrader, $hook_extra = array() ) {
            if ( false !== $reply ) {
                return $reply;
            }

            if ( ! $this->key || ! is_string( $package ) || '' === $package ) {
                return $reply;
            }

            if ( ! $this->is_package_for_this_plugin( $package, $hook_extra ) ) {
                return $reply;
            }

            $remote = $this->request_remote_version( true );

            if ( ! is_array( $remote ) || empty( $remote['package'] ) || empty( $remote['package_token'] ) ) {
                return new WP_Error(
                    'villatheme_package_refresh_failed',
                    'Unable to refresh the VillaTheme download package. Please try again.'
                );
            }

            if ( $upgrader && isset( $upgrader->skin ) && is_object( $upgrader->skin ) && method_exists( $upgrader->skin, 'feedback' ) ) {
                $upgrader->skin->feedback( 'downloading_package', $remote['package'] );
            }

            return $this->download_package_with_token( $remote['package'], $remote['package_token'] );
        }

        /**
         * Download zip with Authorization: Bearer (token never goes in the URL).
         *
         * @param string $url   Package endpoint URL.
         * @param string $token One-time download token.
         *
         * @return string|WP_Error Local tempfile path or error.
         */
        private function download_package_with_token( $url, $token ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';

            $url   = (string) $url;
            $token = (string) $token;

            if ( '' === $url || '' === $token ) {
                return new WP_Error( 'villatheme_package_empty', 'Download package data is incomplete.' );
            }

            $tmp = wp_tempnam( $url );

            if ( ! $tmp ) {
                return new WP_Error( 'http_no_file', __( 'Could not create temporary file.' ) );
            }

            $headers = array_merge(
                $this->get_update_site_headers(),
                array(
                    'Authorization'               => 'Bearer ' . $token,
                    'X-VillaTheme-Download-Token' => $token,
                )
            );

            $response = wp_safe_remote_get(
                $url,
                array(
                    'timeout'    => 300,
                    'stream'     => true,
                    'filename'   => $tmp,
                    'user-agent' => $this->get_update_user_agent(),
                    'headers'    => $headers,
                )
            );

            if ( is_wp_error( $response ) ) {
                wp_delete_file( $tmp );

                return $response;
            }

            $code = (int) wp_remote_retrieve_response_code( $response );

            if ( 200 !== $code ) {
                wp_delete_file( $tmp );

                return new WP_Error(
                    'villatheme_package_http_error',
                    sprintf( 'Download failed with HTTP %d.', $code )
                );
            }

            $content_md5 = wp_remote_retrieve_header( $response, 'content-md5' );

            if ( $content_md5 ) {
                $md5_check = verify_file_md5( $tmp, $content_md5 );

                if ( is_wp_error( $md5_check ) ) {
                    wp_delete_file( $tmp );

                    return $md5_check;
                }
            }

            $handle = fopen( $tmp, 'rb' );

            if ( $handle ) {
                $magic = fread( $handle, 4 );
                fclose( $handle );

                if ( ! is_string( $magic ) || 0 !== strpos( $magic, 'PK' ) ) {
                    wp_delete_file( $tmp );

                    return new WP_Error(
                        'villatheme_package_invalid',
                        'Downloaded file is not a valid plugin package.'
                    );
                }
            }

            return $tmp;
        }

        /**
         * Whether the package URL belongs to the current plugin update.
         *
         * @param string $package    Package URL.
         * @param array  $hook_extra Extra arguments from upgrader_pre_download.
         *
         * @return bool
         */
        private function is_package_for_this_plugin( $package, $hook_extra = array() ) {
            if ( ! empty( $hook_extra['plugin'] ) && (string) $hook_extra['plugin'] === (string) $this->plugin_slug ) {
                return true;
            }

            $updates = get_site_transient( 'update_plugins' );

            if ( is_object( $updates ) && ! empty( $updates->response[ $this->plugin_slug ]->package )
                && (string) $updates->response[ $this->plugin_slug ]->package === (string) $package ) {
                return true;
            }

            $parsed = wp_parse_url( $package );

            if ( empty( $parsed['path'] ) || false === strpos( (string) $parsed['path'], '/download' ) ) {
                return false;
            }

            $query = array();

            if ( ! empty( $parsed['query'] ) ) {
                parse_str( (string) $parsed['query'], $query );
            }

            return isset( $query['id'] ) && (string) absint( $query['id'] ) === (string) absint( $this->item_id );
        }

        /**
         * Public package endpoint used in the update transient (no secrets).
         *
         * @return string
         */
        private function get_public_package_url() {
            return add_query_arg(
                'id',
                absint( $this->item_id ),
                untrailingslashit( (string) $this->update_path ) . '/download'
            );
        }

        /**
         * Cached metadata only (version, changelog, message) — never includes package secrets.
         *
         * @return array|false
         */
        public function get_remote_meta() {
            $cached = get_transient( 'villatheme_item_' . $this->item_id );

            if ( is_array( $cached ) ) {
                unset( $cached['package'], $cached['package_token'] );

                return $cached;
            }

            $remote = $this->request_remote_version();

            if ( ! is_array( $remote ) ) {
                return false;
            }

            unset( $remote['package'], $remote['package_token'] );

            return $remote;
        }

        /**
         * Return the remote version data (includes a fresh package URL when available).
         *
         * @return array|false
         */
        public function get_remote_version() {
            return $this->request_remote_version();
        }

        /**
         * Customer site URL for v4 bind. Sent in body and X-VillaTheme-Site, not User-Agent.
         *
         * @return string
         */
        private function get_update_site() {
            return (string) get_site_url();
        }

        /**
         * Generic User-Agent without the site URL (access logs and proxies leak UA).
         *
         * @return string
         */
        private function get_update_user_agent() {
            global $wp_version;

            return 'WordPress/' . $wp_version;
        }

        /**
         * Headers that identify this site to downloads/v4 and /download.
         *
         * @return array<string, string>
         */
        private function get_update_site_headers() {
            return array(
                'X-VillaTheme-Site' => $this->get_update_site(),
            );
        }

        /**
         * Call the update API and cache metadata without package secrets.
         *
         * @param bool $with_token When true, request a one-time package_token for download.
         *
         * @return array|false
         */
        private function request_remote_version( $with_token = false ) {
            if ( ! $this->item_id ) {
                return false;
            }

            if ( $with_token && ! $this->key ) {
                return false;
            }

            $site = $this->get_update_site();
            $body = array(
                'key'  => $this->key,
                'id'   => $this->item_id,
                'site' => $site,
            );

            if ( $with_token ) {
                $body['token'] = 1;
            }

            $timeout = 15;
            if ( $with_token || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
                $timeout = 30;
            }

            $this->log_update_debug(
                'downloads/v4 request',
                array(
                    'url'     => $this->update_path,
                    'id'      => $this->item_id,
                    'site'    => $site,
                    'token'   => $with_token ? 1 : 0,
                    'timeout' => $timeout,
                    'key'     => $this->mask_secret( (string) $this->key ),
                )
            );

            $request = wp_remote_post(
                $this->update_path,
                array(
                    'user-agent' => $this->get_update_user_agent(),
                    'timeout'    => $timeout,
                    'headers'    => $this->get_update_site_headers(),
                    'body'       => $body,
                )
            );

            if ( is_wp_error( $request ) ) {
                $error = $request->get_error_message();
                $this->log_update_debug( 'downloads/v4 wp_error', $error );
                $this->store_request_message( $error, 0 );

                return false;
            }

            $http = (int) wp_remote_retrieve_response_code( $request );
            $raw  = (string) wp_remote_retrieve_body( $request );

            $this->log_update_debug(
                'downloads/v4 response',
                array(
                    'http' => $http,
                    'body' => $this->sanitize_response_for_log( $raw ),
                )
            );

            if ( 429 === $http ) {
                $this->store_request_message( 'Too many requests. Please wait 10 seconds and try again.', 0 );

                return false;
            }

            if ( 200 !== $http ) {
                $this->store_request_message( sprintf( 'Update API HTTP %d.', $http ), 0 );

                return false;
            }

            $result = json_decode( $raw, true );

            if ( ! is_array( $result ) ) {
                $this->store_request_message( 'Update API returned invalid JSON.', 0 );

                return false;
            }

            if ( ! empty( $result['message'] ) ) {
                $message = json_decode( $result['message'], true );

                if ( is_array( $message ) ) {
                    update_option( $this->slug . '_messages', $message );
                    $this->messages = $message;
                }
            }

            $api_code = isset( $result['code'] ) ? (int) $result['code'] : 0;

            // HTTP 200 with JSON code 400 is still an API error — do not cache it for 24h.
            if ( $api_code >= 400 ) {
                return $result;
            }

            $cache = $result;
            unset( $cache['package'], $cache['package_token'] );
            set_transient( 'villatheme_item_' . $this->item_id, $cache, DAY_IN_SECONDS );

            return $result;
        }

        /**
         * Persist a request-level message for the Auto Update Key UI.
         *
         * @param string $message Message text.
         * @param int    $status  1 = ok, 0 = error.
         */
        private function store_request_message( $message, $status ) {
            $data = array(
                'message' => (string) $message,
                'status'  => (int) $status,
                'update'  => 0,
            );

            update_option( $this->slug . '_messages', $data );
            $this->messages = $data;
        }

        /**
         * Mask secrets in debug output.
         *
         * @param string $value Raw secret.
         *
         * @return string
         */
        private function mask_secret( $value ) {
            $len = strlen( $value );

            if ( $len <= 8 ) {
                return $len ? str_repeat( '*', $len ) : '';
            }

            return substr( $value, 0, 4 ) . str_repeat( '*', $len - 8 ) . substr( $value, - 4 );
        }

        /**
         * Decode API JSON for logs and strip package_token.
         *
         * @param string $raw Response body.
         *
         * @return array|string
         */
        private function sanitize_response_for_log( $raw ) {
            $decoded = json_decode( $raw, true );

            if ( ! is_array( $decoded ) ) {
                return $raw;
            }

            if ( ! empty( $decoded['package_token'] ) ) {
                $decoded['package_token'] = $this->mask_secret( (string) $decoded['package_token'] );
            }

            if ( ! empty( $decoded['package'] ) ) {
                $decoded['package'] = (string) $decoded['package'];
            }

            return $decoded;
        }

        /**
         * Write request/response debug when WP_DEBUG is on.
         *
         * @param string       $label Log label.
         * @param mixed        $data  Payload.
         */
        private function log_update_debug( $label, $data ) {
            if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
                return;
            }

            $payload = is_string( $data ) ? $data : wp_json_encode( $data );

            if ( function_exists( 'wc_get_logger' ) ) {
                wc_get_logger()->debug(
                    $label . ' ' . $payload,
                    array( 'source' => 'woocommerce-multi-currency-update' )
                );

                return;
            }

            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log( '[WMC Update] ' . $label . ' ' . $payload );
        }


        /**
         * Add our self-hosted description to the filter
         *
         * @param boolean $false
         * @param array $action
         * @param object $arg
         *
         * @return bool|object
         */
        public function check_info( $false, $action, $arg ) {
            if ( isset( $arg->slug ) && $arg->slug === $this->slug ) {
                $changelog = $this->get_changelog();

                return new WP_Error( 'villatheme_premium_plugin', nl2br( $changelog ), 'changelog' );
            }

            return $false;
        }

        private function get_changelog() {
            $item = $this->get_remote_meta();

            return ( is_array( $item ) && isset( $item['changelog'] ) ) ? $item['changelog'] : '';
        }
    }
}