<?php
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.NamingConventions.PrefixAllGlobals -- Shared legacy helper names and plugin-table fallbacks are public compatibility APIs.

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Helper functions for the Instant Popup Builder plugin
 *
 * @package    Instant_Popup_Builder
 * @subpackage Instant_Popup_Builder/includes
 * @since      1.1.4
 */

/**
 * Ensures a value is always a string, preventing null/type errors in PHP 8.1+
 *
 * This helper function is used to safely pass values to string functions
 * like strpos(), str_replace(), substr(), trim(), and explode() which
 * require string arguments in PHP 8.1+.
 *
 * @since 1.1.4
 * @param mixed $value The value to convert to a string.
 * @return string Returns the value as a string, or empty string if not a string.
 */
function ipb_string( $value ): string {
	return is_string( $value ) ? $value : '';
}

/**
 * Get the shared subscription agreement copy.
 *
 * @param array $settings Subscription settings.
 * @return array
 */
function ipb_get_subscription_agreement_copy( $settings ) {
	$settings = is_array( $settings ) ? $settings : array();
	$prefix   = ! empty( $settings['agreement_prefix_text'] ) ? $settings['agreement_prefix_text'] : 'I agree to the';

	$strip_prefix = static function ( $text ) use ( $prefix ) {
		$text = trim( (string) $text );
		foreach ( array( $prefix, 'I agree to the' ) as $candidate ) {
			$candidate = trim( (string) $candidate );
			if ( '' !== $candidate && 0 === stripos( $text, $candidate ) ) {
				$text = trim( substr( $text, strlen( $candidate ) ) );
				break;
			}
		}

		return $text;
	};

	$privacy_label = ! empty( $settings['privacy_policy_label'] ) ? $settings['privacy_policy_label'] : '';
	if ( '' === $privacy_label && ! empty( $settings['privacy_policy_text'] ) ) {
		$privacy_label = $strip_prefix( $settings['privacy_policy_text'] );
	}

	$terms_label = ! empty( $settings['terms_conditions_label'] ) ? $settings['terms_conditions_label'] : '';
	if ( '' === $terms_label && ! empty( $settings['terms_conditions_text'] ) ) {
		$terms_label = $strip_prefix( $settings['terms_conditions_text'] );
	}

	return array(
		'prefix'        => '' !== trim( (string) $prefix ) ? trim( (string) $prefix ) : 'I agree to the',
		'privacy_label' => '' !== trim( (string) $privacy_label ) ? trim( (string) $privacy_label ) : 'Privacy Policy',
		'terms_label'   => '' !== trim( (string) $terms_label ) ? trim( (string) $terms_label ) : 'Terms and conditions',
	);
}

/**
 * Render shared subscription Privacy Policy / Terms agreement links.
 *
 * @param array  $settings    Subscription settings.
 * @param string $privacy_url Privacy URL.
 * @param string $terms_url   Terms URL.
 * @return string
 */
function ipb_render_subscription_agreement_links( $settings, $privacy_url, $terms_url ) {
	$settings     = is_array( $settings ) ? $settings : array();
	$show_privacy = isset( $settings['show_privacy'] ) && '1' === (string) $settings['show_privacy'] && ! empty( $privacy_url );
	$show_terms   = isset( $settings['show_terms'] ) && '1' === (string) $settings['show_terms'] && ! empty( $terms_url );

	if ( ! $show_privacy && ! $show_terms ) {
		return '';
	}

	$copy  = ipb_get_subscription_agreement_copy( $settings );
	$parts = array();

	if ( $show_privacy ) {
		$parts[] = '<a href="' . esc_url( $privacy_url ) . '" target="_blank" rel="noopener">' . esc_html( $copy['privacy_label'] ) . '</a>';
	}

	if ( $show_terms ) {
		$parts[] = '<a href="' . esc_url( $terms_url ) . '" target="_blank" rel="noopener">' . esc_html( $copy['terms_label'] ) . '</a>';
	}

	return esc_html( $copy['prefix'] ) . ' ' . implode( ' ' . esc_html__( 'and', 'instant-popup-builder' ) . ' ', $parts );
}

/**
 * Render shared subscription agreement links after resolving saved URL/page settings.
 *
 * @param array $settings Subscription settings.
 * @return string
 */
function ipb_render_subscription_agreement_from_settings( $settings ) {
	$settings    = is_array( $settings ) ? $settings : array();
	$privacy_url = '';
	$terms_url   = '';

	if ( isset( $settings['privacy_link_type'] ) && 'page' === $settings['privacy_link_type'] && ! empty( $settings['privacy_policy_page'] ) ) {
		$privacy_url = get_permalink( $settings['privacy_policy_page'] );
	} elseif ( ! empty( $settings['privacy_policy_url'] ) ) {
		$privacy_url = $settings['privacy_policy_url'];
	}

	if ( isset( $settings['terms_link_type'] ) && 'page' === $settings['terms_link_type'] && ! empty( $settings['terms_conditions_page'] ) ) {
		$terms_url = get_permalink( $settings['terms_conditions_page'] );
	} elseif ( ! empty( $settings['terms_conditions_url'] ) ) {
		$terms_url = $settings['terms_conditions_url'];
	}

	return ipb_render_subscription_agreement_links( $settings, $privacy_url, $terms_url );
}

/**
 * Sanitizes HTML content for HTML popups while preserving structure and formatting
 *
 * This function allows a more permissive set of HTML tags and attributes
 * compared to wp_kses_post(), giving users more control over HTML popup content
 * while still maintaining security.
 *
 * @since 1.1.4
 * @param string $html The HTML content to sanitize.
 * @return string The sanitized HTML content.
 */
function ipb_sanitize_html_popup_content( $html ) {
	if ( ! is_string( $html ) ) {
		return '';
	}

	// Define allowed HTML tags and attributes for HTML popups
	$allowed_html = array(
		'div' => array(
			'class' => true,
			'id' => true,
			'style' => true,
			'data-*' => true,
		),
		'table' => array(
			'class' => true,
			'id' => true,
			'style' => true,
			'width' => true,
			'border' => true,
			'cellpadding' => true,
			'cellspacing' => true,
		),
		'tr' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'td' => array(
			'class' => true,
			'id' => true,
			'style' => true,
			'colspan' => true,
			'rowspan' => true,
			'width' => true,
			'height' => true,
		),
		'th' => array(
			'class' => true,
			'id' => true,
			'style' => true,
			'colspan' => true,
			'rowspan' => true,
		),
		'img' => array(
			'src' => true,
			'alt' => true,
			'title' => true,
			'class' => true,
			'id' => true,
			'style' => true,
			'width' => true,
			'height' => true,
		),
		'a' => array(
			'href' => true,
			'target' => true,
			'rel' => true,
			'class' => true,
			'id' => true,
			'style' => true,
			'title' => true,
		),
		'p' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'h1' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'h2' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'h3' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'h4' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'h5' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'h6' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'span' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'strong' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'em' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'ul' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'ol' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'li' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
		'br' => array(),
		'hr' => array(
			'class' => true,
			'id' => true,
			'style' => true,
		),
	);

	return wp_kses( $html, $allowed_html );
}

/**
 * Allowed HTML for text popup content (TinyMCE), shared by sanitization and late escaping at output.
 *
 * @since 1.1.5
 * @return array<string, array<string, bool>> Allowed HTML for wp_kses().
 */
function ipb_get_allowed_html_text_popup_content() {
	$allowed_html = wp_kses_allowed_html( 'post' );

	$tags_with_style = array( 'p', 'div', 'span', 'strong', 'em', 'u', 's', 'strike', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code' );
	foreach ( $tags_with_style as $tag ) {
		if ( isset( $allowed_html[ $tag ] ) ) {
			$allowed_html[ $tag ]['style'] = true;
		} else {
			$allowed_html[ $tag ] = array( 'style' => true, 'class' => true, 'id' => true );
		}
	}

	if ( ! isset( $allowed_html['font'] ) ) {
		$allowed_html['font'] = array( 'color' => true, 'face' => true, 'size' => true, 'style' => true );
	}

	return $allowed_html;
}

/**
 * Sanitizes text popup content from TinyMCE while preserving all formatting and styles
 *
 * This function allows all standard WordPress post content HTML tags and attributes,
 * including inline styles from TinyMCE, ensuring formatting is preserved exactly as entered.
 *
 * @since 1.1.5
 * @param string $content The TinyMCE content to sanitize.
 * @return string The sanitized content with all formatting preserved.
 */
function ipb_sanitize_text_popup_content( $content ) {
	if ( ! is_string( $content ) ) {
		return '';
	}

	return wp_kses( $content, ipb_get_allowed_html_text_popup_content() );
}

/**
 * Parse the `closing` JSON column into normalized values for the front end.
 *
 * Uses the database row as the source of truth so behavior does not depend on
 * transients being hydrated in the same request (fixes multi-popup / ordering issues).
 *
 * @since 1.1.10
 * @param mixed $closing_json Raw `closing` column (JSON string or empty).
 * @return array{position:string,escape:string,outside:string,hide:string} position: top-left|top-right|bottom-left|bottom-right; others: yes|no.
 */
function ipb_parse_closing_settings_from_json( $closing_json ) {
	$defaults = array(
		'position' => 'top-right',
		'escape'   => 'no',
		'outside'  => 'no',
		'hide'     => 'no',
	);
	if ( ! is_string( $closing_json ) || '' === trim( $closing_json ) ) {
		return $defaults;
	}
	$data = json_decode( $closing_json, true );
	if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
		$obj = json_decode( $closing_json );
		if ( JSON_ERROR_NONE !== json_last_error() || ! is_object( $obj ) ) {
			return $defaults;
		}
		$data = json_decode( wp_json_encode( $obj ), true );
		if ( ! is_array( $data ) ) {
			return $defaults;
		}
	}
	$opts = array();
	if ( isset( $data['close_option'] ) ) {
		$co = $data['close_option'];
		if ( is_array( $co ) ) {
			$opts = $co;
		} elseif ( is_object( $co ) ) {
			$opts = array_values( (array) $co );
		} elseif ( is_string( $co ) ) {
			// Legacy / edge: single token or comma-separated list stored as a string.
			$co = trim( $co );
			if ( '' !== $co ) {
				if ( false !== strpos( $co, ',' ) ) {
					$opts = array_map( 'trim', explode( ',', $co ) );
				} else {
					$opts = array( $co );
				}
			}
		}
	}
	$allowed_tokens = array( 'escape', 'box-outside', 'hide-close' );
	$opts_flat        = array();
	foreach ( $opts as $v ) {
		$t = is_string( $v ) ? trim( $v ) : trim( (string) $v );
		if ( '' !== $t && in_array( $t, $allowed_tokens, true ) ) {
			$opts_flat[] = $t;
		}
	}
	$opts_flat = array_values( array_unique( $opts_flat, SORT_STRING ) );
	$pos       = isset( $data['close_position'] ) && is_string( $data['close_position'] ) ? trim( $data['close_position'] ) : '';
	$allowed_pos = array( 'top-left', 'top-right', 'bottom-left', 'bottom-right' );
	$defaults['position'] = in_array( $pos, $allowed_pos, true ) ? $pos : 'top-right';
	$defaults['escape']   = in_array( 'escape', $opts_flat, true ) ? 'yes' : 'no';
	$defaults['outside']  = in_array( 'box-outside', $opts_flat, true ) ? 'yes' : 'no';
	$defaults['hide']     = in_array( 'hide-close', $opts_flat, true ) ? 'yes' : 'no';
	return $defaults;
}

/**
 * Normalize closing[] POST values to the allowlisted tokens only.
 *
 * @since 1.1.10
 * @param mixed $raw Raw checkbox array from POST (array of tokens, or missing).
 * @return string[]
 */
function ipb_sanitize_closing_post_options( $raw ) {
	$allowed = array( 'escape', 'box-outside', 'hide-close' );
	if ( ! isset( $raw ) || null === $raw ) {
		return array();
	}
	if ( is_object( $raw ) ) {
		$raw = array_values( (array) $raw );
	}
	if ( is_string( $raw ) ) {
		$raw = array( $raw );
	}
	if ( ! is_array( $raw ) ) {
		return array();
	}
	// Flatten nested arrays (some stacks / proxies send checkbox values nested).
	$ipb_flatten_closing = null;
	$ipb_flatten_closing  = function ( $arr ) use ( &$ipb_flatten_closing ) {
		$acc = array();
		foreach ( $arr as $v ) {
			if ( is_array( $v ) ) {
				$acc = array_merge( $acc, $ipb_flatten_closing( $v ) );
			} else {
				$acc[] = $v;
			}
		}
		return $acc;
	};
	$raw = $ipb_flatten_closing( $raw );
	$out = array();
	foreach ( $raw as $v ) {
		$t = sanitize_text_field( wp_unslash( is_string( $v ) ? $v : (string) $v ) );
		if ( '' !== $t && in_array( $t, $allowed, true ) ) {
			$out[] = $t;
		}
	}
	return array_values( array_unique( $out, SORT_STRING ) );
}

/**
 * Read closing options from POST: dedicated fields when `ipb_close_opts_version=1`, else `ipb_closing[]` / `closing[]`.
 *
 * @since 1.1.10
 * @return string[] Allowlisted close_option tokens.
 */
function ipb_sanitize_closing_from_request() {
	$raw = null;
	$out = array();
	// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Caller verifies nonce in edit/save flows.
	$v1 = isset( $_POST['ipb_close_opts_version'] ) && '1' === sanitize_text_field( wp_unslash( (string) $_POST['ipb_close_opts_version'] ) );
	if ( $v1 ) {
		$is_yes = static function ( $key ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Missing
			if ( ! isset( $_POST[ $key ] ) ) {
				return false;
			}
			// phpcs:ignore WordPress.Security.NonceVerification.Missing
			$v = sanitize_text_field( wp_unslash( (string) $_POST[ $key ] ) );
			return in_array( strtolower( trim( $v ) ), array( 'yes', '1', 'on', 'true' ), true );
		};
		if ( $is_yes( 'ipb_close_escape' ) ) {
			$out[] = 'escape';
		}
		if ( $is_yes( 'ipb_close_outside' ) ) {
			$out[] = 'box-outside';
		}
		if ( $is_yes( 'ipb_close_hide' ) ) {
			$out[] = 'hide-close';
		}
		$out = array_values( array_unique( $out, SORT_STRING ) );
	} else {
		// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Called only from nonce-protected handlers; values are normalized immediately below.
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- This parser is called only from nonce-protected save handlers.
		if ( isset( $_POST['ipb_closing'] ) ) {
			$raw = wp_unslash( $_POST['ipb_closing'] );
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- This parser is called only from nonce-protected save handlers.
		} elseif ( isset( $_POST['closing'] ) ) {
			$raw = wp_unslash( $_POST['closing'] );
		}
		// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$out = ipb_sanitize_closing_post_options( $raw );
	}
	return $out;
}

/**
 * Close option + close position for popup save/update from admin POST.
 *
 * Uses `ipb_close_opts_version` + ipb_close_* fields (see ipb_sanitize_closing_from_request()).
 * If no closing-related keys exist in POST and $fallback_popup_id > 0, restores from DB
 * so partial/stripped requests do not wipe closing settings on extension edit flows.
 *
 * @since 1.1.11
 * @param int $fallback_popup_id Popup row ID for DB fallback; use 0 for create-only handlers.
 * @return array{close_option:string[],close_position:string}
 */
function ipb_get_closing_save_from_post( $fallback_popup_id = 0 ) {
	global $wpdb;

	// phpcs:disable WordPress.Security.NonceVerification.Missing -- All callers are nonce-protected save handlers.
	$close_option = function_exists( 'ipb_sanitize_closing_from_request' )
		? ipb_sanitize_closing_from_request()
		: (
			isset( $_POST['closing'] ) && ! empty( $_POST['closing'] )
				? array_map( 'sanitize_text_field', wp_unslash( $_POST['closing'] ) )
				: array()
		);
	$close_position = isset( $_POST['close_position'] ) && ! empty( $_POST['close_position'] )
		? sanitize_text_field( wp_unslash( $_POST['close_position'] ) )
		: '';

	// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Callers verify nonces before save.
	$has_closing_in_post = isset( $_POST['ipb_close_opts_version'] )
		|| isset( $_POST['ipb_closing'] )
		|| isset( $_POST['closing'] );
	// phpcs:enable WordPress.Security.NonceVerification.Missing

	$fid = absint( $fallback_popup_id );
	if ( ! $has_closing_in_post && $fid > 0 && isset( $wpdb->prefix ) ) {
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT closing FROM {$wpdb->prefix}instant_popup_builder WHERE id = %d",
				$fid
			)
		);
		if ( $row && ! empty( $row->closing ) ) {
			$data = json_decode( $row->closing, true );
			if ( is_array( $data ) ) {
				$close_option = isset( $data['close_option'] ) && is_array( $data['close_option'] )
					? $data['close_option']
					: array();
				if ( function_exists( 'ipb_sanitize_closing_post_options' ) ) {
					$close_option = ipb_sanitize_closing_post_options( $close_option );
				}
				$close_position = isset( $data['close_position'] ) && is_string( $data['close_position'] )
					? $data['close_position']
					: '';
			}
		}
	}

	return array(
		'close_option'   => $close_option,
		'close_position' => $close_position,
	);
}
