<?php

use InstagramFeed\Builder\SBI_Db;
use InstagramFeed\Builder\SBI_Source;
use InstagramFeed\Platform_Data;

if (!defined('ABSPATH')) {
	die('-1');
}

/**
 * Class SB_Instagram_Posts_Manager
 *
 * Set as a global object to record and report errors as well
 * as control aspects of image resizing
 *
 * @since 2.0/4.0
 */
class SB_Instagram_Posts_Manager
{
	/**
	 * @var mixed|void
	 */
	var $sbi_options;

	/**
	 * @var int
	 */
	var $limit;

	/**
	 * @var array
	 */
	var $errors;

	/**
	 * @var array
	 */
	var $display_error;

	/**
	 * @var bool
	 */
	var $resizing_tables_exist;

	/**
	 * SB_Instagram_Posts_Manager constructor.
	 */
	public function __construct()
	{
		$this->sbi_options = get_option('sb_instagram_settings');
		$this->errors = get_option('sb_instagram_errors', array());
		if (!isset($this->errors['connection'])) {
			$this->errors = array(
				'connection' => array(),
				'hashtag' => array(),
				'resizing' => array(),
				'database_create' => array(),
				'upload_dir' => array(),
				'accounts' => array(),
				'error_log' => array(),
				'action_log' => array(),
				'revoked' => array(),
			);
		}

		$this->display_error = array();
		if ($this->does_resizing_tables_exist()) {
			$this->resizing_tables_exist = true;
		}

		require_once(trailingslashit(dirname(__FILE__)) . '/Platform_Data.php');
		$platform_data_manager = new Platform_Data();
		$platform_data_manager->register_hooks();
	}

	/**
	 * Used to skip image resizing if the tables were never successfully
	 * created
	 *
	 * @return bool
	 *
	 * @since 2.0/5.0
	 */
	public function does_resizing_tables_exist()
	{
		global $wpdb;

		$table_name = esc_sql($wpdb->prefix . SBI_INSTAGRAM_FEEDS_POSTS);
		$resizing_key = 'sbi_resizing_exists';

		$sbi_resizing_cache = wp_cache_get($resizing_key);

		if (false === $sbi_resizing_cache) {
			if ($wpdb->get_var("show tables like '$table_name'") == $table_name) {
				wp_cache_set($resizing_key, true);
			} else {
				wp_cache_set($resizing_key, false);
			}
		}

		return $sbi_resizing_cache;
	}

	/**
	 * Whether or not the one time request to the "top posts" endpoint for the hashtag
	 * was made
	 *
	 * @param string $hashtag
	 *
	 * @return bool
	 */
	public static function top_post_request_already_made($hashtag)
	{
		$list_of_top_hashtags = get_option('sbi_top_api_calls', array());

		return in_array($hashtag, $list_of_top_hashtags, true);
	}

	/**
	 * @param $hashtag
	 */
	public static function maybe_update_list_of_top_hashtags($hashtag)
	{
		$list_of_top_hashtags = get_option('sbi_top_api_calls', array());

		if (!in_array($hashtag, $list_of_top_hashtags, true)) {
			$list_of_top_hashtags[] = $hashtag;
			update_option('sbi_top_api_calls', $list_of_top_hashtags);
		}
	}

	/**
	 * Stores information about an encountered error related to a connected account
	 *
	 * @param $connected_account array
	 * @param $error_type string
	 * @param $details mixed/array/string
	 *
	 * @since 2.7/5.10
	 */
	public function add_connected_account_error($connected_account, $error_type, $details)
	{
		$account_id = $connected_account['user_id'];
		$this->errors['accounts'][$account_id][$error_type] = $details;

		if ($error_type === 'api') {
			$this->errors['accounts'][$account_id][$error_type]['clear_time'] = time() + 60 * 3;
		}

		if (
			isset($details['error']['code'])
			&& (int)$details['error']['code'] === 18
		) {
			$this->errors['accounts'][$account_id][$error_type]['clear_time'] = time() + 60 * 15;
		}
		SBI_Source::add_error($account_id, $details);
	}

	/**
	 * Stores errors so they can be retrieved and explained to users
	 * in messages as well as temporarily disable certain features
	 *
	 * @param string           $type
	 * @param array|string     $details
	 * @param mixed/bool/array $connected_account_term
	 *
	 * @since 2.7/5.10
	 */
	public function add_error($type, $details, $connected_account_term = false)
	{
		$connected_account = false;
		$log_item = date('m-d H:i:s') . ' - ';

		if ($connected_account_term) {
			$connected_account = is_array($connected_account_term) ? $connected_account_term : SB_Instagram_Connected_Account::lookup($connected_account_term);
			$this->add_connected_account_error($connected_account, $type, $details);
		}

		// $details is an array for 'api', 'wp_remote_get', and 'hashtag' types, while it's a string for the rest.
		switch ($type) {
			case 'api':
			case 'wp_remote_get':
				$this->handleConnectionError($details, $connected_account, $log_item);
				break;
			case 'hashtag':
				$this->handleHashtagError($details, $connected_account, $log_item);
				break;
			case 'image_editor':
			case 'storage':
				$this->errors['resizing'] = $details;
				$log_item .= $details;
				break;
			case 'database_create':
			case 'upload_dir':
			case 'unused_feed':
			case 'platform_data_deleted':
			case 'database_error':
				$this->errors[$type] = $details;
				$log_item .= $details;
				break;
			default:
				$log_item .= $details;
				break;
		}

		$this->updateErrorLog($log_item);
	}

	/**
	 * Handles connection errors for Instagram feed.
	 *
	 * @param array  $details Details of the connection error.
	 * @param object $connected_account The connected Instagram account object.
	 * @param array  &$log_item Reference to the log item array to store error details.
	 *
	 * @return void
	 */
	private function handleConnectionError($details, $connected_account, &$log_item)
	{
		$connection_details = array('error_id' => '', 'critical' => false);

		if (isset($details['error']['code'])) {
			$connection_details['error_id'] = $details['error']['code'];
			$connection_details['critical'] = $this->is_critical_error($details);

			if ($this->is_app_permission_related($details)) {
				if (!in_array($connected_account['user_id'], $this->errors['revoked'], true)) {
					$this->errors['revoked'][] = $connected_account['user_id'];
				}

				/**
				 * Fires when an app permission related error is encountered
				 *
				 * @param array $connected_account The connected account that encountered the error
				 *
				 * @since 6.0.6
				 */
				do_action('sbi_app_permission_revoked', $connected_account);
			}
		} elseif (isset($details['response']) && is_wp_error($details['response'])) {
			foreach ($details['response']->errors as $key => $item) {
				$connection_details['error_id'] = $key;
			}
			$connection_details['critical'] = true;
		}

		if (get_the_ID() !== 0) {
			$connection_details['post_id'] = get_the_ID();
		}

		$connection_details['error_message'] = $this->generate_error_message($details, $connected_account);
		$log_item .= $connection_details['error_message']['admin_only'];
		$this->maybe_set_display_error('connection', $connection_details);
		$this->errors['connection'] = $connection_details;
	}

	/**
	 * Certain API errors are considered critical and will trigger
	 * the various notifications to users to correct them.
	 *
	 * @param $details
	 *
	 * @return bool
	 *
	 * @since 2.7/5.10
	 */
	public function is_critical_error($details)
	{
		$error_code = (int)$details['error']['code'];

		$critical_codes = array(
			803, // ID doesn't exist
			100, // access token or permissions
			190, // access token or permissions
			10, // app permissions or scopes
		);

		return in_array($error_code, $critical_codes, true);
	}

	/**
	 * Should clear platform data
	 *
	 * @param $details
	 *
	 * @return bool
	 *
	 * @since 2.7/5.10
	 */
	public function is_app_permission_related($details)
	{
		$error_code = (int)$details['error']['code'];
		$error_subcode = isset($details['error']['error_subcode']) ? (int)$details['error']['error_subcode'] : 0;

		$critical_codes = array(
			190, // access token or permissions
		);

		$critical_subcodes = array(
			458, // access token or permissions
		);

		if (in_array($error_code, $critical_codes, true)) {
			if (strpos($details['error']['message'], 'user has not authorized application') !== false) {
				return true;
			}
			return in_array($error_subcode, $critical_subcodes, true);
		}

		return false;
	}

	/**
	 * Creates an array of information for easy display of API errors
	 *
	 * @param $response
	 * @param array    $connected_account
	 *
	 * @return array
	 *
	 * @since 2.7/5.10
	 */
	public function generate_error_message($response, $connected_account = array('username' => ''))
	{

		$error_message_return = array(
			'error_message' => '',
			'admin_only' => '',
			'frontend_directions' => '',
			'backend_directions' => '',
			'time' => time(),
		);
		$hash = isset($response['error']['code']) ? '#' . (int)$response['error']['code'] : '';

		if (isset($response['response']) && is_wp_error($response['response'])) {
			$error_message_return['error_message'] = __('HTTP Error. Unable to connect to the Instagram API.', 'instagram-feed') . ' ' . __('Feed will not update.', 'instagram-feed');
			$error_message_return['admin_only'] = sprintf(__('Error connecting to %s.', 'instagram-feed'), $response['url']);

			$error_message_return['frontend_directions'] = '<a href="https://smashballoon.com/instagram-feed/docs/errors/' . $hash . '" target="_blank" rel="noopener">' . __('Directions on how to resolve this issue', 'instagram-feed') . '</a>';

			if (isset($response['response']) && isset($response['response']->errors)) {
				$num = count($response['response']->errors);
				$i = 1;
				foreach ($response['response']->errors as $key => $item) {
					$error_message_return['admin_only'] .= ' ' . $key . ' - ' . $item[0];
					if ($i < $num) {
						$error_message_return['admin_only'] .= ',';
					}
					$num++;
				}
			}

			return $error_message_return;
		}
		$hash = '#' . (int)$response['error']['code'];

		if (isset($response['error']['message'])) {
			if ((int)$response['error']['code'] === 100) {
				$error_message_return['error_message'] = __('Error: Access Token is not valid or has expired.', 'instagram-feed') . ' ' . __('Feed will not update.', 'instagram-feed');
				$error_message_return['admin_only'] = sprintf(__('API error %s:', 'instagram-feed'), $response['error']['code']) . ' ' . $response['error']['message'];
				$error_message_return['frontend_directions'] = '<a href="https://smashballoon.com/instagram-feed/docs/errors/' . $hash . '" target="_blank" rel="noopener">' . __('Directions on how to resolve this issue', 'instagram-feed') . '</a>';
			} elseif ((int)$response['error']['code'] === 18) {
				$error_message_return['error_message'] = __('Error: Hashtag limit of 30 unique hashtags per week has been reached.', 'instagram-feed');
				$error_message_return['admin_only'] = __('If you need to display more than 30 hashtag feeds on your site, consider connecting an additional business account from a separate Instagram Identity and Facebook page. Connecting an additional Instagram business account from the same Facebook page will not raise the limit.', 'instagram-feed');
				$error_message_return['frontend_directions'] = '<a href="https://smashballoon.com/instagram-feed/docs/errors/' . $hash . '" target="_blank" rel="noopener">' . __('Directions on how to resolve this issue', 'instagram-feed') . '</a>';
			} elseif ((int)$response['error']['code'] === 10) {
				$error_message_return['error_message'] = sprintf(__('Error: Connected account for the user %s does not have permission to use this feed type.', 'instagram-feed'), $connected_account['username']);
				$error_message_return['admin_only'] = __('Try using the big blue button on the "Configure" tab to reconnect the account and update its permissions.', 'instagram-feed');
				$error_message_return['frontend_directions'] = '<a href="https://smashballoon.com/instagram-feed/docs/errors/' . $hash . '" target="_blank" rel="noopener">' . __('Directions on how to resolve this issue', 'instagram-feed') . '</a>';
			} elseif ((int)$response['error']['code'] === 24) {
				$error_message_return['error_message'] = __('Error: Cannot retrieve posts for this hashtag.', 'instagram-feed');
				$error_message_return['admin_only'] = $response['error']['error_user_msg'];
				$error_message_return['frontend_directions'] = '<a href="https://smashballoon.com/instagram-feed/docs/errors/" target="_blank" rel="noopener">' . __('Directions on how to resolve this issue', 'instagram-feed') . '</a>';
			} else {
				$error_message_return['error_message'] = __('There has been a problem with your Instagram Feed.', 'instagram-feed');
				$error_message_return['admin_only'] = sprintf(__('API error %s:', 'instagram-feed'), $response['error']['code']) . ' ' . $response['error']['message'];
				$error_message_return['frontend_directions'] = '<a href="https://smashballoon.com/instagram-feed/docs/errors/' . $hash . '" target="_blank" rel="noopener">' . __('Directions on how to resolve this issue', 'instagram-feed') . '</a>';
			}
		} else {
			$error_message_return['error_message'] = __('An unknown error has occurred.', 'instagram-feed');
			$error_message_return['admin_only'] = json_encode($response);
		}
		return $error_message_return;
	}

	/**
	 * Display errors are saved with the feed cache so they will still be displayed
	 * on the frontend
	 *
	 * @param string $type
	 * @param array  $error
	 *
	 * @since 2.7/5.10
	 */
	public function maybe_set_display_error($type, $error)
	{
		if ($type === 'connection') {
			if (empty($this->display_error['connection'])) {
				$this->display_error['connection'] = $error;
			}
		} elseif ($type === 'configuration') {
			if (empty($this->display_error['configuration'])) {
				$this->display_error['configuration'] = $error;
			}
		} elseif ($type === 'hashtag') {
			$this->display_error['hashtag'][] = $error;
		} elseif ($type === 'hashtag_limit') {
			if (empty($this->display_error['connection'])) {
				$this->display_error['hashtag_limit'] = $error;
			}
		}
	}

	/**
	 * Handles errors related to hashtags.
	 *
	 * @param array  $details Details of the error.
	 * @param object $connected_account The connected Instagram account object.
	 * @param array  &$log_item Reference to the log item array to store error details.
	 *
	 * @return void
	 */
	private function handleHashtagError($details, $connected_account, &$log_item)
	{
		$hashtag_details = array(
			'error_id' => '',
			'hashtag' => isset($details['hashtag']) ? $details['hashtag'] : '',
		);

		if (isset($details['error']['code']) && (int)$details['error']['code'] === 24) {
			$hashtag_details['clear_time'] = time() + 60 * 5;
		}

		if (isset($details['error']['code'])) {
			$hashtag_details['error_id'] = $details['error']['code'];
		} elseif (isset($details['response']) && is_wp_error($details['response'])) {
			foreach ($details['response']->errors as $key => $item) {
				$hashtag_details['error_id'] = $key;
			}
		}

		if (get_the_ID() !== 0) {
			$hashtag_details['post_id'] = get_the_ID();
		}

		$hashtag_details['error_message'] = $this->generate_error_message($details, $connected_account);
		$log_item .= $hashtag_details['error_message']['admin_only'];
		$this->maybe_set_display_error('hashtag', $hashtag_details);

		$found = false;
		if (isset($details['hashtag'])) {
			foreach ($this->errors['hashtag'] as $hashtag_error_item) {
				if (
					isset($hashtag_error_item['hashtag']) &&
					strtolower($hashtag_error_item['hashtag']) === strtolower($details['hashtag']) &&
					$hashtag_error_item['error_id'] === $details['error_id']
				) {
					$found = true;
					break;
				}
			}
		}

		if (!$found) {
			$this->errors['hashtag'][] = $hashtag_details;
		}
	}

	/**
	 * Updates the error log with a new log item.
	 *
	 * @param mixed $log_item The item to be added to the error log.
	 */
	private function updateErrorLog($log_item)
	{
		$current_log = $this->errors['error_log'];
		if (is_array($current_log) && count($current_log) >= 10) {
			reset($current_log);
			unset($current_log[key($current_log)]);
		}
		$current_log[] = $log_item;
		$this->errors['error_log'] = $current_log;
		update_option('sb_instagram_errors', $this->errors, false);
	}

	/**
	 * @return mixed
	 *
	 * @since 2.7/5.10
	 */
	public function get_error_log()
	{
		return $this->errors['error_log'];
	}

	/**
	 * @return mixed
	 *
	 * @since 2.7/5.10
	 */
	public function get_action_log()
	{
		return $this->errors['action_log'];
	}

	/**
	 * @param string $type
	 *
	 * @since 2.7/5.10
	 */
	public function maybe_remove_display_error($type)
	{
		if (isset($this->display_error[$type])) {
			unset($this->display_error[$type]);
		}
	}

	/**
	 * The plugin has a limit on how many post records can be stored and
	 * images resized to avoid overloading servers. This function deletes the post that
	 * has the longest time passed since it was retrieved.
	 *
	 * @since 2.0/4.0
	 */
	public function delete_least_used_image()
	{
		global $wpdb;
		$table_name = $wpdb->prefix . SBI_INSTAGRAM_POSTS_TYPE;
		$feeds_posts_table_name = esc_sql($wpdb->prefix . SBI_INSTAGRAM_FEEDS_POSTS);

		$max = isset($this->limit) && $this->limit > 1 ? $this->limit : 1;

		$oldest_posts = $wpdb->get_results("SELECT id, media_id, mime_type FROM $table_name ORDER BY last_requested ASC LIMIT $max", ARRAY_A);

		$upload = wp_upload_dir();
		$file_suffixes = array('thumb', 'low', 'full');

		foreach ($oldest_posts as $post) {
			$extension = isset($post['mime_type']) && $post['mime_type'] === 'image/webp'
				? '.webp' : '.jpg';
			foreach ($file_suffixes as $file_suffix) {
				$file_name = trailingslashit($upload['basedir']) . trailingslashit(SBI_UPLOADS_NAME) . $post['media_id'] . $file_suffix . $extension;
				if (is_file($file_name)) {
					unlink($file_name);
				}
			}

			$wpdb->query($wpdb->prepare("DELETE FROM $table_name WHERE id = %d", $post['id']));
			$wpdb->query($wpdb->prepare("DELETE FROM $feeds_posts_table_name WHERE record_id = %d", $post['id']));
		}
	}

	/**
	 * Calculates how many records are in the database and whether or not it exceeds the limit
	 *
	 * @return bool
	 *
	 * @since 2.0/4.0
	 */
	public function max_total_records_reached()
	{
		global $wpdb;
		$table_name = $wpdb->prefix . SBI_INSTAGRAM_POSTS_TYPE;

		$num_records = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");

		if (!isset($this->limit) && (int)$num_records > SBI_MAX_RECORDS) {
			$this->limit = (int)$num_records - SBI_MAX_RECORDS;
		}

		return (int)$num_records > SBI_MAX_RECORDS;
	}

	/**
	 * The plugin caps how many new images are created in a 15 minute window to
	 * avoid overloading servers
	 *
	 * @return bool
	 *
	 * @since 2.0/4.0
	 */
	public function max_resizing_per_time_period_reached()
	{
		global $wpdb;
		$table_name = $wpdb->prefix . SBI_INSTAGRAM_POSTS_TYPE;

		$fifteen_minutes_ago = date('Y-m-d H:i:s', time() - 15 * 60);

		$num_new_records = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE created_on > '$fifteen_minutes_ago'");

		return (int)$num_new_records > 100;
	}

	/**
	 * @return bool
	 *
	 * @since 2.0/4.0
	 */
	public function image_resizing_disabled($data = false)
	{
		$options = sbi_get_database_settings();
		$disable_resizing = isset($options['sb_instagram_disable_resize']) ? $options['sb_instagram_disable_resize'] === 'on' || $options['sb_instagram_disable_resize'] === true : false;
		$disable_resizing = apply_filters('sbi_image_resizing_disabled', $disable_resizing, $data);

		if (!$disable_resizing) {
			$disable_resizing = isset($this->resizing_tables_exist) ? !$this->resizing_tables_exist : !$this->does_resizing_tables_exist();
		}

		return $disable_resizing;
	}

	/**
	 * Resets the custom tables and deletes all image files
	 *
	 * @since 2.0/4.0
	 */
	public function delete_all_sbi_instagram_posts()
	{
		$upload = wp_upload_dir();

		global $wpdb;

		$posts_table_name = $wpdb->prefix . SBI_INSTAGRAM_POSTS_TYPE;

		$image_files = glob(trailingslashit($upload['basedir']) . trailingslashit(SBI_UPLOADS_NAME) . '*'); // get all file names
		foreach ($image_files as $file) { // iterate files
			if (is_file($file)) {
				unlink($file);
			}
		}

		$connected_accounts = SB_Instagram_Connected_Account::get_all_connected_accounts();

		foreach ($connected_accounts as $account_id => $data) {
			if (isset($data['local_avatar'])) {
				unset($connected_accounts[$account_id]['local_avatar']);
			}
		}

		$options = sbi_get_database_settings();
		$options['connected_accounts'] = $connected_accounts;
		update_option('sb_instagram_settings', $options);

		// Delete tables
		$wpdb->query("DROP TABLE IF EXISTS $posts_table_name");

		$feeds_posts_table_name = esc_sql($wpdb->prefix . SBI_INSTAGRAM_FEEDS_POSTS);
		$wpdb->query("DROP TABLE IF EXISTS $feeds_posts_table_name");

		$table_name = $wpdb->prefix . 'options';

		$wpdb->query(
			"DELETE FROM $table_name
			WHERE `option_name` LIKE ('%\_transient\_\$sbi\_%')"
		);
		$wpdb->query(
			"DELETE FROM $table_name
			WHERE `option_name` LIKE ('%\_transient\_timeout\_\$sbi\_%')"
		);
		delete_option('sbi_hashtag_ids');
		delete_option('sbi_local_avatars');
		delete_option('sbi_local_avatars_info');

		$upload = wp_upload_dir();
		$upload_dir = $upload['basedir'];
		$upload_dir = trailingslashit($upload_dir) . SBI_UPLOADS_NAME;
		global $sbi_notices;
		if (!file_exists($upload_dir)) {
			$created = wp_mkdir_p($upload_dir);
			if ($created) {
				$this->remove_error('upload_dir');
				$sbi_notices->remove_notice('upload_dir');
			} else {
				$this->add_error('upload_dir', __('There was an error creating the folder for storing resized images.', 'instagram-feed') . ' ' . $upload_dir);
			}
		} else {
			$this->remove_error('upload_dir');
			$sbi_notices->remove_notice('upload_dir');
		}

		sbi_create_database_table();
	}

	/**
	 * @param string           $type
	 * @param mixed/array/bool $connected_account
	 *
	 * @since 2.0/4.0
	 */
	public function remove_error($type, $connected_account = false)
	{
		$update = false;

		if (!empty($this->errors[$type])) {
			$this->errors[$type] = array();
			$this->add_action_log('Cleared ' . $type . ' error.');
			$update = true;
		}

		if (!empty($connected_account)) {
			if ($this->remove_connected_account_error($connected_account, $type, false)) {
				$this->add_action_log('Cleared connected account error ' . $connected_account['username'] . '.');
			}

			if ($type === 'connection' && $this->remove_connected_account_error($connected_account, 'api', false)) {
				$this->add_action_log('Cleared connected account error ' . $connected_account['username'] . '.');
			}

			if (
				!empty($this->errors['revoked'])
				&& ($key = array_search($connected_account['user_id'], $this->errors['revoked'])) !== false
			) {
				unset($this->errors['revoked'][$key]);
			}

			$update = true;
		}

		if ($update) {
			update_option('sb_instagram_errors', $this->errors, false);
		}
	}

	/**
	 * Stores a time stamped string of information about
	 * actions that might lead to correcting an error
	 *
	 * @param string $log_item
	 *
	 * @since 2.7/5.10
	 */
	public function add_action_log($log_item)
	{
		$current_log = $this->errors['action_log'];

		if (is_array($current_log) && count($current_log) >= 10) {
			reset($current_log);
			unset($current_log[key($current_log)]);
		}
		$current_log[] = date('m-d H:i:s') . ' - ' . $log_item;

		$this->errors['action_log'] = $current_log;
		update_option('sb_instagram_errors', $this->errors, false);
	}

	/**
	 * @param array  $clearing_account
	 * @param string $clearing_error_type
	 * @param bool   $update
	 *
	 * @return bool
	 *
	 * @since 2.7/5.10
	 */
	public function remove_connected_account_error($clearing_account, $clearing_error_type = 'all', $update = true)
	{
		$cleared = false;
		if (!isset($this->errors['accounts']) || !isset($clearing_account['user_id'])) {
			return $cleared;
		}

		$clearing_account_id = $clearing_account['user_id'];

		foreach ($this->errors['accounts'] as $account_id => $error_types) {
			if (!SB_Instagram_Connected_Account::lookup($account_id)) {
				unset($this->errors['accounts'][$account_id]);
				continue;
			}

			foreach ($error_types as $error_type => $details) {
				if (
					(string)$account_id === (string)$clearing_account_id ||
					(isset($details['username']) && $details['username'] === $clearing_account['username']) ||
					(isset($details['access_token']) && $details['access_token'] === $clearing_account['access_token'])
				) {
					if ($error_type === $clearing_error_type || $clearing_error_type === 'all') {
						unset($this->errors['accounts'][$account_id][$error_type]);
						$cleared = true;
					}
				}
			}

			if (empty($this->errors['accounts'][$account_id])) {
				unset($this->errors['accounts'][$account_id]);
			}
		}

		if ($update) {
			update_option('sb_instagram_errors', $this->errors, false);
		}

		return $cleared;
	}

	/**
	 *
	 * @since 2.7/5.10
	 */
	public function remove_all_errors()
	{
		delete_option('sb_instagram_errors');

		sb_instagram_cron_clear_cache();
	}

	/**
	 * When an account is used to make a successful connection
	 *
	 * @since 2.7/5.10
	 */
	public function reset_api_errors()
	{
		$this->errors['connection'] = array();
		$this->errors['accounts'] = array();

		update_option('sb_instagram_errors', $this->errors, false);
		sb_instagram_cron_clear_cache();

		global $sbi_notices;
		$sbi_notices->remove_notice('critical_error');
	}

	/**
	 * @deprecated
	 */
	public function update_error_page($id)
	{
		if ($id !== 0) {
			update_option('sb_instagram_error_page', $id, false);
		}
	}

	/**
	 * @return bool
	 *
	 * @since 2.7/5.10
	 */
	public function get_error_page()
	{
		if (isset($this->errors['connection']['post_id'])) {
			return $this->errors['connection']['post_id'];
		}
		return false;
	}

	/**
	 * Get the frontend errors
	 *
	 * @param bool|object $instagram_feed The Instagram Feed object.
	 *
	 * @return array
	 * @since 2.0/5.0
	 */
	public function get_frontend_errors($instagram_feed = false)
	{
		if ($instagram_feed) {
			$cached_errors = $instagram_feed->get_cached_feed_error();
			if (!empty($cached_errors)) {
				return $cached_errors;
			}
		}

		$error_messages = array();
		if (!empty($this->display_error['connection']['error_message'])) {
			$error_messages[] = $this->display_error['connection']['error_message'];
		}
		if (!empty($this->display_error['configuration'])) {
			$error_messages[] = $this->display_error['configuration'];
		}
		if (!empty($this->display_error['hashtag'][0])) {
			$error_24 = array();
			$error_24_message = array();
			foreach ($this->display_error['hashtag'] as $hashtag_error) {
				if ($hashtag_error['error_id'] === 24) {
					if (!in_array($hashtag_error['hashtag'], $error_24, true)) {
						$error_24[] = $hashtag_error['hashtag'];
					}
					if (empty($error_24_message)) {
						$error_24_message = $hashtag_error['error_message'];
						$error_24_message['admin_only'] = str_replace($hashtag_error['hashtag'], '###', $error_24_message['admin_only']);
					}
				} else {
					$error_messages[] = $hashtag_error['error_message'];
				}
			}
			if (!empty($error_24_message)) {
				$hashtag_string = count($error_24) > 1 ? implode('", "', $error_24) : $error_24[0];
				$error_24_message['admin_only'] = str_replace('###', $hashtag_string, $error_24_message['admin_only']);
				$error_messages[] = $error_24_message;
			}
		}
		if (!empty($this->display_error['hashtag_limit'])) {
			$response = array(
				'error' => $this->display_error['hashtag_limit']['error'],
			);

			$error_messages[] = $this->generate_error_message($response);
		}

		return $error_messages;
	}

	/**
	 * @param $account
	 *
	 * @return bool
	 *
	 * @since 2.7/5.10
	 */
	public function account_over_hashtag_limit($account)
	{
		if (!isset($this->errors['accounts'][$account['user_id']])) {
			return false;
		}
		if (isset($this->errors['accounts'][$account['user_id']]['hashtag_limit'])) {
			if ($this->errors['accounts'][$account['user_id']]['hashtag_limit']['clear_time'] < time()) {
				$this->remove_connected_account_error($account, 'hashtag_limit', true);

				return false;
			} else {
				$this->maybe_set_display_error('hashtag_limit', $this->errors['accounts'][$account['user_id']]['hashtag_limit']);
				return true;
			}
		}

		return false;
	}

	/**
	 * @param $connected_account
	 *
	 * @return bool
	 *
	 * @since 2.7/5.10
	 */
	public function connected_account_has_error($connected_account)
	{
		if (!isset($connected_account['user_id'])) {
			return false;
		}
		if (empty($this->errors['accounts'])) {
			return false;
		}

		$account_id = $connected_account['user_id'];

		if (!empty($this->errors['accounts'][$account_id])) {
			foreach ($this->errors['accounts'][$account_id] as $error_key => $error_info) {
				if (strpos($error_key, 'hashtag') === false && $this->is_critical_error($error_info)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Whether or not the hashtag is unvailable in the API for some reason
	 *
	 * @param $hashtag
	 *
	 * @return bool
	 *
	 * @since 2.7/5.10
	 */
	public function hashtag_has_error($hashtag)
	{
		if (!isset($this->errors['hashtag'][0])) {
			return false;
		}

		$to_save = array();
		$changed = false;
		$return = false;

		foreach ($this->errors['hashtag'] as $hashtag_error) {
			if (
				!empty($hashtag_error['hashtag'])
				&& strtolower($hashtag_error['hashtag']) === strtolower($hashtag)
			) {
				if (!empty($hashtag_error['clear_time'])) {
					if ($hashtag_error['clear_time'] < time()) {
						$changed = true;
						$return = false;
						// clear the error, return false
					} else {
						$to_save[] = $hashtag_error;
						$this->maybe_set_display_error('hashtag', $hashtag_error);
						$return = true;
					}
				}
			} else {
				if (!empty($hashtag_error['clear_time'])) {
					if ($hashtag_error['clear_time'] < time()) {
						$changed = true;
					} else {
						$to_save[] = $hashtag_error;
					}
				} else {
					$to_save[] = $hashtag_error;
				}
			}
		}

		if ($changed) {
			$this->errors['hashtag'] = $to_save;
			update_option('sb_instagram_errors', $this->errors, false);
		}

		return $return;
	}

	/**
	 * Only some errors should cause the user to be notified using email and site health
	 *
	 * @return string
	 */
	public function get_critical_errors()
	{
		if (!$this->are_critical_errors()) {
			return '';
		}
		$accounts_revoked_string = '';
		$accounts_revoked = '';

		if ($this->was_app_permission_related_error()) {
			$accounts_revoked = $this->get_app_permission_related_error_ids();
			if (count($accounts_revoked) > 1) {
				$accounts_revoked = implode(', ', $accounts_revoked);
			} else {
				$accounts_revoked = $accounts_revoked[0];
			}
			$accounts_revoked_string = sprintf(__('Instagram Feed related data for the account(s) %s was removed due to permission for the Smash Balloon App on Facebook or Instagram being revoked. <br><br> To prevent the automated data deletion for the account, please reconnect your account within 7 days.', 'instagram-feed'), $accounts_revoked);
		}

		if (isset($this->errors['connection']['critical'])) {
			$errors = $this->get_errors();
			$error_message = '';

			if ($errors['connection']['error_id'] === 190) {
				$error_message .= '<strong>' . __('Action Required Within 7 Days', 'instagram-feed') . '</strong><br>';
				$error_message .= __('An account admin has deauthorized the Smash Balloon app used to power the Instagram Feed plugin.', 'instagram-feed');
				$error_message .= ' ' . sprintf(__('If the Instagram source is not reconnected within 7 days then all Instagram data will be automatically deleted on your website for this account (ID: %s) due to Facebook data privacy rules.', 'instagram-feed'), $accounts_revoked);
				$error_message .= __('<br><br>To prevent the automated data deletion for the source, please reconnect your source within 7 days.', 'instagram-feed');
				$error_message .= '<br><br><a href="https://smashballoon.com/doc/action-required-within-7-days/?instagram&utm_campaign=instagram-free&utm_source=permissionerror&utm_medium=notice&utm_content=More Information" target="_blank" rel="noopener">' . __('More Information', 'instagram-feed') . '</a>';
			} else {
				$error_message_array = $errors['connection']['error_message'];
				$error_message .= '<strong>' . $error_message_array['error_message'] . '</strong><br>';
				$error_message .= $error_message_array['admin_only'] . '<br><br>';
				if (!empty($accounts_revoked_string)) {
					$error_message .= $accounts_revoked_string . '<br><br>';
				}
				if (!empty($error_message_array['backend_directions'])) {
					$error_message .= $error_message_array['backend_directions'];
				} else {
					$retry = '';
					if (is_admin()) {
						$retry = '<button data-url="' . get_the_permalink($this->errors['connection']['post_id']) . '" class="sbi-clear-errors-visit-page sbi-space-left sbi-btn sbi-notice-btn sbi-btn-grey">' . __('View Feed and Retry', 'instagram-feed') . '</button>';
					}
					$hash = isset($errors['connection']['error_id']) ? '#' . (int)$errors['connection']['error_id'] : '';
					$error_message .= '<div class="license-action-btns"><p class="sbi-error-directions"><a class="sbi-license-btn sbi-btn-blue sbi-notice-btn" href="https://smashballoon.com/instagram-feed/docs/errors/' . $hash . '" target="_blank" rel="noopener">' . __('Directions on how to resolve this issue', 'instagram-feed') . '</a>' . $retry . '</p></div>';
				}
			}
		} else {
			$connected_accounts = SB_Instagram_Connected_Account::get_all_connected_accounts();
			foreach ($connected_accounts as $connected_account) {
				if (
					isset($connected_account['private'])
					&& sbi_private_account_near_expiration($connected_account)
				) {
					$link_1 = '<a href="https://help.instagram.com/116024195217477/In">';
					$link_2 = '</a>';
					$error_message_array = array(
						'error_message' => __('Error: Private Instagram Account.', 'instagram-feed'),
						'admin_only' => sprintf(__('It looks like your Instagram account is private. Instagram requires private accounts to be reauthenticated every 60 days. Refresh your account to allow it to continue updating, or %1$smake your Instagram account public%2$s.', 'instagram-feed'), $link_1, $link_2),
						'frontend_directions' => '<a href="https://smashballoon.com/instagram-feed/docs/errors/#10">' . __('Click here to troubleshoot', 'instagram-feed') . '</a>',
						'backend_directions' => '',
					);
				}

				if (
					!empty($this->errors['accounts'][$connected_account['user_id']]['api']['error'])
					&& $this->is_critical_error($this->errors['accounts'][$connected_account['user_id']]['api'])
				) {
					$error_message_array = $this->generate_error_message($this->errors['accounts'][$connected_account['user_id']]['api'], $connected_account);
				}

				if (!isset($error_message) && isset($error_message_array)) {
					$error_message = $error_message_array['admin_only'] . '<br><br>';
					if (!empty($error_message_array['backend_directions'])) {
						$error_message .= $error_message_array['backend_directions'];
					} else {
						$retry = '';
						if (is_admin()) {
							$retry = '<button data-url="' . get_the_permalink($this->errors['connection']['post_id']) . '" class="sbi-clear-errors-visit-page sbi-space-left sbi-btn sbi-notice-btn sbi-btn-grey">' . __('View Feed and Retry', 'instagram-feed') . '</button>';
						}
						$error_message .= '<p class="sbi-error-directions"><a class="sbi-license-btn sbi-btn-blue sbi-notice-btn" href="https://smashballoon.com/instagram-feed/docs/errors/" target="_blank" rel="noopener">' . __('Directions on how to resolve this issue', 'instagram-feed') . '</a>' . $retry . '</p>';
					}
				}
			}
		}
		if (isset($error_message)) {
			$error_message = str_replace('Please read the Graph API documentation at https://developers.facebook.com/docs/graph-api', '', $error_message);
		} else {
			$error_message = '';
		}

		return $error_message;
	}

	/**
	 * Whether or not there is at least one critical error
	 *
	 * @return bool
	 */
	public function are_critical_errors()
	{
		if (isset($this->errors['connection']['critical'])) {
			return true;
		} else {
			$connected_accounts = SB_Instagram_Connected_Account::get_all_connected_accounts();
			foreach ($connected_accounts as $connected_account) {
				if (
					isset($connected_account['private'])
					&& sbi_private_account_near_expiration($connected_account)
				) {
					return true;
				}

				$user_id = !empty($connected_account['user_id']) ? $connected_account['user_id'] : 0;
				$user_id = empty($user_id) && !empty($connected_account['account_id']) ? $connected_account['account_id'] : 0;

				if (isset($this->errors['accounts'][$user_id]['api']) && isset($this->errors['accounts'][$user_id]['api']['error'])) {
					return $this->is_critical_error($this->errors['accounts'][$user_id]['api']);
				}
			}
		}

		return false;
	}

	/**
	 * Whether or not there was a platform data clearing error
	 *
	 * @return bool
	 */
	public function was_app_permission_related_error()
	{
		return !empty($this->errors['revoked']);
	}

	public function get_app_permission_related_error_ids()
	{
		return $this->errors['revoked'];
	}

	/**
	 * @return array
	 *
	 * @since 2.0/4.0
	 */
	public function get_errors()
	{
		return $this->errors;
	}

	/**
	 * @since 2.0/5.0
	 */
	public function reset_frontend_errors()
	{
		$this->display_error = array();
	}

	/**
	 * Remove all API request delays, triggered after saving settings
	 *
	 * @since 2.7/5.10
	 */
	public function clear_api_request_delays()
	{
		if (
			empty($this->errors['accounts'])
			&& empty($this->errors['hashtag'])
		) {
			return;
		}

		$changed = false;
		foreach ($this->errors['accounts'] as $account_id => $account_error) {
			if (!empty($account_error['api']['clear_time'])) {
				$this->errors['accounts'][$account_id]['api']['clear_time'] = 0;
			}
			$changed = true;
		}

		foreach ($this->errors['hashtag'] as $key => $hashtag_error) {
			if (!empty($hashtag_error['hashtag']) && !empty($hashtag_error['clear_time'])) {
				$this->errors['hashtag'][$key]['clear_time'] = 0;
				$changed = true;
			}
		}

		if ($changed) {
			update_option('sb_instagram_errors', $this->errors, false);
		}
	}

	/**
	 * @since 2.0/5.1.2
	 */
	public function are_current_api_request_delays($connected_account)
	{
		if (empty($this->errors['accounts'])) {
			return false;
		}
		$account_id = $connected_account['user_id'];

		$is_delay = false;
		if (isset($this->errors['accounts'][$account_id]['api']) && !empty($this->errors['accounts'][$account_id]['api']['clear_time'])) {
			if ($this->errors['accounts'][$account_id]['api']['clear_time'] < time()) {
				$is_delay = false;
			} else {
				$is_delay = true;
			}
		}

		return apply_filters('sbi_is_api_delay', $is_delay);
	}

	/**
	 * Delete any data associated with the Instagram API and the
	 * connected account being deleted.
	 *
	 * @param $to_delete_connected_account
	 */
	public function delete_platform_data($to_delete_connected_account)
	{

		$are_other_business_accounts = false;
		$all_connected_accounts = SB_Instagram_Connected_Account::get_all_connected_accounts();

		$to_update = array();
		foreach ($all_connected_accounts as $connected_account) {
			if ((int)$connected_account['user_id'] !== (int)$to_delete_connected_account['user_id']) {
				$to_update[$connected_account['user_id']] = $connected_account;

				if (
					isset($connected_account['type'])
					&& $connected_account['type'] === 'business'
				) {
					$are_other_business_accounts = true;
				}
			}
		}

		SB_Instagram_Connected_Account::update_connected_accounts($to_update);

		SBI_Db::delete_source_by_account_id($to_delete_connected_account['user_id']);

		$manager = new SB_Instagram_Data_Manager();

		$manager->delete_caches();
		$manager->delete_comments_data();

		if (empty($to_update) || !$are_other_business_accounts) {
			$manager->delete_hashtag_data();
		} else {
			$manager->delete_non_hashtag_sbi_instagram_posts($to_delete_connected_account['username']);
		}
	}
}
