<?php
/**
 * This class is serving as the base for various ad types and providing a foundation for defining common ad attributes and methods.
 *
 * @package AdvancedAds
 * @author  Advanced Ads <info@wpadvancedads.com>
 * @since   1.48.0
 */

namespace AdvancedAds\Abstracts;

use Advanced_Ads;
use Advanced_Ads_Inline_Css;
use Advanced_Ads_Utils;
use Advanced_Ads_Visitor_Conditions;
use AdvancedAds\Traits;
use AdvancedAds\Constants;
use AdvancedAds\Frontend\Stats;
use AdvancedAds\Utilities\WordPress;
use AdvancedAds\Utilities\Conditional;
use AdvancedAds\Framework\Utilities\Str;
use AdvancedAds\Interfaces\Entity_Interface;
use AdvancedAds\Compatibility\Compatibility;
use AdvancedAds\Framework\Utilities\Formatting;

defined( 'ABSPATH' ) || exit;

/**
 * Ad.
 */
abstract class Ad extends Data implements Entity_Interface {

	use Traits\Entity;
	use Traits\Wrapper;

	/**
	 * This is the object type.
	 *
	 * @var string
	 */
	protected $object_type = 'ad';

	/**
	 * The label for the ad.
	 *
	 * @var string|null
	 */
	private $label = null;

	/**
	 * Hold groups within the ad
	 *
	 * @var Group[]
	 */
	private $groups = null;

	/**
	 * Wrapper for the ad.
	 *
	 * @var array|null
	 */
	protected $wrapper = null;

	/**
	 * Core data for this object. Name value pairs (name + default value).
	 *
	 * @var array
	 */
	protected $data = [
		'title'            => '',
		'slug'             => '',
		'status'           => false,
		'author_id'        => 0,
		'content'          => '',
		'description'      => '',
		'width'            => 0,
		'height'           => 0,
		'visitors'         => [],
		'conditions'       => [],
		'has_weekdays'     => false,
		'weekdays'         => [],
		'type'             => 'dummy',
		'url'              => '',
		'expiry_date'      => false,
		'allow_php'        => false,
		'allow_shortcodes' => false,
		'reserve_space'    => false,
		'debugmode'        => false,
		'wrapper-id'       => '',
		'wrapper-class'    => '',
		'position'         => 'none',
		'clearfix'         => false,
		'margin'           => [
			'top'    => 0,
			'right'  => 0,
			'bottom' => 0,
			'left'   => 0,
		],
	];

	/**
	 * Get the ad if ID is passed, otherwise the ad is new and empty.
	 *
	 * @param Ad|WP_Post|int $ad Ad to init.
	 */
	public function __construct( $ad = 0 ) {
		parent::__construct();

		$this->set_ad_id( $ad );
		$this->data_store = wp_advads_get_ad_repository();

		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/**
	 * Set the ad ID depending on what was passed.
	 *
	 * @param Ad|WP_Post|int $ad Ad instance, post instance or numeric.
	 *
	 * @return void
	 */
	private function set_ad_id( $ad ): void {
		if ( is_numeric( $ad ) && $ad > 0 ) {
			$this->set_id( $ad );
		} elseif ( $ad instanceof self ) {
			$this->set_id( absint( $ad->get_id() ) );
		} elseif ( ! empty( $ad->ID ) ) {
			$this->set_id( absint( $ad->ID ) );
		} else {
			$this->set_object_read( true );
		}
	}

	/* Getter ------------------- */

	/**
	 * Get description.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string
	 */
	public function get_description( $context = 'view' ): string {
		return $this->get_prop( 'description', $context );
	}

	/**
	 * Get url.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string
	 */
	public function get_url( $context = 'view' ): string {
		return $this->get_prop( 'url', $context );
	}

	/**
	 * Get the expiry date of the ad.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return int
	 */
	public function get_expiry_date( $context = 'view' ): int {
		return $this->get_prop( 'expiry_date', $context );
	}

	/**
	 * Get the width of the ad in pixels.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return int
	 */
	public function get_width( $context = 'view' ): int {
		return $this->get_prop( 'width', $context );
	}

	/**
	 * Get the height of the ad in pixels.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return int
	 */
	public function get_height( $context = 'view' ): int {
		return $this->get_prop( 'height', $context );
	}

	/**
	 * Get the display conditions for the ad.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return array
	 */
	public function get_display_conditions( $context = 'view' ): array {
		return $this->get_prop( 'conditions', $context );
	}

	/**
	 * Get the visitor conditions for the ad.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return array
	 */
	public function get_visitor_conditions( $context = 'view' ): array {
		return array_filter(
			$this->get_prop( 'visitors', $context ),
			function ( $value ) {
				return 'unknown' !== $value['type'];
			}
		);
	}

	/**
	 * Get clearfix.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return bool
	 */
	public function get_clearfix( $context = 'view' ): bool {
		return $this->get_prop( 'clearfix', $context );
	}

	/**
	 * Get position.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string
	 */
	public function get_position( $context = 'view' ): string {
		return $this->get_prop( 'position', $context );
	}

	/**
	 * Get the margin setting for the ad.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return array
	 */
	public function get_margin( $context = 'view' ): array {
		return $this->get_prop( 'margin', $context );
	}

	/**
	 * Get the weight of the ad.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return int
	 */
	public function get_weight( $context = 'view' ): int {
		return $this->get_prop( 'weight', $context );
	}

	/**
	 * Retrieves the number of clicks for the ad.
	 *
	 * @return int
	 */
	public function get_clicks(): int {
		$post = get_post();

		if ( isset( $post->clicks ) ) {
			return absint( $post->clicks );
		}

		global $wpdb;
		$clicks = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				'SELECT SUM(count) FROM ' . $wpdb->prefix . 'advads_clicks WHERE ad_id = %d',
				$this->get_id()
			)
		);

		return absint( $clicks );
	}

	/**
	 * Retrieves the number of impressions for the ad.
	 *
	 * @return int
	 */
	public function get_impressions(): int {
		$post = get_post();

		if ( isset( $post->impressions ) ) {
			return absint( $post->impressions );
		}

		global $wpdb;

		$impressions = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				'SELECT SUM(count) FROM ' . $wpdb->prefix . 'advads_impressions WHERE ad_id = %d',
				$this->get_id()
			)
		);

		return absint( $impressions );
	}

	/**
	 * Retrieves the number of ctr for the ad.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return int|string
	 */
	public function get_ctr( $context = 'view' ) {
		$post = get_post();
		$ctr  = $post->ctr ?? 0;

		if ( ! $ctr && $this->get_impressions() > 0 ) {
			$ctr = $this->get_clicks() / $this->get_impressions();
		}

		return 'view' === $context ? number_format_i18n( 100 * $ctr, 2 ) : $ctr;
	}

	/* Setter ------------------- */

	/**
	 * Set display conditions.
	 *
	 * @param string $conditions Ad conditions.
	 *
	 * @return void
	 */
	public function set_display_conditions( $conditions ): void {
		$this->set_prop( 'conditions', $conditions );
	}

	/**
	 * Set conditions.
	 *
	 * @param string $conditions Ad conditions.
	 *
	 * @return void
	 */
	public function set_visitor_conditions( $conditions ): void {
		$this->set_prop( 'visitors', $conditions );
	}

	/**
	 * Set clearfix.
	 *
	 * @param string $clearfix Ad clearfix.
	 *
	 * @return void
	 */
	public function set_clearfix( $clearfix ): void {
		$this->set_prop( 'clearfix', Formatting::string_to_bool( $clearfix ) );
	}

	/**
	 * Set position.
	 *
	 * @param string $position Ad position.
	 *
	 * @return void
	 */
	public function set_position( $position ): void {
		$this->set_prop( 'position', empty( $position ) ? 'none' : $position );
	}

	/**
	 * Set margin.
	 *
	 * @param array $margin Ad margin.
	 *
	 * @return void
	 */
	public function set_margin( $margin ): void {
		$margin = [
			'top'    => intval( $margin['top'] ?? 0 ),
			'right'  => intval( $margin['right'] ?? 0 ),
			'bottom' => intval( $margin['bottom'] ?? 0 ),
			'left'   => intval( $margin['left'] ?? 0 ),
		];

		$this->set_prop( 'margin', $margin );
	}

	/**
	 * Set expiry date.
	 *
	 * @param array|string $expiry_date Ad expiry date.
	 *
	 * @return void
	 */
	public function set_expiry_date( $expiry_date ): void {
		if ( is_array( $expiry_date ) ) {
			$expiry_date = $this->normalize_expiry_date( $expiry_date );
		}

		$this->set_prop( 'expiry_date', $expiry_date );
	}

	/**
	 * Set weekdays.
	 *
	 * @param array $weekdays Ad weekdays.
	 *
	 * @return void
	 */
	public function set_weekdays( $weekdays ): void {
		$this->set_prop( 'weekdays', $weekdays );
	}

	/**
	 * Set description.
	 *
	 * @param string $description Ad description.
	 *
	 * @return void
	 */
	public function set_description( $description ): void {
		$this->set_prop( 'description', $description );
	}

	/**
	 * Set url.
	 *
	 * @param string $url Ad url.
	 *
	 * @return void
	 */
	public function set_url( $url ): void {
		global $pagenow;

		// If the tracking add-on is not active.
		// If this is not the ad edit page.
		if ( ! defined( 'AAT_VERSION' ) && ! in_array( $pagenow, [ 'post.php', 'post-new.php' ], true ) ) {
			$placeholders = [
				'[POST_ID]',
				'[POST_SLUG]',
				'[CAT_SLUG]',
				'[AD_ID]',
			];

			$url = str_replace( $placeholders, '', $url );
		}

		$this->set_prop( 'url', trim( (string) $url ) );
	}

	/**
	 * Set the width.
	 *
	 * @param float|string $width Total width.
	 */
	public function set_width( $width ) {
		$this->set_prop( 'width', '' === $width ? 0 : absint( $width ) );
	}

	/**
	 * Set the height.
	 *
	 * @param float|string $height Total height.
	 *
	 * @return void
	 */
	public function set_height( $height ): void {
		$this->set_prop( 'height', '' === $height ? 0 : absint( $height ) );
	}

	/**
	 * Set if debug mode is enabled.
	 *
	 * @param bool $enabled Is Enabled.
	 *
	 * @return void
	 */
	public function set_debugmode( $enabled ): void {
		$this->set_prop( 'debugmode', Formatting::string_to_bool( $enabled ) );
	}

	/**
	 * Set if php is allowed.
	 *
	 * @param bool|string $allowed Allowed.
	 *
	 * @return void
	 */
	public function set_allow_php( $allowed ): void {
		$this->set_prop( 'allow_php', Formatting::string_to_bool( $allowed ) );
	}

	/**
	 * Set if shortcodes is allowed.
	 *
	 * @param bool|string $allowed Allowed.
	 *
	 * @return void
	 */
	public function set_allow_shortcodes( $allowed ): void {
		$this->set_prop( 'allow_shortcodes', Formatting::string_to_bool( $allowed ) );
	}

	/**
	 * Set add  sizes is allowed.
	 *
	 * @param bool|string $allowed Allowed.
	 *
	 * @return void
	 */
	public function set_reserve_space( $allowed ): void {
		$this->set_prop( 'reserve_space', Formatting::string_to_bool( $allowed ) );
	}

	/**
	 * Set if has weekdays.
	 *
	 * @param bool|string $has_weekdays Has weekdays.
	 *
	 * @return void
	 */
	public function set_has_weekdays( $has_weekdays ): void {
		$this->set_prop( 'has_weekdays', Formatting::string_to_bool( $has_weekdays ) );
	}

	/* Conditional ------------------- */

	/**
	 * Check if debug mode is enabled within the ad.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return bool
	 */
	public function is_debug_mode( $context = 'view' ): bool {
		return $this->get_prop( 'debugmode', $context );
	}

	/**
	 * Check if PHP code execution is allowed within the ad.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return bool
	 */
	public function is_php_allowed( $context = 'view' ): bool {
		return $this->get_prop( 'allow_php', $context );
	}

	/**
	 * Check if shortcode execution is allowed within the ad.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return bool
	 */
	public function is_shortcode_allowed( $context = 'view' ): bool {
		return $this->get_prop( 'allow_shortcodes', $context );
	}

	/**
	 * Check if has weekdays.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return bool
	 */
	public function has_weekdays( $context = 'view' ): bool {
		return $this->get_prop( 'has_weekdays', $context );
	}

	/**
	 * Check if space is reserved.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return bool
	 */
	public function is_space_reserved( $context = 'view' ): bool {
		return $this->get_prop( 'reserve_space', $context );
	}

	/**
	 * Check whether this ad is expired.
	 *
	 * @return bool
	 */
	public function is_expired(): bool {
		if ( $this->get_expiry_date() <= 0 || $this->get_expiry_date() > time() ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks if the ad is placed in the header.
	 *
	 * @return bool
	 */
	public function is_head_placement(): bool {
		if ( null !== $this->get_parent() && $this->get_parent()->is_type( 'header' ) ) {
			return true;
		}

		// group as head placement.
		if ( 'header' === $this->get_prop( 'group_placement_context' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if the ad is a top-level ad.
	 *
	 * @deprecated 2.0.0 use is_parent_placement() instead.
	 *
	 * @return bool
	 */
	public function is_top_level(): bool {
		_deprecated_function( __FUNCTION__, '2.0.0', 'is_parent_placement' );

		return $this->is_parent_placement();
	}

	/**
	 * Determines whether the ad can be displayed.
	 *
	 * @param array $check_options check options.
	 *
	 * @return bool True if the ad can be displayed, false otherwise.
	 */
	public function can_display( $check_options = [] ): bool {
		$check_options = wp_parse_args(
			$check_options,
			[
				'passive_cache_busting' => false,
				'ignore_debugmode'      => false,
			]
		);

		// Prevent ad to show up through wp_head, if this is not a header placement.
		if ( doing_action( 'wp_head' ) && ! $this->is_head_placement() && ! Compatibility::can_inject_during_wp_head() ) {
			return false;
		}

		// Check If the current ad is requested using a shortcode placed in the content of the current ad.
		$shortcode_ad_id = $this->get_prop( 'shortcode_ad_id' );
		if ( $shortcode_ad_id && absint( $shortcode_ad_id ) === $this->get_id() ) {
			return false;
		}

		/**
		 * Allows bypassing server side visitor conditions checks or ignoring debug mode
		 *
		 * @param array $check_options can display check options.
		 */
		$check_options = apply_filters( 'advanced-ads-can-display-ad-check-options', $check_options );

		// Force ad display if debug mode is enabled.
		if ( $this->is_debug_mode() && ! $check_options['ignore_debugmode'] ) {
			return true;
		}

		if ( ! $check_options['passive_cache_busting'] ) {
			// Don’t display ads that are not published or private for users not logged in.
			if ( ! $this->is_status( 'publish' ) && ! ( $this->is_status( 'private' ) && is_user_logged_in() ) ) {
				return false;
			}

			if ( ! $this->can_display_by_visitor() ) {
				return false;
			}
		} elseif ( ! $this->is_status( 'publish' ) ) {
			return false;
		}

		if ( $this->is_expired() ) {
			return false;
		}

		$can_display = apply_filters( 'advanced-ads-can-display-ad', true, $this, $check_options );

		// Add own conditions to flag output as possible or not.
		return apply_filters_deprecated( 'advanced-ads-can-display', [ $can_display, $this, $check_options ], '2.0.0', 'advanced-ads-can-display-ad', 'Use advanced-ads-can-display-ad instead.' );
	}

	/* Additional Methods ------------------- */

	/**
	 * Prepare frontend output.
	 *
	 * @return string
	 */
	public function generate_html(): string {
		$output = $this->prepare_frontend_output();

		// Filter to manipulate the output before the wrapper is added.
		if ( $output && ! $this->is_head_placement() ) {
			$output = apply_filters( 'advanced-ads-output-inside-wrapper', $output, $this );
		}

		return $output;
	}

	/**
	 * Get the wrapper attributes.
	 *
	 * @return array
	 */
	public function get_wrapper_attributes(): array {
		// Early bail!!
		if ( null !== $this->wrapper ) {
			return $this->wrapper;
		}

		$attrs       = [];
		$attrs['id'] = $this->create_wrapper_id();
		if ( Str::is_non_empty( $this->get_wrapper_class() ) ) {
			$classes = explode( ' ', $this->get_wrapper_class() );

			foreach ( $classes as $_class ) {
				$attrs['class'][] = sanitize_html_class( $_class );
			}
		}

		if ( ! $this->is_head_placement() ) {
			$position     = $this->get_position();
			$use_position = false;

			if ( $this->is_parent_placement() ) {
				$classes = $this->get_prop( 'ad_args.output.class' );
				if ( $classes && is_array( $classes ) ) {
					$attrs['class'] = array_merge( (array) ( $attrs['class'] ?? [] ), $classes );
				}

				if ( $this->is_parent_placement() && 'default' !== $this->get_parent()->get_prop( 'placement_position' ) ) {
					$use_position = true;
					$position     = $this->get_parent()->get_prop( 'placement_position' );
				}
			}

			$this->get_wrapper_styles( $attrs, $position, $use_position );
		}

		// Edit Bar tooltip.
		if ( ! defined( 'ADVANCED_ADS_DISABLE_EDIT_BAR' ) && Conditional::user_can( 'advanced_ads_edit_ads' ) && $this->is_parent_placement() ) {
			$attrs['data-title'][] = $this->get_tooltip_title();
		}

		// Health Tool highlight.
		if ( Conditional::user_can( 'advanced_ads_edit_ads' ) && ! $this->is_parent_group() ) {
			$attrs['class'][] = wp_advads()->get_frontend_prefix() . 'highlight-wrapper';
		}

		$attrs = apply_filters_deprecated(
			'advanced-ads-output-wrapper-attributes',
			[ $attrs, $this ],
			'2.0.0',
			'advanced-ads-wrapper-attributes-ad'
		);

		$this->wrapper = apply_filters( 'advanced-ads-wrapper-attributes-ad', $attrs, $this );

		return $this->wrapper;
	}

	/**
	 * Prepares the output for the group.
	 *
	 * @return string The prepared output.
	 */
	public function prepare_output(): string {
		$output = $this->generate_html();

		// Don’t deliver anything, if main ad content is empty.
		if ( empty( $output ) ) {
			return '';
		}

		$ad_args        = $this->get_prop( 'ad_args' );
		$output_options = $this->get_prop( 'output_options' ) ?? [];
		$global_output  = $output_options['global_output'] ?? ! isset( $ad_args['global_output'] ) || $ad_args['global_output'];

		$this->set_prop_temp( 'global_output', $global_output );

		$output_options['global_output'] = $global_output;

		if ( ! $this->is_head_placement() ) {
			$output = $this->add_wrapper( $output, $global_output );

			// Add a clearfix, if set.
			if (
				( $this->is_parent_placement() && ! empty( $this->get_parent()->get_prop( 'placement_clearfix' ) ) )
				|| $this->get_clearfix()
			) {
				$output .= '<br style="clear: both; display: block; float: none;"/>';
			}
		}

		// Add the ad to the global output array.
		if ( $output_options['global_output'] ) {
			Stats::get()->add_entity( 'ad', $this->get_id(), $this->get_title() );
		}

		do_action_deprecated(
			'advanced-ads-output',
			[ $this, $output, $output_options ],
			'2.0.0',
			'advanced-ads-ad-output-ready',
			'Use advanced-ads-ad-output-ready instead.'
		);

		return apply_filters_deprecated(
			'advanced-ads-output-final',
			[ $output, $this, $output_options ],
			'2.0.0',
			'advanced-ads-ad-output',
			'Use advanced-ads-ad-output instead.'
		);
	}

	/**
	 * Get groups
	 *
	 * @return bool|array
	 */
	public function get_groups() {
		if ( null === $this->groups ) {
			$this->groups = [];
			$terms        = wp_get_object_terms( $this->get_id(), Constants::TAXONOMY_GROUP );
			if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
				foreach ( $terms as $term ) {
					$this->groups[ $term->term_id ] = wp_advads_get_group( $term );
				}
			}
		}

		return $this->groups;
	}

	/**
	 * Get ad type object
	 *
	 * @return Ad_Type|bool
	 */
	public function get_type_object() {
		if ( ! wp_advads_has_ad_type( $this->get_type() ) ) {
			wp_advads_create_ad_type( $this->get_type() );
		}

		return wp_advads_get_ad_type( $this->get_type() );
	}

	/**
	 * Get ad schedule details
	 *
	 * @return array
	 */
	public function get_ad_schedule_details(): array {
		$status_strings     = [];
		$html_classes       = 'advads-filter-timing';
		$post_start         = get_post_time( 'U', true, $this->get_id() );
		$expiry_date_format = get_option( 'date_format' ) . ', ' . get_option( 'time_format' );

		$status_type = get_post_status( $this->get_id() );
		if ( empty( $status_type ) || 'publish' === $status_type ) {
			$status_type = 'published';
		}

		if ( $post_start > time() ) {
			$status_type   = 'future';
			$html_classes .= ' advads-filter-future';

			/* translators: %s is a date. */
			$status_strings[] = sprintf( __( 'starts %s', 'advanced-ads' ), get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $post_start ), $expiry_date_format ) );
		}

		$expiry = $this->get_expiry_date();
		if ( ! empty( $expiry ) ) {
			$html_classes .= ' advads-filter-any-exp-date';
			$expiry_date   = ( new \DateTimeImmutable( '@' . $expiry ) )->setTimezone( WordPress::get_timezone() );
			$tz            = ' (' . WordPress::get_timezone_name() . ')';

			if ( $expiry > time() ) {
				$status_type = 'expiring';
				/* translators: %s is a date. */
				$status_strings[] = sprintf( __( 'expires %s', 'advanced-ads' ), $expiry_date->format( $expiry_date_format ) ) . $tz;
			} else {
				$status_type   = 'expired';
				$html_classes .= ' advads-filter-expired';

				/* translators: %s is a date. */
				$status_strings[] = sprintf( __( 'expired %s', 'advanced-ads' ), $expiry_date->format( $expiry_date_format ) ) . $tz;
			}
		}

		$labels = [
			'published' => __( 'Published', 'advanced-ads' ),
			'draft'     => __( 'Draft', 'advanced-ads' ),
			'trash'     => __( 'Trashed', 'advanced-ads' ),
		];

		if ( isset( $labels[ $status_type ] ) ) {
			$status_strings[] = $labels[ $status_type ];
		}

		return compact( 'post_start', 'status_type', 'status_strings', 'html_classes' );
	}

	/**
	 * Load a template with the information on ad expiry
	 *
	 * @return string
	 */
	public function get_ad_schedule_html(): string {
		[
			'post_start'     => $post_start,
			'status_type'    => $status_type,
			'status_strings' => $status_strings
		] = $this->get_ad_schedule_details();

		if ( empty( $status_strings ) ) {
			return '';
		}

		ob_start();
		include ADVADS_ABSPATH . 'views/admin/tables/ads/icon-status.php';

		return ob_get_clean();
	}

	/**
	 * Get the post edit link
	 *
	 * @return string|null
	 */
	public function get_edit_link() {
		return get_edit_post_link( $this->get_id(), 'other' );
	}

	/**
	 * Normalize expiry date
	 *
	 * @param array $expiry_date Expiry date.
	 *
	 * @return int
	 */
	private function normalize_expiry_date( $expiry_date ): int {
		// Early bail!!
		if ( empty( $expiry_date['enabled'] ) ) {
			return 0;
		}

		$local_string = sprintf(
			'%04d-%02d-%02d %02d:%02d:00',
			absint( $expiry_date['year'] ),
			absint( $expiry_date['month'] ),
			absint( $expiry_date['day'] ),
			absint( $expiry_date['hour'] ),
			absint( $expiry_date['minute'] )
		);

		$local_dt = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $local_string, WordPress::get_timezone() );

		return $local_dt ? $local_dt->getTimestamp() : 0;
	}

	/**
	 * Process shortcodes.
	 *
	 * @param string $output Ad content.
	 *
	 * @return string
	 */
	protected function do_shortcode( $output ): string {
		$ad_args                    = $this->temp_data;
		$ad_args['shortcode_ad_id'] = $this->get_id();
		$output                     = preg_replace(
			'/\[(the_ad_group|the_ad_placement|the_ad) /',
			'[$1 ad_args="' . rawurlencode( wp_json_encode( $ad_args ) ) . '" ',
			$output
		);

		return do_shortcode( $output );
	}

	/**
	 * Add a wrapper arount the ad content if wrapper information are given
	 *
	 * @since 1.1.4
	 *
	 * @param string $ad_content content of the ad.
	 * @param bool   $global_output Global output.
	 *
	 * @return string $wrapper ad within the wrapper
	 */
	private function add_wrapper( $ad_content = '', $global_output = false ): string {
		$ad_args = $this->get_prop( 'ad_args' );
		$label   = $this->get_label();
		$attrs   = $this->get_wrapper_attributes();

		$attrs = apply_filters_deprecated(
			'advanced-ads-output-wrapper-options',
			[ $attrs, $this ],
			'2.0.0',
			'advanced-ads-wrapper-attributes-ad'
		);

		$attrs = apply_filters( 'advanced-ads-wrapper-attributes-ad', $attrs, $this );
		// Create another wrapper so that the label does not reduce the height of the ad wrapper.
		if ( $label && ! empty( $attrs['style']['height'] ) ) {
			$height_attrs = [ 'style' => [ 'height' => $attrs['style']['height'] ] ];
			unset( $attrs['style']['height'] );
			$ad_content = '<div' . Advanced_Ads_Utils::build_html_attributes( $height_attrs ) . '>' . $ad_content . '</div>';
		}

		// Adds inline css to the wrapper.
		if ( ! empty( $ad_args['inline-css'] ) && $this->is_parent_placement() ) {
			$attrs = ( new Advanced_Ads_Inline_Css() )->add_css( $attrs, $ad_args['inline-css'], $global_output );
		}

		// Edit Bar logic.
		$edit_bar = '';
		if (
			! defined( 'ADVANCED_ADS_DISABLE_EDIT_BAR' ) &&
			Conditional::user_can( 'advanced_ads_edit_ads' ) &&
			$this->is_parent_placement()
		) {
			ob_start();
			include ADVADS_ABSPATH . 'public/views/ad-edit-bar.php';
			$edit_bar = trim( ob_get_clean() );
		}

		$wrapper_element = ! empty( $this->get_prop( 'inline_wrapper_element' ) ) ? 'span' : 'div';

		// Build the box.
		$wrapper  = '<' . $wrapper_element . Advanced_Ads_Utils::build_html_attributes( $attrs ) . '>';
		$wrapper .= $label;
		$wrapper .= $edit_bar;
		$wrapper .= apply_filters( 'advanced-ads-output-wrapper-before-content', '', $this );
		$wrapper .= $ad_content;
		$wrapper .= apply_filters( 'advanced-ads-output-wrapper-after-content', '', $this );
		$wrapper .= '</' . $wrapper_element . '>';

		return $wrapper;
	}

	/**
	 * Creates a wrapper array for the group.
	 *
	 * @deprecated 2.0.0 use get_wrapper_attributes() instead.
	 *
	 * @return array
	 */
	public function create_wrapper(): array {
		_deprecated_function( __FUNCTION__, '2.0.0', 'get_wrapper_attributes' );

		return $this->get_wrapper_attributes();
	}

	/**
	 * Retrieves the label for the ad.
	 *
	 * @return string The label for the ad.
	 */
	private function get_label(): string {
		if ( null === $this->label ) {
			$ad_args = $this->get_prop( 'ad_args' );
			$state   = $ad_args['ad_label'] ?? 'default';
			$label   = Advanced_Ads::get_instance()->get_label( $this, $state );

			$this->label = $this->is_parent_placement() && $label ? $label : '';
		}

		return $this->label;
	}

	/**
	 * Check visitor conditions
	 *
	 * @return bool $can_display true if can be displayed in frontend based on visitor settings
	 * @since 1.1.0
	 */
	private function can_display_by_visitor() {
		$wp_the_query = $this->get_prop( 'wp_the_query' );
		if ( $wp_the_query && ! empty( $wp_the_query['is_feed'] ) ) {
			return true;
		}

		$conditions = $this->get_visitor_conditions();
		if ( empty( $conditions ) ) {
			return true;
		}

		$last_result = false;
		$length      = count( $conditions );

		for ( $i = 0; $i < $length; ++$i ) {
			$_condition = current( $conditions );
			// Ignore OR if last result was true.
			if ( $last_result && isset( $_condition['connector'] ) && 'or' === $_condition['connector'] ) {
				next( $conditions );
				continue;
			}

			$result      = Advanced_Ads_Visitor_Conditions::frontend_check( $_condition, $this );
			$last_result = $result;
			if ( ! $result ) {
				// return false only, if the next condition doesn’t have an OR operator.
				$next = next( $conditions );
				if ( ! isset( $next['connector'] ) || 'or' !== $next['connector'] ) {
					return false;
				}
			} else {
				next( $conditions );
			}
		}

		// Check mobile condition.
		if ( isset( $conditions['mobile'] ) ) {
			switch ( $conditions['mobile'] ) {
				case 'only':
					if ( ! wp_is_mobile() ) {
						return false;
					}
					break;
				case 'no':
					if ( wp_is_mobile() ) {
						return false;
					}
					break;
			}
		}

		return true;
	}

	/**
	 * Create a random wrapper id
	 *
	 * @since 1.1.4
	 *
	 * @return string $id random id string
	 */
	public function create_wrapper_id(): string {
		$wrapper_id = sanitize_key( $this->get_wrapper_id() );
		if ( '' !== $wrapper_id ) {
			return $wrapper_id;
		}

		return wp_advads()->get_frontend_prefix() . wp_rand();
	}

	/**
	 * Generate the tooltip title for a placement with associated ads.
	 *
	 * @return string Tooltip title containing placement and ads name.
	 */
	private function get_tooltip_title(): string {
		$ads = $this->is_parent_group()
			? wp_list_pluck( $this->get_parent()->get_ads(), 'post_title' )
			: [ $this->get_title() ];

		// Construct and format the tooltip title using the placement ID and ad titles.
		return sprintf(
			/* translators: %1$s is a placement name, %2$s is the ads name. */
			__( 'Placement name: %1$s; Ads: %2$s', 'advanced-ads' ),
			esc_attr( $this->get_parent()->get_title() ),
			esc_attr( $ads ? implode( ',', $ads ) : '' )
		);
	}
}
