<?php
namespace PowerpackElements\Classes;

/**
 * Handles logic for the admin settings page.
 *
 * @since 1.0.0
 */
final class PP_Admin_Settings {
	/**
	 * Holds any errors that may arise from
	 * saving admin settings.
	 *
	 * @since 1.0.0
	 * @var array $errors
	 */
	public static $errors = array();

	public static $settings = array();

	/**
	 * Initializes the admin settings.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public static function init() {
		self::migrate_settings();

		add_action( 'plugins_loaded', __CLASS__ . '::init_hooks' );

		/*
		 * Deliberately outside init_hooks(), which returns early on anything
		 * that is not an admin request — cron is exactly that. Without this a
		 * site nobody signs into loses its Instagram token after 60 days even
		 * while the front end is serving the feed every day.
		 */
		add_action( 'init', __CLASS__ . '::schedule_instagram_token_refresh' );
		add_action( 'pp_refresh_instagram_access_token', __CLASS__ . '::refresh_instagram_access_token' );
	}

	/**
	 * Adds the admin menu and enqueues CSS/JS if we are on
	 * the plugin's admin settings page.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public static function init_hooks() {
		if ( ! is_admin() ) {
			return;
		}

		// Remove all third party plugins notices.
		add_action( 'in_admin_header', __CLASS__ . '::remove_all_notices', PHP_INT_MAX );

		add_action( 'admin_menu', __CLASS__ . '::menu', 601 );
		add_filter( 'all_plugins', __CLASS__ . '::update_branding' );
		// add_action( 'elementor/init', __CLASS__ . '::add_admin_actions', 0 );

		$enabled_modules = get_option( 'pp_elementor_modules' );
		if ( empty( $enabled_modules ) ) {
			self::default_disabled_modules();
		}

		// The screen saves over the REST API now; there is no form post to
		// intercept here.
		if ( isset( $_REQUEST['page'] ) && 'powerpack-settings' === $_REQUEST['page'] ) {
			add_action( 'admin_enqueue_scripts', __CLASS__ . '::styles_scripts' );
		}

		add_action( 'admin_init', __CLASS__ . '::refresh_instagram_access_token' );

		// Late, so it runs after the filter it is undoing.
		add_filter( 'admin_footer_text', __CLASS__ . '::admin_footer_text', 100 );
	}

	/**
	 * Restore the default admin footer on this plugin's screens.
	 *
	 * Elementor replaces the footer on any screen whose id contains its own
	 * name, and this screen is a submenu of Elementor's, so it inherited a
	 * request to review a different plugin. WordPress's own text is put back
	 * rather than the line being emptied, so the footer keeps the shape every
	 * other admin page has.
	 *
	 * @since 3.0.0
	 * @param string $text Footer text.
	 * @return string
	 */
	public static function admin_footer_text( $text ) {
		if ( ! self::is_settings_screen() ) {
			return $text;
		}

		return '<span id="footer-thankyou">' . sprintf(
			/* translators: %s: WordPress.org link. */
			esc_html__( 'Thank you for creating with %s.', 'powerpack' ),
			'<a href="https://wordpress.org/">WordPress</a>'
		) . '</span>';
	}

	/**
	 * Whether the screen being rendered belongs to this plugin.
	 *
	 * @since 3.0.0
	 * @return bool
	 */
	private static function is_settings_screen() {
		if ( ! function_exists( 'get_current_screen' ) ) {
			return false;
		}

		$screen = get_current_screen();

		return $screen && false !== strpos( $screen->id, 'powerpack-settings' );
	}

	/**
	 * This plugin's own admin notices, captured for the settings screen.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	private static $notices = '';

	/**
	 * Clear the screen of notices, keeping this plugin's own.
	 *
	 * Every plugin on the site writes to admin_notices, and a settings screen
	 * that opens under three unrelated banners is not a settings screen anyone
	 * can read. They are all dropped here — but dropping ours with them meant a
	 * licence warning or an update prompt never reached the one screen where it
	 * is relevant, so ours are rendered first and kept.
	 *
	 * They are held rather than printed because WordPress fires admin_notices
	 * above the whole page. render() puts them in the markup and the settings
	 * app moves them below its header, which is where they belong.
	 *
	 * @since 3.0.0
	 * @return void
	 */
	public static function remove_all_notices() {
		if ( ! isset( $_REQUEST['page'] ) || 'powerpack-settings' !== $_REQUEST['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return;
		}

		self::$notices = self::capture_own_notices();

		remove_all_actions( 'admin_notices' );
		remove_all_actions( 'all_admin_notices' );
	}

	/**
	 * Render this plugin's notices and return the markup.
	 *
	 * Priority order is preserved, because a notice registered late is meant to
	 * appear after one registered early.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	private static function capture_own_notices() {
		global $wp_filter;

		$html = '';

		foreach ( [ 'admin_notices', 'all_admin_notices' ] as $hook ) {
			if ( empty( $wp_filter[ $hook ] ) ) {
				continue;
			}

			$callbacks = $wp_filter[ $hook ]->callbacks;
			ksort( $callbacks );

			foreach ( $callbacks as $group ) {
				foreach ( $group as $registered ) {
					if ( ! self::is_own_notice( $registered['function'] ) ) {
						continue;
					}

					ob_start();
					call_user_func( $registered['function'] );
					$html .= ob_get_clean();
				}
			}
		}

		return trim( $html );
	}

	/**
	 * Whether a notice callback belongs to this plugin.
	 *
	 * Attribution is by name, which is as much as the hook records. A closure
	 * carries no name and is treated as someone else's: showing a third party
	 * notice inside this plugin's chrome is the worse of the two mistakes.
	 *
	 * @since 3.0.0
	 * @param callable $callback Registered callback.
	 * @return bool
	 */
	private static function is_own_notice( $callback ) {
		if ( is_string( $callback ) ) {
			return 0 === strpos( $callback, 'pp_' ) || 0 === strpos( $callback, 'powerpack' );
		}

		if ( is_array( $callback ) && isset( $callback[0] ) ) {
			$class = is_object( $callback[0] ) ? get_class( $callback[0] ) : (string) $callback[0];

			return false !== stripos( $class, 'PowerpackElements' );
		}

		return false;
	}

	/**
	 * Add admin action hooks
	 *
	 * @since 2.7.9
	 */
	public static function add_admin_actions() {
		// add_action( 'elementor/editor/after_enqueue_styles', __CLASS__ . '::styles_scripts' );
	}

	/**
	 * Enqueues the needed CSS/JS for the builder's admin settings page.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public static function styles_scripts() {
		/*
		 * Idempotent. The hook fires once in a real request, but the screen is
		 * also enqueued directly by the test harness, and running twice
		 * appended a second preload block — two JSON objects where the client
		 * expects one.
		 */
		static $done = false;

		if ( $done ) {
			return;
		}

		$done = true;

		// The white label logo field opens the media library to pick one.
		wp_enqueue_media();

		$settings   = self::get_settings();
		$asset_path = POWERPACK_ELEMENTS_PATH . 'assets/build/settings/index.asset.php';

		if ( ! file_exists( $asset_path ) ) {
			return;
		}

		$asset = include $asset_path;

		wp_enqueue_script(
			'pp-settings-app',
			POWERPACK_ELEMENTS_URL . 'assets/build/settings/index.js',
			$asset['dependencies'],
			$asset['version'],
			true
		);

		wp_enqueue_style(
			'pp-settings-app',
			POWERPACK_ELEMENTS_URL . 'assets/build/settings/style-index.css',
			array( 'wp-components' ),
			$asset['version']
		);

		wp_style_add_data( 'pp-settings-app', 'rtl', 'replace' );

		/*
		 * Follow the colour scheme the user picked on their profile. The
		 * override is scoped to the screen and also covers
		 * '--wp-admin-theme-color', so the block components rendered inside it
		 * — switches, buttons — pick up the same accent as our own chrome.
		 */
		$scheme_colors = self::get_admin_scheme_colors();

		if ( ! empty( $scheme_colors ) ) {
			$soft = ! empty( $scheme_colors['soft'] ) ? sprintf( '--ppe-primary-color-soft:%s;', esc_attr( $scheme_colors['soft'] ) ) : '';

			wp_add_inline_style(
				'pp-settings-app',
				sprintf(
					'.pp-settings{--ppe-primary-color:%1$s;--ppe-primary-color-light:%2$s;--wp-admin-theme-color:%1$s;%3$s}',
					esc_attr( $scheme_colors['primary'] ),
					esc_attr( $scheme_colors['light'] ),
					$soft
				)
			);
		}

		wp_set_script_translations( 'pp-settings-app', 'powerpack', POWERPACK_ELEMENTS_PATH . 'languages' );

		wp_enqueue_style(
			'powerpack-icons',
			POWERPACK_ELEMENTS_URL . 'assets/lib/ppicons/css/powerpack-icons.css',
			array(),
			POWERPACK_ELEMENTS_VER
		);

		$docs_link    = ! empty( $settings['docs_link'] ) ? $settings['docs_link'] : 'https://powerpackelements.com/docs/';
		$support_link = ! empty( $settings['support_link'] ) ? $settings['support_link'] : 'https://powerpackelements.com/contact/';

		wp_add_inline_script(
			'pp-settings-app',
			'window.ppSettingsBootstrap = ' . wp_json_encode( array(
				'adminLabel'   => self::get_admin_label(),
				'version'      => POWERPACK_ELEMENTS_VER,

				/*
				 * Which edition is running. PowerPack Lite is a separate
				 * plugin, so this file only ever executes in Pro — but sending
				 * it rather than hardcoding it in the bundle keeps the two
				 * builds able to share one settings app.
				 *
				 * There is deliberately no upgrade prompt here. This is the
				 * paid edition; the free one sends 'showUpgrade' and
				 * 'upgradeUrl', and an unlicensed install of this edition is
				 * asked to activate its license on the License panel, not to
				 * buy something it already has.
				 */
				'isPro'        => true,
				'hideLogo'     => 'on' === $settings['hide_logo'],

				/*
				 * The mark this site supplied, if it supplied one. The header
				 * draws ours from an icon font, so it needs to know whether to
				 * draw an image instead rather than simply being handed a URL.
				 */
				'customLogo'   => self::get_custom_logo(),
				'docsLink'     => $docs_link,
				'supportLink'  => $support_link,

				/*
				 * The header's two links are each covered by the switch for that
				 * kind of link, rather than by a pair of their own: Hide Docs
				 * Links reaches every documentation link in this screen, header
				 * included, and Hide Support Link is now only about this one.
				 */
				'showDocs'     => 'on' !== $settings['hide_docs_links'],
				'showSupport'  => 'on' !== $settings['hide_support'],

				/*
				 * Whether to offer the rollback control on the Advanced panel.
				 * Reinstalling an older build is governed by update_plugins,
				 * not by the capability that opens this screen, so an editor
				 * with access to the widget library is never shown a control
				 * every action on which would be refused.
				 */
				'canRollback'  => current_user_can( 'update_plugins' ),

				// Sent with the page so a dismissed checklist never flashes up
				// before a request comes back to say it was dismissed.
				'setupDone'    => (bool) get_user_meta( get_current_user_id(), PP_Settings_REST_Controller::SETUP_DISMISSED_META, true ),
			) ) . ';',
			'before'
		);

		/*
		 * Hand the first responses to the client with the page, so the screen
		 * paints without a request waterfall.
		 */
		$preload = rest_preload_api_request( array(), '/powerpack/v1/settings' );
		$preload = rest_preload_api_request( $preload, '/powerpack/v1/modules' );

		wp_add_inline_script(
			'wp-api-fetch',
			sprintf(
				'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
				wp_json_encode( $preload )
			),
			'after'
		);

		if ( isset( $settings['plugin_short_name'] ) && '' !== $settings['plugin_short_name'] ) {
			$short_name  = $settings['plugin_short_name'];
			$custom_css  = '.elementor-element [class*="power-pack-"]:after {';
			$custom_css .= 'content: "' . $short_name . '"; }';
			wp_add_inline_style( 'powerpack-editor', $custom_css );
		}
	}

	/**
	 * Accent colours from the current user's admin colour scheme.
	 *
	 * Core does not expose the scheme through a CSS custom property — the
	 * scheme stylesheets only consume '--wp-admin-theme-color', which is set
	 * once to the default. The registered palettes do carry it though, and the
	 * highlight is consistently the second to last swatch across every scheme
	 * core ships, whether the palette has three entries or four.
	 *
	 * @since 3.0.0
	 * @return array Empty when the scheme is unknown, otherwise 'primary' and 'light'.
	 */
	private static function get_admin_scheme_colors() {
		global $_wp_admin_css_colors;

		$scheme = get_user_option( 'admin_color' );

		if ( empty( $scheme ) || empty( $_wp_admin_css_colors[ $scheme ]->colors ) ) {
			return array();
		}

		$colors = array_values( (array) $_wp_admin_css_colors[ $scheme ]->colors );
		$count  = count( $colors );

		if ( $count < 2 ) {
			return array();
		}

		return array(
			'primary' => $colors[ $count - 2 ],
			'light'   => $colors[ $count - 1 ],
			'soft'    => self::hex_to_rgba( $colors[ $count - 2 ], 0.1 ),
		);
	}

	/**
	 * Convert a hex colour to an rgba() string.
	 *
	 * Used for the tinted backgrounds behind active and hovered navigation, so
	 * they follow the accent instead of staying a fixed blue.
	 *
	 * @since 3.0.0
	 * @param string $hex   Hex colour, with or without the leading hash.
	 * @param float  $alpha Alpha channel.
	 * @return string Empty when the colour cannot be parsed.
	 */
	private static function hex_to_rgba( $hex, $alpha ) {
		$hex = ltrim( (string) $hex, '#' );

		if ( 3 === strlen( $hex ) ) {
			$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
		}

		if ( ! preg_match( '/^[0-9a-f]{6}$/i', $hex ) ) {
			return '';
		}

		return sprintf(
			'rgba(%d, %d, %d, %s)',
			hexdec( substr( $hex, 0, 2 ) ),
			hexdec( substr( $hex, 2, 2 ) ),
			hexdec( substr( $hex, 4, 2 ) ),
			$alpha
		);
	}

	/**
	 * Get settings.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	public static function get_settings() {
		$default_settings = array(
			'plugin_name'             => '',
			'plugin_short_name'       => '',
			'plugin_desc'             => '',
			'plugin_author'           => '',
			'plugin_uri'              => '',
			'admin_label'             => '',
			'support_link'            => '',
			'docs_link'               => '',
			'hide_support'            => 'off',

			/*
			 * Inherits from the switch it was split out of, so an upgrade does
			 * not hand these links back on a site that had hidden them. The rule
			 * lives with the field definition rather than here; see
			 * PP_Settings_Registry::widget_help_default().
			 */
			'hide_widget_help'        => PP_Settings_Registry::widget_help_default(),
			'hide_wl_settings'        => 'off',
			'hide_plugin'             => 'off',
			'hide_logo'               => 'off',
			'hide_license_key_link'   => 'off',
			'hide_demo_links'         => 'off',
			'hide_docs_links'         => 'off',
			'hide_elements_tab'       => 'off',
			'hide_extensions_tab'     => 'off',
			'hide_integration_tab'    => 'off',
			'hide_header_footer_tab'  => 'off',
			'hide_woo_tab'            => 'off',
			'hide_login_register_tab' => 'off',
			'hide_advanced_tab'       => 'off',
			'google_map_api'          => '',
			'google_map_lang'         => '',
			'google_calendar_api'     => '',
			'pp_enable_csv_upload'    => '',
		);

		$settings = get_option( 'pp_elementor_settings' );

		if ( ! is_array( $settings ) || empty( $settings ) ) {
			$settings = $default_settings;
		}

		if ( is_array( $settings ) && ! empty( $settings ) ) {
			$settings = array_merge( $default_settings, $settings );
		}

		return apply_filters( 'pp_elements_admin_settings', $settings );
	}

	/**
	 * Get admin label from settings.
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public static function get_admin_label() {
		$settings = self::get_settings();

		$admin_label = $settings['admin_label'];

		return trim( $admin_label ) == '' ? 'PowerPack' : trim( $admin_label );
	}

	/**
	 * The short form of the name, for places with no room for the long one.
	 *
	 * White label already collects this. Until now the only thing reading it was
	 * a CSS rule that stamps it on widget badges in the editor, so a site could
	 * set it and still find "PPE" in the right click menu.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public static function get_short_label() {
		$settings = self::get_settings();

		$short_name = isset( $settings['plugin_short_name'] ) ? trim( $settings['plugin_short_name'] ) : '';

		return '' === $short_name ? 'PPE' : $short_name;
	}

	/**
	 * The mark to show, or nothing when it has been turned off.
	 *
	 * Every place that draws the logo asks here, so "hide the logo" means the
	 * same thing in all of them. It did not before: the switch reached the
	 * settings header and nothing else, while the template library carried on
	 * showing ours inside the editor, which is the one place a client looks.
	 *
	 * @since 3.0.0
	 * @return string Image URL, or '' when no mark should be drawn.
	 */
	public static function get_logo() {
		$custom = self::get_custom_logo();

		if ( '' !== $custom ) {
			return $custom;
		}

		$settings = self::get_settings();

		if ( 'on' === $settings['hide_logo'] ) {
			return '';
		}

		return POWERPACK_ELEMENTS_URL . 'assets/images/pp-logo-sm.svg';
	}

	/**
	 * The mark this site has supplied, if any.
	 *
	 * Separate from get_logo() because two of the places that draw a mark can
	 * only draw ours: the settings header uses an icon font, and the Magic Wand
	 * hands an icon class to a page on another origin. Neither can show an
	 * uploaded image, so both need to know the difference between "the default
	 * mark" and "a mark of their own" rather than just being handed a URL.
	 *
	 * Hiding wins over a custom logo, the same way it wins over ours. It is the
	 * blunter instrument and the one an agency reaches for last.
	 *
	 * @since 3.0.0
	 * @return string Image URL, or '' when there is none.
	 */
	public static function get_custom_logo() {
		$settings = self::get_settings();

		if ( 'on' === $settings['hide_logo'] ) {
			return '';
		}

		$logo = isset( $settings['plugin_logo'] ) ? trim( (string) $settings['plugin_logo'] ) : '';

		if ( '' === $logo ) {
			return '';
		}

		if ( is_numeric( $logo ) ) {
			$url = wp_get_attachment_url( absint( $logo ) );

			// An attachment that has since been deleted falls back to ours
			// rather than to a broken image.
			return $url ? $url : '';
		}

		return $logo;
	}

	static public function parse_error( $message ) {
		if ( isset( $_GET['pp_debug'] ) ) {
			return $message;
		}
		if ( false !== strpos( $message, 'powerpackelements' ) ) {
			return esc_html__( 'Could not connect to the host.', 'powerpack' );
		}

		return;
	}

	/**
	 * Renders the admin settings menu.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public static function menu() {
		$admin_label = self::get_admin_label();
		$title       = $admin_label;
		$cap         = 'manage_options';
		$slug        = 'powerpack-settings';
		$func        = __CLASS__ . '::render';

		if ( current_user_can( 'manage_options' ) ) {
			if ( defined( '\ELEMENTOR_VERSION' ) && version_compare( \ELEMENTOR_VERSION, '3.35.0', '>=' ) ) {
				add_submenu_page( 'elementor-home', $title, $title, $cap, $slug, $func );
			} else {
				add_submenu_page( 'elementor', $title, $title, $cap, $slug, $func );
			}
		}
	}

	/**
	 * Render the settings screen mount point.
	 *
	 * Everything below this node is rendered by the React app in
	 * assets/build/settings. The noscript message matters: without it a failed
	 * bundle leaves an administrator staring at an empty page with no clue why.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public static function render() {
		if ( ! file_exists( POWERPACK_ELEMENTS_PATH . 'assets/build/settings/index.asset.php' ) ) {
			?>
			<div class="wrap">
				<div class="notice notice-error">
					<p><?php esc_html_e( 'The PowerPack settings screen has not been built. Run "npm install" followed by "npm run build:settings" in the plugin directory.', 'powerpack' ); ?></p>
				</div>
			</div>
			<?php
			return;
		}
		?>
		<div class="wrap pp-settings-wrap">
			<?php
			/*
			 * Hidden until the app has moved it below the header. Without the
			 * attribute a notice would flash at the top of the page on every
			 * load, in the position this is trying to move it out of.
			 *
			 * Not escaped: this is notice markup the plugin itself just
			 * rendered, captured verbatim from its own callbacks.
			 */
			if ( '' !== self::$notices ) {
				echo '<div class="pp-settings-notices" hidden>' . self::$notices . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			}
			?>
			<div id="pp-settings-root"></div>
			<noscript>
				<div class="notice notice-error">
					<p><?php esc_html_e( 'The PowerPack settings screen needs JavaScript. Please enable it and reload this page.', 'powerpack' ); ?></p>
				</div>
			</noscript>
		</div>
		<?php
	}

	/**
	 * Renders the action for a form.
	 *
	 * @since 1.0.0
	 * @param string $type The type of form being rendered.
	 * @return void
	 */
	public static function get_form_action( $type = '' ) {
		if ( is_network_admin() ) {
			return esc_url( network_admin_url( '/admin.php?page=powerpack-settings' . $type ) );
		} else {
			return esc_url( admin_url( '/admin.php?page=powerpack-settings' . $type ) );
		}
	}

	/**
	 * Returns an option from the database for
	 * the admin settings page.
	 *
	 * @since 1.0.0
	 * @param string $key The option key.
	 * @return mixed
	 */
	public static function get_option( $key, $network_override = true ) {
		if ( is_network_admin() ) {
			$value = get_site_option( $key );
		} elseif ( ! $network_override && is_multisite() ) {
			$value = get_site_option( $key );
		} elseif ( $network_override && is_multisite() ) {
			$value = get_option( $key );
			$value = ( false === $value || ( is_array( $value ) && in_array( 'disabled', $value ) && get_option( 'pp_override_ms' ) != 1 ) ) ? get_site_option( $key ) : $value;
		} else {
			$value = get_option( $key );
		}

		return $value;
	}

	/**
	 * Updates an option from the admin settings page.
	 *
	 * @since 1.0.0
	 * @param string $key The option key.
	 * @param mixed $value The value to update.
	 * @return mixed
	 */
	public static function update_option( $key, $value, $network_override = true ) {
		if ( is_network_admin() ) {
			update_site_option( $key, $value );
		}
		// Delete the option if network overrides are allowed and the override checkbox isn't checked.
		elseif ( $network_override && is_multisite() && ! isset( $_POST['pp_override_ms'] ) ) {
			delete_option( $key );
		} else {
			update_option( $key, $value );
		}
	}

	/**
	 * Delete an option from the admin settings page.
	 *
	 * @since 1.0.0
	 * @param string $key The option key.
	 * @param mixed $value The value to delete.
	 * @return mixed
	 */
	public static function delete_option( $key ) {
		if ( is_network_admin() ) {
			delete_site_option( $key );
		} else {
			delete_option( $key );
		}
	}

	/**
	 * Set the branding data to plugin.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	public static function update_branding( $all_plugins ) {
		if ( ! is_array( $all_plugins ) || empty( $all_plugins ) || ! isset( $all_plugins[ POWERPACK_ELEMENTS_BASE ] ) ) {
			return $all_plugins;
		}

		$settings = self::get_settings();

		$all_plugins[ POWERPACK_ELEMENTS_BASE ]['Name']           = ! empty( $settings['plugin_name'] ) ? $settings['plugin_name'] : $all_plugins[ POWERPACK_ELEMENTS_BASE ]['Name'];
		$all_plugins[ POWERPACK_ELEMENTS_BASE ]['PluginURI']      = ! empty( $settings['plugin_uri'] ) ? $settings['plugin_uri'] : $all_plugins[ POWERPACK_ELEMENTS_BASE ]['PluginURI'];
		$all_plugins[ POWERPACK_ELEMENTS_BASE ]['Description']    = ! empty( $settings['plugin_desc'] ) ? $settings['plugin_desc'] : $all_plugins[ POWERPACK_ELEMENTS_BASE ]['Description'];
		$all_plugins[ POWERPACK_ELEMENTS_BASE ]['Author']         = ! empty( $settings['plugin_author'] ) ? $settings['plugin_author'] : $all_plugins[ POWERPACK_ELEMENTS_BASE ]['Author'];
		$all_plugins[ POWERPACK_ELEMENTS_BASE ]['AuthorURI']      = ! empty( $settings['plugin_uri'] ) ? $settings['plugin_uri'] : $all_plugins[ POWERPACK_ELEMENTS_BASE ]['AuthorURI'];
		$all_plugins[ POWERPACK_ELEMENTS_BASE ]['Title']          = ! empty( $settings['plugin_name'] ) ? $settings['plugin_name'] : $all_plugins[ POWERPACK_ELEMENTS_BASE ]['Title'];
		$all_plugins[ POWERPACK_ELEMENTS_BASE ]['AuthorName']     = ! empty( $settings['plugin_author'] ) ? $settings['plugin_author'] : $all_plugins[ POWERPACK_ELEMENTS_BASE ]['AuthorName'];

		if ( $settings['hide_plugin'] == 'on' ) {
			unset( $all_plugins[ POWERPACK_ELEMENTS_BASE ] );
		}

		return $all_plugins;
	}











	public static function migrate_settings() {
		if ( ! is_multisite() ) {
			return;
		}
		if ( 'yes' === get_option( 'pp_multisite_settings_migrated' ) ) {
			return;
		}

		$fields = array(
			'pp_license_status',
			'pp_license_key',
			'pp_elementor_settings',
			'pp_elementor_modules',
			'pp_elementor_extensions',
			'pp_elementor_taxonomy_thumbnail_enable',
			'pp_elementor_taxonomy_thumbnail_taxonomies',
		);

		foreach ( $fields as $field ) {
			$value = get_site_option( $field );
			if ( $value ) {
				update_option( $field, $value );
			}
		}

		update_option( 'pp_multisite_settings_migrated', 'yes' );
	}

	/**
	* Refresh instagram token after 30 days.
	*
	* @since 2.5.2
	*/
	public static function refresh_instagram_access_token( $access_token = '', $widget_id = '' ) {
		/*
		 * Only a token that came out of the settings screen can be written
		 * back. A token passed in belongs to one widget and lives in that
		 * widget's Elementor settings, so the refreshed value has nowhere to
		 * go — the call still extends the token's life at Instagram's end,
		 * which is all that path can do.
		 */
		$is_stored_token = empty( $access_token );

		if ( $is_stored_token ) {
			$access_token = trim( (string) self::get_option( 'pp_instagram_access_token' ) );
		}

		$updated_access_token = 'ppe_updated_instagram_access_token';

		if ( ! empty( $widget_id ) ) {
			$updated_access_token = "ppe_updated_instagram_access_token_widget_$widget_id";
		}

		if ( empty( $access_token ) ) {
			return;
		}

		$updated = get_transient( $updated_access_token );

		if ( ! empty( $updated ) ) {
			return;
		}

		$endpoint_url = add_query_arg(
			[
				'access_token' => $access_token,
				'grant_type'   => 'ig_refresh_token',
			],
			'https://graph.instagram.com/refresh_access_token'
		);

		$response = wp_remote_get( $endpoint_url );

		if ( ! $response || 200 !== wp_remote_retrieve_response_code( $response ) || is_wp_error( $response ) ) {
			set_transient( $updated_access_token, 'error', DAY_IN_SECONDS );
			return;
		}

		$body = wp_remote_retrieve_body( $response );

		if ( ! $body ) {
			set_transient( $updated_access_token, 'error', DAY_IN_SECONDS );
			return;
		}

		$body = json_decode( $body, true );

		if ( empty( $body['access_token'] ) || empty( $body['expires_in'] ) ) {
			set_transient( $updated_access_token, 'error', DAY_IN_SECONDS );
			return;
		}

		if ( $is_stored_token ) {
			self::store_instagram_access_token( $body['access_token'], (int) $body['expires_in'] );
		}

		set_transient( $updated_access_token, 'updated', 30 * DAY_IN_SECONDS );
	}

	/**
	 * Save a refreshed Instagram token back where the old one was read from.
	 *
	 * Instagram returns a fresh long-lived token rather than confirming the old
	 * one, so the response has to be stored or the next refresh is made against
	 * a value Instagram has already replaced.
	 *
	 * self::update_option() cannot be used: away from the settings screen there
	 * is no $_POST['pp_override_ms'], and on multisite that makes it delete the
	 * option instead of writing it — a cron run would throw the token away. The
	 * value is put back at whichever level it was read from, so a network wide
	 * token does not fragment into per-site copies.
	 *
	 * @since 3.0.0
	 * @param string $access_token Long lived token returned by Instagram.
	 * @param int    $expires_in   Seconds until the new token expires.
	 * @return void
	 */
	private static function store_instagram_access_token( $access_token, $expires_in ) {
		$expires_at = time() + $expires_in;

		// An empty site level option means self::get_option() fell back to the
		// network one, which is therefore where this token belongs. The leading
		// slash is WordPress's get_option(), not this class's same-named method.
		if ( is_multisite() && '' === trim( (string) \get_option( 'pp_instagram_access_token' ) ) ) {
			update_site_option( 'pp_instagram_access_token', $access_token );
			update_site_option( 'pp_instagram_token_expires', $expires_at );
			return;
		}

		update_option( 'pp_instagram_access_token', $access_token );
		update_option( 'pp_instagram_token_expires', $expires_at );
	}

	/**
	 * Keep the daily Instagram token refresh on the schedule.
	 *
	 * The refresh itself is throttled by a 30 day transient, so this is a
	 * single transient read on all but one run in thirty.
	 *
	 * @since 3.0.0
	 * @return void
	 */
	public static function schedule_instagram_token_refresh() {
		if ( wp_next_scheduled( 'pp_refresh_instagram_access_token' ) ) {
			return;
		}

		wp_schedule_event( time(), 'daily', 'pp_refresh_instagram_access_token' );
	}

	/**
	* Run While Fresh installation.
	*
	* @since 2.11.4
	*/
	public static function default_disabled_modules() {
		default_disabled_modules();
	}
}

PP_Admin_Settings::init();
