<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

use Firebase\JWT\JWT;
use PixelCaffeine\Admin\Exception\FBAPIException;
use PixelCaffeine\Admin\Exception\FBAPILoginException;
use Symfony\Component\Yaml\Yaml;

/**
 * @class AEPC_Facebook_Adapter
 */
class AEPC_Facebook_Adapter {

	const FEED_SCHEDULE_INTERVAL_HOURLY = 'HOURLY';
	const FEED_SCHEDULE_INTERVAL_WEEKLY = 'WEEKLY';
	const FEED_SCHEDULE_INTERVAL_DAILY = 'DAILY';
	const FEED_SCHEDULE_WEEK_DAY_SUNDAY = 'SUNDAY';
	const FEED_SCHEDULE_WEEK_DAY_MONDAY = 'MONDAY';
	const FEED_SCHEDULE_WEEK_DAY_TUESDAY = 'TUESDAY';
	const FEED_SCHEDULE_WEEK_DAY_WEDNESDAY = 'WEDNESDAY';
	const FEED_SCHEDULE_WEEK_DAY_THURSDAY = 'THURSDAY';
	const FEED_SCHEDULE_WEEK_DAY_FRIDAY = 'FRIDAY';
	const FEED_SCHEDULE_WEEK_DAY_SATURDAY = 'SATURDAY';

	const CA_RULE_AND = 'and';
	const CA_RULE_OR = 'or';
	const CA_RULE_CONTAINS = 'i_contains';
	const CA_RULE_NOT_CONTAINS = 'i_not_contains';
	const CA_RULE_EQ = 'eq';
	const CA_RULE_NEQ = 'neq';
	const CA_RULE_GTE = 'gte';
	const CA_RULE_GT = 'gt';
	const CA_RULE_LT = 'lt';
	const CA_RULE_LTE = 'lte';

	/** @var null The token generated by FB for authorization */
	public $access_token = false;

	/** @var string The account ID */
	protected $account_id = '';

	/** @var string The pixel ID */
	protected $pixel_id = '';

	/** @var string The business ID */
	protected $business_id = '';

	/** @var string The base API URL */
	protected $api_url = 'https://wca-connector.adespresso.com';

	/** @var string The base API URL */
	protected $api_stage = 'prod';

	/**
	 * Init facebook adapter
	 */
	public function __construct() {

		// Get the access token and connect the website to facebook api through access token
		add_action( 'admin_init', array( $this, 'connect' ), 5 );

		// Get the auth-response request to save the access token and login the site for the facebook requests
		add_action( 'admin_init', array( $this, 'login' ), 5 );

		// Remove the access token to disconnect the site from facebook api and force the user to make a new login
		add_action( 'admin_init', array( $this, 'disconnect' ), 5 );
	}

	/**
	 * Define access token, where we can know if the user did login or not
	 */
	public function connect() {
		// Load local configuration, if defined the file resources/fb.yml
		if ( file_exists( PixelCaffeine()->plugin_path() . '/includes/resources/fb.dev.yml' ) && class_exists( '\Symfony\Component\Yaml\Yaml' ) ) {
			$local_config = Yaml::parse( file_get_contents( PixelCaffeine()->plugin_path() . '/includes/resources/fb.dev.yml' ) );
			$this->api_url = $local_config['fb']['api_url'];
			$this->api_stage = $local_config['fb']['api_stage'];
		}

		// Get access token, it means logged in to facebook if it's not empty
		$token = get_option( 'aepc_fb_access_token' );

		// Save access token and then login only if the token is not expired
		if ( ! empty( $token ) && ! $this->is_token_expired( $token ) ) {
			$this->access_token = $token['access_token'];

			// Set account and pixel ID from option set by user
			$this->set_account_id( get_option( 'aepc_account_id', '' ) );
			$this->set_pixel_id( get_option( 'aepc_pixel_id', '' ) );
			$this->set_business_id( get_option( 'aepc_business_id', '' ) );
		}
	}

	/**
	 * Check if token expired or it's lifetime if token_expiration is 0
	 *
	 * @param $token
	 *
	 * @return bool
	 */
	protected function is_token_expired( $token ) {
		return $token['token_expiration'] > 0 && time() >= $token['token_expiration'];
	}

	/**
	 * Get the complete URL of the request to endpoint defined in parameter
	 *
	 * @param string $endpoint It must start with /
	 * @param array $args The array of arguments for request
	 *
	 * @return string
	 */
	protected function get_request_api_url( $endpoint, $args = array() ) {
		return add_query_arg( $args, $this->api_url . '/' . $this->api_stage . $endpoint );
	}

	/**
	 * Return the URL where make the login to facebook
	 *
	 * @return string
	 */
	public function get_login_url() {
		return $this->get_request_api_url( '/auth-request', array(
			'site_id' => $this->get_uuid(),
			'redirect_uri' => urlencode( add_query_arg( 'action', 'fb-login', AEPC_Admin::get_page('general-settings')->get_view_url() ) )
		) );
	}

	/**
	 * Return the URL where make the logout to facebook
	 *
	 * @return string
	 */
	public function get_logout_url() {
		return wp_nonce_url( add_query_arg( 'action', 'fb-disconnect', AEPC_Admin::get_page('general-settings')->get_view_url() ), 'facebook_disconnect' );
	}

	/**
	 * Decode the token passed in parameter and save own access token and expiration
	 *
	 * @param $token
	 *
	 * @throws Exception If the token is empty or expired
	 */
	public function doLogin( $token ) {
		// Token empty doesn't allowed
		if ( empty( $token ) ) {
			throw new FBAPILoginException( __( 'Empty Token', 'pixel-caffeine' ), 400 );
		}

		try {
			JWT::$leeway = defined( 'AEPC_LEEWAY' ) ? AEPC_LEEWAY : 3600;
			$jwt = (array) JWT::decode( $token, $this->get_uuid(), array( 'HS256' ) );

			// Check if token is expired
			if ( $this->is_token_expired( $jwt ) ) {
				$this->set_disconnected( 'expired' );
				throw new FBAPILoginException( __( 'Expired Token', 'pixel-caffeine' ), 408 );
			}

			delete_option( 'aepc_fb_access_expired' );
			update_option( 'aepc_fb_access_token', $jwt );
			$this->access_token = $jwt['access_token'];

		} catch ( Firebase\JWT\BeforeValidException $e ) {
			$message = __( 'Invalid login, please try again.', 'pixel-caffeine' );

			// Debug information
			if ( PixelCaffeine()->is_debug_mode() ) {
				$message .= '<br /><strong>DEBUG:</strong> ' . $e->getCode() . ' - ' . $e->getMessage();
			}

			throw new FBAPILoginException( $message, 400 );
		}
	}

	/**
	 * Get the auth-response request to save the access token and login the site for the facebook requests
	 */
	public function login() {
		if (
			empty( $_GET['action'] )
			|| 'fb-login' != $_GET['action']
			|| ( empty( $_GET['token'] ) && empty( $_GET['error'] ) )
			|| ! current_user_can( 'manage_ads' )
		) {
			return;
		}

		// Refresh the page to remove query args
		$to = get_option( 'aepc_account_id', '' ) == '' ? AEPC_Admin::get_page( 'general-settings' )->get_view_url() : false;

		try {
			if ( isset( $_GET['error'] ) ) {
				$error = sanitize_text_field( $_GET['error'] );
				$error_description = sanitize_text_field( $_GET['error_description'] );

				if ( 'access_denied' == $error ) {
					$error_description = __( 'Please, grant the permissions requested', 'pixel-caffeine' );
				} else if ( 'permissions_denied' == $error ) {
					$error_description = sprintf( __( 'Please, grant the permissions: %s', 'pixel-caffeine' ), $error_description );
				}

				throw new FBAPILoginException( sprintf( '<strong>%s</strong> %s', __( 'Facebook authentication error', 'pixel-caffeine' ), $error_description ), 401 );
			}

			$this->doLogin( $_GET['token'] );

			// Force to refresh the user data
			$this->get_user( true );

			// If an account ID already saved, refresh the business ID if not yet
			if ( get_option( 'aepc_account_id', '' ) != '' ) {
				AEPC_Admin::save_business_id( $this->get_business_id_from_account_id() );
			}

			if ( false === $to ) {
				AEPC_Admin_Notices::add_notice( 'success', 'main', __( '<strong>Facebook connected</strong> Your facebook account is properly connected now.', 'pixel-caffeine' ) );
			}
		} catch( Exception $e ) {
			AEPC_Admin_Notices::add_notice( 'error', 'main', $e->getMessage() );
		}

		wp_redirect( add_query_arg( 'ref', 'fblogin', remove_query_arg( array( 'action', 'token' ), $to ) ) );
		exit();
	}

	/**
	 * Set access_token to false and delete the option saved on database, so in the next connect any access token will
	 * be saved and then the site won't logged in
	 *
	 * @param string $why 'logout' or 'expired'
	 */
	public function set_disconnected( $why = 'logout' ) {
		$this->access_token = false;
		delete_option( 'aepc_fb_access_token' );
		update_option( 'aepc_fb_access_expired', 'expired' === $why );
		delete_transient( 'aepc_fb_user' );
	}

	/**
	 * Disconnect the user removing the saved access token
	 */
	public function disconnect() {
		if (
			empty( $_GET['action'] )
			|| 'fb-disconnect' != $_GET['action']
			|| empty( $_GET['_wpnonce'] )
			|| ! wp_verify_nonce( $_GET['_wpnonce'], 'facebook_disconnect' )
			|| ! current_user_can( 'manage_ads' )
		) {
			return;
		}

		try {
			$this->set_disconnected();
			delete_option( 'aepc_account_id' );
			delete_option( 'aepc_pixel_id' );
			delete_option( 'aepc_business_id' );

		} catch( Exception $e ) {
			AEPC_Admin_Notices::add_notice( 'error', 'main', $e->getMessage() );
		}

		if ( defined( 'DOING_AJAX' ) && DOING_AJAX || isset( $_GET['ajax'] ) && 1 == $_GET['ajax'] ) {
			wp_send_json_success();
		}

		// Refresh the page to remove the query args
		wp_redirect( remove_query_arg( array( 'ref', 'action', '_wpnonce' ) ) );
		exit();
	}

	/**
	 * Set the access token in the instance
	 *
	 * @param $access_token
	 */
	public function set_access_token( $access_token ) {
		$this->access_token = $access_token;
	}

	/**
	 * Check if authorization is right
	 *
	 * @return bool
	 */
	public function is_logged_in() {
		return ! empty( $this->access_token );
	}

	/**
	 * Check if the user has token expired or the connection is lost
	 *
	 * @return mixed|void
	 */
	public function is_expired() {
		return get_option( 'aepc_fb_access_expired' );
	}

	/**
	 * Get the Account ID
	 *
	 * @return string
	 */
	public function get_account_id() {
		return $this->account_id;
	}

	/**
	 * Set the Account ID
	 *
	 * @param $account_id
	 */
	public function set_account_id( $account_id ) {
		$this->account_id = ! empty( $account_id ) ? (string) $account_id : '';
	}

	/**
	 * Get the Pixel ID
	 *
	 * @return string
	 */
	public function get_pixel_id() {
		return $this->pixel_id;
	}

	/**
	 * Set the Pixel ID
	 *
	 * @param $pixel_id
	 */
	public function set_pixel_id( $pixel_id ) {
		$this->pixel_id = ! empty( $pixel_id ) ? (string) $pixel_id : '';
	}

	/**
	 * Get the Business ID
	 *
	 * @return string
	 */
	public function get_business_id() {
		return $this->business_id;
	}

	/**
	 * Set the Business ID
	 *
	 * @param $business_id
	 */
	public function set_business_id( $business_id ) {
		$this->business_id = $business_id;
	}

	// FB Methods

	/**
	 * Make the facebook api requesto to connector
	 *
	 * @param string $method The Facebook API endpoint to call.
	 * @param string $endpoint The http method to use for the facebook API call (GET, POST, DELETE).
	 * @param array $args The list of GET or POST parameters to pass to the Facebook API call (without the fb access_token).
	 * @param bool $cache Cache the request if another identical request is made, avoid call to facebook api with the same request.
	 *
	 * @return array|WP_Error
	 * @throws Exception
	 */
	public function request( $method, $endpoint, $args = array(), $cache = true ) {
		if ( ! $this->is_logged_in() ) {
			throw new FBAPIException( __( 'Please, connect your facebook account to make operations to the custom audiences of your Ad account.', 'pixel-caffeine' ), 401 );
		}

		// Key to set/get from cache an already made request
		$cache_key = 'fb_call_' . md5( $method . $endpoint . serialize( $args ) );

		$payload = apply_filters( 'aepc_facebook_request_payload', array(
			'endpoint' => $endpoint,
			'method' => $method,
			'auth_token' => $this->access_token,
			'parameters' => $args
		), $method, $endpoint, $this );

		if ( ! $cache || false === ( $response = wp_cache_get( $cache_key ) ) ) {
			// Do request
			$response = wp_remote_post( $this->get_request_api_url( '/request' ), array(
				'timeout' => 15,
				'headers' => array( 'content-type' => 'application/json' ),
				'body' => json_encode( array(
					'request' => JWT::encode( $payload, $this->get_uuid() )
				) )
			) );

			wp_cache_set( $cache_key, $response );
		}

		// Check about errors and throw Exception if one
		$this->check_error_messages( $response, $payload );

		return $response;
	}

	/**
	 * Check if the response contains some error message and throw an exception
	 *
	 * @param $response
	 * @param $payload
	 *
	 * @throws Exception
	 */
	protected function check_error_messages( $response, $payload ) {
		$body = json_decode( wp_remote_retrieve_body( $response ) );
		$message = $code = '';
		$general_code = 400;
		$general_error = sprintf(
			__( 'Something went wrong during the connection with Facebook API. Please, try to %1$srefresh the page%2$s.', 'pixel-caffeine' ),
			'<a href="' . ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : add_query_arg( null, null ) ) . '">',
			'</a>'
		);

		// Check if request fails with wp_error response
		if ( is_wp_error( $response ) ) {
			/** @var WP_Error $response */
			$error_code = $response->get_error_code();
			$error_message = $response->get_error_message();

			// Define error messages
			$wp_error = array(
				'http_request_failed' => __( 'Cannot save on facebook account because of something gone wrong during facebook connection.', 'pixel-caffeine' )
			);

			// Particlar case with cUrl version not supported
			if ( false !== strpos( $error_message, 'cURL error 35' ) ) {
				$error_code = 'curl_error';
				$wp_error[ $error_code ] = __( 'The request goes in error from your server due by some oldest version of "cUrl" package. Please, ask to your hosting to upgrade it in order to fix it. HINT: enable the "debug mode" by Advanced Settings on bottom of this page. Then refresh and copy the entire message for your hosting support service, to give their more details about the issue.', 'pixel-caffeine' );
			}

			$message = isset( $wp_error[ $error_code ] ) ? $wp_error[ $error_code ] : $general_error;

			// Debug information
			if ( PixelCaffeine()->is_debug_mode() ) {
				$message .= '<br /><strong>DEBUG:</strong> ' . $error_code . ' - ' . $error_message;
			}
		}

		// Check errors from HTTP request
		elseif ( wp_remote_retrieve_response_code( $response ) >= 400 ) {

			// Define error messages
			$http_messages = array(
				408 => sprintf( __( 'Facebook connection timed out. Please, login again from %shere%s', 'pixel-caffeine' ), '<a href="' . AEPC_Admin::get_page('general-settings')->get_view_url() . '" target="_blank">', '</a>' )
			);

			$code = wp_remote_retrieve_response_code( $response );
			$message = isset( $http_messages[ $code ] ) ? $http_messages[ $code ] : $general_error;

			// Debug information
			if ( PixelCaffeine()->is_debug_mode() && isset( $body->errorMessage ) ) {
				$message .= '<br /><strong>DEBUG:</strong> ' . trim( str_replace( $code, '', $body->errorMessage ) );
			}

			// Run custom actions if occurred an error
			if ( apply_filters( 'aepc_fb_requests_needs_refresh', true ) && in_array( $code, array( 408 ) ) ) {
				$this->set_disconnected( 'expired' );
				wp_send_json_error( array( 'refresh' => true ) );
			}
		}

		// Check errors in body for facebook api error
		elseif ( isset( $body->error ) ) {

			// Define errors to show for user
			$fb_errors = array(
				190 => sprintf( __( 'Facebook connection timed out or you need to authorize again. Please, login again from %shere%s', 'pixel-caffeine' ), '<a href="' . AEPC_Admin::get_page('general-settings')->get_view_url() . '" target="_blank">', '</a>' ),
				200 => sprintf( __( 'Permission error: %s', 'pixel-caffeine' ), $body->error->message ),
				272 => __( 'This Ads action requires the user to be admin of the application.', 'pixel-caffeine' ),
				273 => __( 'This Ads action requires the user to be admin of the ad account.', 'pixel-caffeine' ),
				274 => __( 'The ad account is not enabled for usage in Ads API. Please add it in developers.facebook.com/apps -> select your app -> settings -> advanced -> advertising accounts -> Ads API.', 'pixel-caffeine' ),
				275 => __( 'The facebook object you are trying to use does not exist.', 'pixel-caffeine' ),
				278 => __( 'You have to extend permission for ads_read to complete the action.', 'pixel-caffeine' ),
				294 => __( 'You have to extend permission for ads_management to complete the action.', 'pixel-caffeine' ),
				2650 => __( 'Failed to update the custom audience.', 'pixel-caffeine' ),
				2651 => __( 'Failed to create lookalike custom audience.', 'pixel-caffeine' ),
				2654 => __( 'Failed to create custom audience on your facebook account.', 'pixel-caffeine' ),
				2655 => make_clickable( __( 'Terms of service has not been accepted. To accept, go to https://www.facebook.com/ads/manage/customaudiences/tos.php', 'pixel-caffeine' ) ),
				2656 => __( 'Failed to delete custom audience because associated lookalikes exist.', 'pixel-caffeine' ),
				2663 => make_clickable( __( 'Terms of service has not been accepted. To accept, go to https://www.facebook.com/customaudiences/app/tos', 'pixel-caffeine' ) ),
				2664 => make_clickable( __( 'The corporate terms of service has not been accepted. To accept, go to https://business.facebook.com/ads/manage/customaudiences/tos.php', 'pixel-caffeine' ) ),
				16000 => __( 'Specified audience is too small.', 'pixel-caffeine' ),
			);

			// Ignore message if delete action
			if ( 'DELETE' === $payload['method'] && in_array( $body->error->code, array( 100, 275 ) ) ) {
				return;
			}

			$message = isset( $fb_errors[ $body->error->code ] ) ? $fb_errors[ $body->error->code ] : $general_error;

			// Debug information
			if ( PixelCaffeine()->is_debug_mode() ) {
				$message .= '<br /><strong>DEBUG:</strong> ' . $body->error->code . ' - ' . $body->error->type . ' - ' . $body->error->message . ' (fbtrace_id: ' . $body->error->fbtrace_id . ')';
			}

			// Run custom actions if occurred an error
			if ( apply_filters( 'aepc_fb_requests_needs_refresh', true ) && in_array( $body->error->code, array( 3, 190, 278, 294 ) ) ) {
				$this->set_disconnected( 'expired' );
				wp_send_json_error( array( 'refresh' => true ) );
			}
		}

		// Throw the exception if some error was found
		if ( ! empty( $message ) ) {
			$exception_code = empty( $code ) ? $general_code : $code;
			throw new FBAPIException( $message, $exception_code, $response );
		}
	}

	/**
	 * Get the current user
	 *
	 * @param bool $force Force to get info from Facebook API
	 *
	 * @return array|mixed|object|WP_Error
	 */
	public function get_user( $force = false ) {
		try {
			if ( $force || false === ( $user = get_transient( 'aepc_fb_user' ) ) ) {
				$response = $this->request( 'GET',
					'/me',
					array(
						'fields' => 'id,name,picture,permissions'
					) );

				$user = json_decode( wp_remote_retrieve_body( $response ) );

				if ( empty( $user ) ) {
					throw new Exception( '', 'http_request_failed' );
				}

				// Save permissions granted
				$permissions = array();
				foreach ( $user->permissions->data as $rule ) {
					if ( 'granted' === $rule->status ) {
						$permissions[] = $rule->permission;
					}
				}
				$user->permissions = $permissions;

				set_transient( 'aepc_fb_user', $user, WEEK_IN_SECONDS );
			}
		} catch ( Exception $e ) {
			$user = new WP_Error( $e->getCode(), $e->getMessage() );
		}

		return $user;
	}

	/**
	 * Get the user ID of user logged in
	 *
	 * @return mixed
	 */
	public function get_user_id() {
		$user = $this->get_user();

		return $user->id;
	}

	/**
	 * Get the display name of user logged in
	 *
	 * @return mixed
	 */
	public function get_user_name() {
		$user = $this->get_user();

		return $user->name;
	}

	/**
	 * Get the photo url of user current logged in
	 *
	 * @return mixed
	 */
	public function get_user_photo_uri() {
		$user = $this->get_user();

		return $user->picture->data->url;
	}

	/**
	 * Check if a permission is granted in the current login
	 *
	 * @param string|array $permissions Permission(s) to check
	 *
	 * @return bool
	 */
	public function is_permission_granted( $permissions ) {
		$user = $this->get_user();

		if ( ! isset( $user->permissions ) ) {
			$user = $this->get_user( true );
		}

		$permissions = (array) $permissions;
		foreach ( $permissions as $permission ) {
			if ( in_array( $permission, $user->permissions ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Return an array with all account ids of the user
	 *
	 * @return object[]
	 */
	public function get_account_ids() {
		$response = $this->request( 'GET', '/' . $this->get_user_id() . '/adaccounts', array(
			'fields' => 'account_id,name',
			'limit' => apply_filters( 'aepc_facebook_request_accounts_limit', 100 )
		) );

		$accounts = json_decode( wp_remote_retrieve_body( $response ) );
		return $accounts->data;
	}

	/**
	 * Get the name of account, checking on option before (set on facebook options saving)
	 *
	 * @param string $account_id
	 *
	 * @return mixed|null
	 */
	public function get_account_name( $account_id = '' ) {
		if ( empty( $account_id ) && ! ( $account_id = $this->get_account_id() ) ) {
			return null;
		}

		if ( false === ( $account_name = get_transient( 'aepc_account_name_' . $account_id ) ) ) {
			$accounts = $this->get_account_ids();
			foreach ( $accounts as $account ) {
				if ( $account->account_id == $account_id ) {
					$account_name = $account->name;
					break;
				}
			}

			set_transient( 'aepc_account_name_' . $account_id, $account_name );
		}

		return $account_name;
	}

	/**
	 * Get the business ID from User ID and Ad Account ID
	 *
	 * @return int
	 */
	public function get_business_id_from_account_id() {
		$business_id = '';

		if ( ! ( $account_id = $this->get_account_id() ) || ! ( $user = $this->get_user() ) ) {
			return $business_id;
		}

		$response = $this->request( 'GET', '/act_' . $account_id, ['fields' => 'business'] );
		$data = json_decode( wp_remote_retrieve_body( $response ) );

		if ( ! empty( $data->business->id ) ) {
			$business_id = $data->business->id;
		}

		return $business_id;
	}

	/**
	 * Return an array with all pixel ids of the user
	 *
	 * @param string $account_id
	 *
	 * @return object[]
	 */
	public function get_pixel_ids( $account_id = '' ) {
		if ( empty( $account_id ) && ! ( $account_id = $this->get_account_id() ) ) {
			return null;
		}

		try {
			$response = $this->request('GET', '/act_' . $account_id . '/adspixels', [
				'fields' => 'id,name',
				'limit'  => apply_filters('aepc_facebook_request_pixels_limit', 100),
			]);
		} catch (Exception $e) {
			return array();
		}

		$pixels = json_decode( wp_remote_retrieve_body( $response ) )->data;

		if ( $business_id = $this->get_business_id_from_account_id() ) {
			try {
				$response = $this->request('GET', '/' . $business_id . '/adspixels', [
					'fields' => 'id,name',
					'limit'  => apply_filters('aepc_facebook_request_pixels_limit', 100),
				]);

				// Add into the list only if it's not been fetched before
				foreach ( json_decode(wp_remote_retrieve_body($response))->data as $pixel ) {
					if ( ! in_array( $pixel->id, wp_list_pluck($pixels, 'id') ) ) {
						$pixels[] = $pixel;
					}
				}
			} catch (Exception $e) {
			}
		}

		return $pixels;
	}

	/**
	 * Return an array with all pixel ids of the user
	 *
	 * @param string $account_id
	 *
	 * @return object[]
	 */
	public function get_offsite_pixel_ids( $account_id = '' ) {
		if ( empty( $account_id ) && ! ( $account_id = $this->get_account_id() ) ) {
			return null;
		}

		$response = $this->request( 'GET', '/act_' . $account_id . '/offsitepixels', array(
			'fields' => 'id,name',
			'limit' => apply_filters( 'aepc_facebook_request_offsite_pixels_limit', 100 )
		) );

		$pixels = json_decode( wp_remote_retrieve_body( $response ) );
		return $pixels->data;
	}

	/**
	 * Get the name of account, checking on option before (set on facebook options saving)
	 *
	 * @param string $pixel_id
	 *
	 * @return mixed|null
	 */
	public function get_pixel_name( $pixel_id = '' ) {
		if ( empty( $pixel_id ) && ! ( $pixel_id = $this->get_pixel_id() ) ) {
			return null;
		}

		if ( false === ( $pixel_name = get_transient( 'aepc_pixel_name_' . $pixel_id ) ) ) {
			try {
				$response = $this->request( 'GET', '/' . $pixel_id, array( 'fields' => 'name' ) );
				$pixel = json_decode( wp_remote_retrieve_body( $response ) );
				$pixel_name = $pixel->name;
			} catch ( Exception $e ) {
				$pixel_name = null;
			}

			set_transient( 'aepc_pixel_name_' . $pixel_id, $pixel_name );
		}

		return $pixel_name;
	}

	/**
	 * Get Pixel statistic of an account ID
	 *
	 * @param array $args
	 * @param string $pixel_id If not defined, get the pixel ID set
	 *
	 * @return array
	 */
	public function get_pixel_stats( $args = array(), $pixel_id = '' ) {
		if ( empty( $pixel_id ) && ! ( $pixel_id = $this->get_pixel_id() ) ) {
			return null;
		}

		$args = wp_parse_args( $args, array(
			'aggregation' => 'pixel_fire',
			'start_time' => time() - 2 * WEEK_IN_SECONDS,
			'end_time' => time()
		) );

		$response = $this->request( 'GET', '/' . $pixel_id . '/stats', $args );

		$pixels = json_decode( wp_remote_retrieve_body( $response ) );
		return (array) $pixels->data;
	}

	/**
	 * Return an array with all product catalogs ids of a business ID
	 *
	 * @param string $business_id
	 *
	 * @return object[]
	 */
	public function get_product_catalogs( $business_id = '' ) {
		if ( empty( $business_id ) && ! ( $business_id = $this->get_business_id() ) ) {
			return null;
		}

		$owned_response = $this->request( 'GET', '/' . $business_id . '/owned_product_catalogs', array(
			'limit' => apply_filters( 'aepc_facebook_request_product_catalogs_limit', 100 )
		) );

		$client_response = $this->request( 'GET', '/' . $business_id . '/client_product_catalogs', array(
			'limit' => apply_filters( 'aepc_facebook_request_product_catalogs_limit', 100 )
		) );

		$owned_product_catalogs = json_decode( wp_remote_retrieve_body( $owned_response ) )->data;
		$client_product_catalogs = json_decode( wp_remote_retrieve_body( $client_response ) )->data;

		return array_merge( $owned_product_catalogs, $client_product_catalogs );
	}

	/**
	 * Return an array with all product feeds ids of a product catalog ID
	 *
	 * @param string $product_catalog_id
	 *
	 * @return object[]
	 */
	public function get_product_feeds( $product_catalog_id ) {
		if ( empty( $product_catalog_id ) ) {
			return null;
		}

		$response = $this->request( 'GET', '/' . $product_catalog_id . '/product_feeds', array(
			'limit' => apply_filters( 'aepc_facebook_request_product_feeds_limit', 100 )
		) );

		$product_catalogs = json_decode( wp_remote_retrieve_body( $response ) );
		return $product_catalogs->data;
	}

	/**
	 * Get the product feed info
	 *
	 * @param $product_feed_id
	 *
	 * @return object
	 */
	public function get_product_feed( $product_feed_id ) {
		if ( empty( $product_feed_id ) ) {
			return null;
		}

		$response = $this->request( 'GET', '/' . $product_feed_id, array(
			'fields' => 'schedule,name',
		) );

		return json_decode( wp_remote_retrieve_body( $response ) );
	}

	/**
	 * Create the audience in Facebook account
	 *
	 * @param array $args {
	 *     Arguments to configure the new custom audience
	 *
	 *     @type string $name The name for the cluster.
	 *     @type string $description A brief explanation of cluster.
	 *     @type int $retention Number of days to keep the user in this cluster. You can use any value between 1 and 180 days. Defaults to 14 days if not specified.
	 *     @type bool $prefill true - Include website traffic recorded prior to the audience creation. false - Only include website traffic beginning at the time of the audience creation.
	 *     @type array rule  Audience rules to be applied on the referrer URL.
	 * }
	 *
	 * @return object
	 */
	public function create_audience( $args = array() ) {
		$args = (object) wp_parse_args( $args, array(
			'name' => '',
			'description' => '',
			'retention' => 14,
			'prefill' => true,
			'rule' => array()
		) );

		$args->rule = $this->compose_rule( $args->rule, $args->retention );

		// Do request
		$response = $this->request( 'POST', '/act_' . $this->get_account_id() . '/customaudiences', array(
			'name' => $args->name,
			'description' => $args->description,
			'prefill' => (int) $args->prefill,
			'rule' => $args->rule
		) );

		// Return only the CA ID from facebook
		return json_decode( wp_remote_retrieve_body( $response ) );
	}

	/**
	 * Retrieve a custom audience created by user
	 *
	 * @param $ca_id
	 * @param string $fields List of fields you want to have
	 *
	 * @return object
	 */
	public function get_audience( $ca_id, $fields = 'name,description,approximate_count' ) {
		$fields = trim( $fields, ", \t\n\r\0\x0B" );

		// If empty, put 'id' as default
		if ( empty( $fields ) ) {
			$fields = 'id';
		}

		$response = $this->request( 'GET', '/' . $ca_id, array(
			'fields' => $fields
		) );

		return json_decode( wp_remote_retrieve_body( $response ) );
	}

	/**
	 * Retrieve all custom audiences created by user
	 *
	 * @param string $fields List of fields you want to have
	 *
	 * @return object[]
	 */
	public function get_audiences( $fields = 'name,description,approximate_count' ) {
		$fields = trim( $fields, ", \t\n\r\0\x0B" );

		// If empty, put 'id' as default
		if ( empty( $fields ) ) {
			$fields = 'id';
		}

		$response = $this->request( 'GET', '/act_' . $this->get_account_id() . '/customaudiences', array(
			'fields' => $fields
		) );

		$audiences = json_decode( wp_remote_retrieve_body( $response ) );
		return $audiences->data;
	}

	/**
	 * Update a custom audience on facebook account
	 *
	 * @param $ca_id
	 * @param array $args
	 *
	 * @return array|mixed|object
	 */
	public function update_audience( $ca_id, $args = array() ) {

		// Check keys and values
		foreach ( $args as $key => $fbparam ) {
			if ( 'retention' == $key ) {
				$args['retention_days'] = $fbparam;
				unset( $args[ $key ] );
			}
		}

		// Compone rules for facebook request
		if ( isset( $args['rule'] ) ) {
			$args['rule'] = $this->compose_rule( $args['rule'] );
		}

		// Do request
		$response = $this->request( 'POST', '/' . $ca_id, $args );

		// Return only the CA ID from facebook
		return json_decode( wp_remote_retrieve_body( $response ) );
	}

	/**
	 * Delete a custom audience on facebook account
	 *
	 * @param $ca_id
	 *
	 * @return array|mixed|object
	 */
	public function delete_audience( $ca_id ) {
		$response = $this->request( 'DELETE', '/' . $ca_id );
		return json_decode( wp_remote_retrieve_body( $response ) );
	}

	/**
	 * Get business ID of current user
	 *
	 * @return object[]
	 *
	 */
	public function get_business_ids() {
		$response = $this->request( 'GET', '/me/businesses' );

		$businesses = json_decode( wp_remote_retrieve_body( $response ) );
		return $businesses->data;
	}

	/**
	 * Create a product catalog
	 *
	 * @param string $name
	 *
	 * @return int
	 * @throws Exception
	 */
	public function create_product_catalog( $name ) {
		$business_id = $this->get_business_id();

		if ( empty( $business_id ) ) {
			throw new Exception( __( 'Unable to create a product catalog because there isn\'t any business ID associated to your Ad account.', 'pixel-caffeine' ) );
		}

		$response = $this->request( 'POST', '/' . $this->get_business_id() . '/owned_product_catalogs', array(
			'name' => $name,
		) );

		$product_catalog = json_decode( wp_remote_retrieve_body( $response ) );
		return intval( $product_catalog->id );
	}

	/**
	 * Add a product feed in a product catalog
	 *
	 * @param $product_catalog_id
	 * @param string $name
	 * @param array $config
	 */
	public function add_product_feed( $product_catalog_id, $name, array $config ) {
		$response = $this->request( 'POST', '/' . $product_catalog_id . '/product_feeds', array(
			'name' => $name,
			'schedule' => $config
		) );

		$product_feed = json_decode( wp_remote_retrieve_body( $response ) );
		return $product_feed->id;
	}

	/**
	 * Add a product feed in a product catalog
	 *
	 * @param $product_feed_id
	 * @param array $config
	 */
	public function update_product_feed( $product_feed_id, array $config ) {
		$response = $this->request( 'POST', '/' . $product_feed_id, array(
			'schedule' => $config
		) );

		$product_feed = json_decode( wp_remote_retrieve_body( $response ) );
		return $product_feed->id;
	}

	/**
	 * Associate the pixel ID to the product catalog
	 *
	 * @param $product_catalog_id
	 */
	public function associate_pixel_to_product_catalog( $product_catalog_id ) {
		$response = $this->request( 'POST', '/' . $product_catalog_id . '/external_event_sources', array(
			'external_event_sources' => array( $this->get_pixel_id() )
		) );
	}

	/**
	 * Compose the rules for facebook reuests
	 *
	 * @param array $filters
	 * @param int $retention_days Since FB Marketing API 3.0
	 *
	 * @return mixed|string|void
	 */
	public function compose_rule( $filters, $retention_days = 180 ) {
		$sample = array(
			'operator' => 'or',
			'rules' => array(
				array(
					'event_sources' => array(
						array(
							'type' => 'pixel',
							'id' => $this->get_pixel_id()
						)
					),
					'retention_seconds' => $retention_days * DAY_IN_SECONDS,
					'filter' => array(
						'operator' => 'and',
						'filters' => array()
					),
				)
			)
		);

		$final_rule = array();
		foreach ( $filters as $filter ) {
			$main_condition = strtr(
				isset( $filter['main_condition'] ) ? $filter['main_condition'] : 'include',
				array( 'include' => 'inclusions', 'exclude' => 'exclusions' )
			);

			if ( ! isset( $final_rule[ $main_condition ] ) ) {
				$final_rule[ $main_condition ] = $sample;
			}

			foreach ( $this->filter_adapter( $filter ) as $formatted_filter ) {
				$final_rule[ $main_condition ]['rules'][0]['filter']['filters'][] = $formatted_filter;
			}
		}

		return json_encode( $final_rule );
	}

	/**
	 * Format a rule with conditions for facebook request
	 *
	 * @param array $args
	 *
	 * @return array
	 */
	public function filter_adapter( $args ) {
		$filters = array();

		// Add AdvancedEvents event condition
		if (
			'attributes' == $args['event_type'] && in_array( $args['event'], array( 'login_status' ) )
			|| 'blog' == $args['event_type'] && in_array( $args['event'], array( 'categories', 'tax_post_tag', 'posts', 'pages' ) )
		) {
			$filters[] = $this->add_ca_filter('event', self::CA_RULE_CONTAINS, 'AdvancedEvents');
		}

		// Add custom fields event condition
		elseif ( 'blog' == $args['event_type'] && in_array( $args['event'], array( 'custom_fields' ) ) ) {
			$filters[] = $this->add_ca_filter('event', self::CA_RULE_CONTAINS, 'CustomFields');
		}

		// Add DPA tracking events on condition
		elseif ( 'ecommerce' == $args['event_type'] && in_array( $args['event'], array_keys( AEPC_Track::$standard_events ) ) ) {
			$filters[] = $this->add_ca_filter('event', self::CA_RULE_CONTAINS, $args['event']);
		}

		// Add custom events
		elseif ( 'events' == $args['event_type'] ) {
			$filters[] = $this->add_ca_filter('event', self::CA_RULE_EQ, $args['event']);
		}

		/**
		 * Then add specific conditions
		 */

		// Force to add conditions key when it doesn't exist
		if ( ! isset( $args['conditions'] ) ) {
			$args['conditions'] = array();
		}

		foreach ( $args['conditions'] as $k => $condition ) {

			// Skip if it's not allowed empty key and empty value
			if ( in_array( $args['event_type'], array( 'attributes', 'blog' ) ) && ( isset( $condition['key'] ) && empty( $condition['key'] ) || empty( $condition['value'] ) ) ) {
				continue;
			}

			// Add blog posts condition for object_type, post_type and object_id parameters
			if ( 'blog' == $args['event_type'] && 'posts' == $args['event'] ) {
				$filters[] = $this->add_ca_filter('post_type', self::CA_RULE_CONTAINS, $condition['key']);
				$filters[] = $this->add_ca_filter('object_type', self::CA_RULE_CONTAINS, 'single');

				// If the value contains [[any]], don't set object_id in condition, because we want to search all posts
				if ( false === strpos( $condition['value'], '[[any]]' ) ) {

					// If the value is an array, separated by comma, set 'or' condition for the values
					if ( false === strpos( $condition['value'], ',' ) ) {
						$filters[] = $this->add_ca_filter('object_id', self::CA_RULE_CONTAINS, $condition['value']);
					} else {
						$or_filters = array();
						foreach ( array_map( 'trim', explode( ',', $condition['value'] ) ) as $value ) {
							$or_filters[] = $this->add_ca_filter('object_id', $condition['operator'], $value);
						}
						$filters[] = array( 'operator' => 'or', 'filters' => $or_filters );
					}
				}
			}

			// Add blog page condition for object_type, post_type and object_id parameters
			elseif ( 'blog' == $args['event_type'] && 'pages' == $args['event'] ) {

				// If [[any]] passed into value, search only for page (all), home, blog
				if ( false !== strpos( $condition['value'], '[[any]]' ) ) {
					$or_filters = array();
					foreach ( array( 'home', 'blog', 'page' ) as $object_type ) {
						$or_filters[] = $this->add_ca_filter('object_type', self::CA_RULE_CONTAINS, $object_type );
					}
					$filters[] = array( 'operator' => 'or', 'filters' => $or_filters );
				}

				else {

					// If the value is an array, separated by comma, set 'or' condition for the values
					if ( false === strpos( $condition['value'], ',' ) ) {
						if ( in_array( $condition['value'], array( 'home', 'blog' ) ) ) {
							$filters[] = $this->add_ca_filter('object_type', self::CA_RULE_CONTAINS, $condition['value']);
						} else {
							$filters[] = $this->add_ca_filter('post_type', self::CA_RULE_CONTAINS, 'page' );
							$filters[] = $this->add_ca_filter('object_type', self::CA_RULE_CONTAINS, 'page' );
							$filters[] = $this->add_ca_filter('object_id', self::CA_RULE_CONTAINS, $condition['value'] );
						}
					} else {
						$or_filters = array();
						foreach ( array_map( 'trim', explode( ',', $condition['value'] ) ) as $value ) {
							if ( in_array( $value, array( 'home', 'blog' ) ) ) {
								$or_filters[] = $this->add_ca_filter('object_type', self::CA_RULE_CONTAINS, $value );
							} else {
								$or_filters[] = array(
									'operator' => 'and',
									'filters' => array(
										$this->add_ca_filter('post_type', self::CA_RULE_CONTAINS, 'page' ),
										$this->add_ca_filter('object_type', self::CA_RULE_CONTAINS, 'page' ),
										$this->add_ca_filter('object_id', self::CA_RULE_CONTAINS, $value ),
									)
								);
							}
						}
						$filters[] = array( 'operator' => 'or', 'filters' => $or_filters );
					}
				}
			}

			// other cases
			else {

				$param = isset( $condition['key'] ) ? $condition['key'] : $args['event'];

				// If the key is [[all]], it shouldn't have any condition
				if ( '[[any]]' != $param ) {

					// If the value is [[any]], the condition must be not equal to empty
					if ( '[[any]]' == $condition['value'] ) {
						$filters[] = $this->add_ca_filter( $param, self::CA_RULE_NEQ, '' );
					}

					// If it's array, set 'or' operator
					elseif ( false === strpos( $condition['value'], ',' ) ) {
						$filters[] = $this->add_ca_filter( $param, $condition['operator'], $condition['value'] );
					} else {
						$or_filters = array();
						foreach ( array_map( 'trim', explode( ',', $condition['value'] ) ) as $value ) {
							$or_filters[] = $this->add_ca_filter( $param, $condition['operator'], $value );
						}
						$filters[] = array( 'operator' => 'or', 'filters' => $or_filters );
					}

				}

			}
		}

		return $filters;
	}

	/**
	 * Return a common format for a specific filter of a CA rule
	 *
	 * @param string $field
	 * @param string $operator
	 * @param mixed $value
	 *
	 * @return array
	 */
	protected function add_ca_filter($field, $operator, $value) {
		return array(
			'field' => $field,
			'operator' => $operator,
			'value' => $value
		);
	}

	/**
	 * Check if the product catalog APIs can work
	 *
	 * @return bool
	 */
	public function is_product_catalog_enabled() {
		return $this->is_logged_in()
		       && $this->is_expired()
		       && $this->is_permission_granted( 'business_management' )
		       && $this->get_business_id();
	}

	/**
	 * Get the unique site ID for the
	 *
	 * @return string
	 */
	public function get_uuid() {
		if ( ! $uuid = get_option( 'aepc_fb_uuid' ) ) {
			$uuid = $this->generate_uuid();
			update_option( 'aepc_fb_uuid', $uuid );
		}

		return $uuid;
	}

	/**
	 * Generate the SITEID useful for fb request in UUIDv4
	 */
	protected function generate_uuid() {
		mt_srand( crc32( serialize( array( microtime( true ), $_SERVER['REMOTE_ADDR'] ) ) ) );

		return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
			// 32 bits for "time_low"
			mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),

			// 16 bits for "time_mid"
			mt_rand( 0, 0xffff ),

			// 16 bits for "time_hi_and_version",
			// four most significant bits holds version number 4
			mt_rand( 0, 0x0fff ) | 0x4000,

			// 16 bits, 8 bits for "clk_seq_hi_res",
			// 8 bits for "clk_seq_low",
			// two most significant bits holds zero and one for variant DCE1.1
			mt_rand( 0, 0x3fff ) | 0x8000,

			// 48 bits for "node"
			mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
		);
	}

	/**
	 * Check if the debug mode is active
	 *
	 * When the debug mode is active, the plugin shouldn't do any facebook request and give ability to use custom audiences
	 * system.
	 *
	 * @return bool
	 */
	public function is_debug() {
		return defined( 'AEPC_DEACTIVE_FB_REQUESTS' ) && AEPC_DEACTIVE_FB_REQUESTS;
	}
}

return new AEPC_Facebook_Adapter();
