<?php
/**
 * Renders and manages the plugin Settings page.
 *
 * @link       https://personalizewp.com
 * @since      2.0.0
 *
 * @package    PersonalizeWP
 */

namespace PersonalizeWP;

use PersonalizeWP\Traits\SingletonTrait;
use PersonalizeWP\Admin\Utils;

defined( 'ABSPATH' ) || exit; // Exit if accessed directly.

/**
 * Class - Settings
 */
class Settings {

	use SingletonTrait;

	/**
	 * Holds instance of plugin object
	 *
	 * @var PersonalizeWP
	 */
	public $plugin;

	/**
	 * Settings key/identifier
	 *
	 * @var string
	 */
	public $option_key = 'personalizewp';

	/**
	 * Plugin settings
	 *
	 * @var array
	 */
	public $options = null;

	/**
	 * Settings fields
	 *
	 * @var array
	 */
	public $fields = array();

	/**
	 * Class constructor.
	 */
	public function __construct() {
		$this->plugin = \personalizewp();

		// Register settings, and fields.
		add_action( 'admin_init', array( $this, 'register_settings' ) );
		add_action( 'personalizewp_profile_extended_fields', [ $this, 'add_profile_custom_fields' ] );
	}

	/**
	 * Extends the base list of supported Profile Fields with the registered custom fields.
	 *
	 * @since 3.2.0
	 *
	 * @param string[] $fields   An array of meta fields to labels.
	 *
	 * @return string[] $fields
	 */
	public function add_profile_custom_fields( $fields ) {

		$custom_fields = $this->get_profile_custom_fields();
		if ( ! empty( $custom_fields ) ) {
			$fields = array_merge( $fields, $custom_fields );
		}

		return $fields;
	}

	/**
	 * Retrieves all custom fields available for Visitor Profiles.
	 *
	 * @since 3.2.0
	 *
	 * @return array
	 */
	public function get_profile_custom_fields() {

		$custom_fields = array();
		$_fields       = $this->plugin->get_setting( 'profile_custom_fields' );
		if ( ! empty( $_fields ) && is_array( $_fields ) ) {
			foreach ( $_fields as $field ) {
				$custom_fields[ sanitize_key( 'custom_' . $field ) ] = $field;
			}
		}

		return $custom_fields;
	}

	/**
	 * Return settings fields
	 *
	 * @return array
	 */
	public function get_fields() {

		$retention_choices = array(
			DAY_IN_SECONDS       => __( 'One Day', 'personalizewp' ),
			WEEK_IN_SECONDS      => __( 'One Week', 'personalizewp' ),
			MONTH_IN_SECONDS     => __( 'One Month', 'personalizewp' ),
			3 * MONTH_IN_SECONDS => __( 'Three Months', 'personalizewp' ),
			6 * MONTH_IN_SECONDS => __( 'Six Months', 'personalizewp' ),
			YEAR_IN_SECONDS      => __( 'One Year', 'personalizewp' ),
			0                    => __( 'Forever', 'personalizewp' ),
		);
		$fields            = array(
			'general'     => array(
				'title'  => esc_html__( 'General', 'personalizewp' ),
				'fields' => array(),
			),
			'performance' => array(
				'title'  => esc_html__( 'Performance', 'personalizewp' ),
				'fields' => array(
					array(
						'name'        => 'delay_initialisation',
						'title'       => esc_html__( 'JavaScript Initialization', 'personalizewp' ),
						'type'        => 'switch',
						'description' => esc_html__( 'When enabled, this setting will delay initialization of PersonalizeWP until a user has interacted with your website (e.g. moving the mouse over the page, touching the screen, scrolling, pressing a key, scrolling with the mouse wheel). This may improve your Core Web Vitals by decreasing your Cumulative Layout Shift (CLS) score.', 'personalizewp' ),
						'after_field' => esc_html__( 'Enable initialization delay', 'personalizewp' ),
						'default'     => 0,
					),
					array(
						'name'        => 'legacy_blocks',
						'title'       => esc_html__( 'Legacy Blocks', 'personalizewp' ),
						'type'        => 'switch',
						'description' => esc_html__( 'When enabled, this setting will support legacy WP-DXP blocks on the public facing side of your site. This should be kept off unless personalized blocks fail to appear, or support has requested it.', 'personalizewp' ),
						'after_field' => esc_html__( 'Enable legacy DXP support', 'personalizewp' ),
						'default'     => 0,
					),
				),
			),
			'privacy'     => array(
				'title'  => esc_html__( 'Privacy', 'personalizewp' ),
				'fields' => array(
					array(
						'name'        => 'anonymise_profiles',
						'title'       => esc_html__( 'Data collection', 'personalizewp' ),
						'type'        => 'switch',
						'description' => esc_html__( 'When enabled, this setting ensures that while user activity is tracked for segmentation and lead scoring purposes, personal identity remains concealed.', 'personalizewp' ),
						'after_field' => esc_html__( 'Always anonymise profiles', 'personalizewp' ),
						'default'     => 0,
					),
					array(
						'name'              => 'ttl_anonymous_profiles',
						'title'             => esc_html__( 'Anonymous Profile retention period', 'personalizewp' ),
						'type'              => 'select',
						'class'             => 'small-text',
						'description'       => esc_html__( 'Anonymous Profiles with last seen entries older than the selected period will be permanently deleted from the database, along with any of their activity entries.', 'personalizewp' ),
						'choices'           => $retention_choices,
						'default'           => WEEK_IN_SECONDS,
						'required'          => true,
						'sanitize_callback' => 'absint',
					),
					array(
						'name'              => 'ttl_known_profiles',
						'title'             => esc_html__( 'Known Profile retention period', 'personalizewp' ),
						'type'              => 'select',
						'class'             => 'small-text',
						'description'       => esc_html__( 'Known Profiles with last seen entries older than the selected period will be permanently deleted from the database.', 'personalizewp' ),
						'choices'           => $retention_choices,
						'default'           => 0,
						'required'          => true,
						'sanitize_callback' => 'absint',
					),
					array(
						'name'              => 'ttl_activities', // Changed in 2.8.0 to ensure opt-in after profile retention option additions.
						'title'             => esc_html__( 'Activity retention period', 'personalizewp' ),
						'type'              => 'select',
						'class'             => 'small-text',
						'description'       => esc_html__( 'All activity entries older than the selected period will be permanently deleted from the database.', 'personalizewp' ),
						'choices'           => $retention_choices,
						'default'           => 0,
						'required'          => true,
						'sanitize_callback' => 'absint',
					),
				),
			),
			'profile'     => array(
				'title'  => esc_html__( 'Visitor Profiles', 'personalizewp' ),
				'fields' => array(
					array(
						'name'        => 'track_anonymous_users',
						'title'       => esc_html__( 'Anonymous Visitor Tracking', 'personalizewp' ),
						'type'        => 'switch',
						'description' => esc_html__( 'When enabled, this setting allows anonymous user activity to be recorded. Otherwise a user will have to be identified first (such as by logging in) before their activity is recorded.', 'personalizewp' ),
						'after_field' => esc_html__( 'Enable Anonymous Visitor Tracking', 'personalizewp' ),
						'default'     => 0,
					),
					array(
						'name'         => 'custom_fields',
						'title'        => esc_html__( 'Custom Fields', 'personalizewp' ),
						'type'         => 'field_list',
						'placeholder'  => esc_html__( 'Add label', 'personalizewp' ),
						'submit_label' => esc_html__( 'Add New Field', 'personalizewp' ),
						'aria_label'   => esc_attr__( 'Remove Custom Field', 'personalizewp' ),
						'description'  => [
							esc_html__( 'Add custom fields to your Visitor Profiles, map form submissions to those fields, and show / hide blocks based on the recorded data. Click to remove if you no longer want to use them.', 'personalizewp' ),
							esc_html__( 'Once you have added your fields, click on the Save Settings button below to add them to the database.', 'personalizewp' ),
						],
						'default'      => array(),
						'callback'     => [ $this, 'output_field' ],
					),
				),
			),
			'security'    => array(
				'title'  => esc_html__( 'Security', 'personalizewp' ),
				'fields' => array(
					array(
						'name'          => 'url_tokens',
						'title'         => esc_html__( 'Authentication', 'personalizewp' ),
						'table_caption' => esc_html__( 'API Keys', 'personalizewp' ),
						'type'          => 'field_table',
						'placeholder'   => esc_html__( 'Add URL', 'personalizewp' ),
						'submit_label'  => esc_html__( 'Add New Key', 'personalizewp' ),
						'aria_label'    => esc_attr__( 'Revoke URL', 'personalizewp' ),
						'description'   => [
							esc_html(
								sprintf(
									/* translators: %s URL of current WP site */
									__( 'Create an API Key below to use the REST API. For your main site URL (%s) this is automatic, but if you wish to use a different URL on the frontend you\'ll need to generate a key.', 'personalizewp' ),
									home_url()
								)
							),
							wp_kses(
								sprintf(
									/* translators: 1: %s expands to a website link to https://help.personalizewp.com/, 2: </a> closing tag. */
									__( 'That API key will need to be passed through on all REST queries. Visit our %1$sdocumentation pages%2$s for more information.', 'personalizewp' ),
									'<a href="' . esc_url( 'https://help.personalizewp.com/' ) . '" target="_blank">',
									'</a>',
								),
								array(
									'a' => array(
										'href'   => array(),
										'target' => array(),
									),
								)
							),
						],
						'default'       => array(),
					),
				),
			),
		);

		/**
		 * Filter allows for modification of options fields
		 *
		 * @return array  Array of option fields
		 */
		$this->fields = apply_filters( 'personalizewp_settings_option_fields', $fields );
		ksort( $this->fields );

		return $this->fields;
	}

	/**
	 * Returns a list of all plugin options.
	 *
	 * @return array
	 */
	public function get_options() {

		// Already loaded so just return.
		if ( isset( $this->options ) ) {
			return $this->options;
		}

		$option_key = $this->option_key;

		$this->options = wp_parse_args(
			(array) get_option( $option_key, array() ),
			$this->get_defaults( $option_key )
		);
		return $this->options;
	}

	/**
	 * Return a singular plugin option.
	 *
	 * @since 3.0.1
	 *
	 * @param string $option Individual option to return
	 *
	 * @return mixed|null Null if the option doesn't exist, the value otherwise
	 */
	public function get_option( $option ) {

		// Load options if not already loaded.
		if ( ! isset( $this->options ) ) {
			$this->get_options();
		}

		return isset( $this->options[ $option ] ) ? $this->options[ $option ] : null;
	}

	/**
	 * Iterate through registered fields and extract default values
	 *
	 * @return array
	 */
	public function get_defaults() {
		$fields   = $this->get_fields();
		$defaults = array();

		foreach ( $fields as $section_name => $section ) {
			foreach ( $section['fields'] as $field ) {
				$defaults[ $section_name . '_' . $field['name'] ] = isset( $field['default'] ) ? $field['default'] : null;
			}
		}

		return (array) $defaults;
	}

	/**
	 * Registers settings fields and sections
	 *
	 * @return void
	 */
	public function register_settings() {
		$sections = $this->get_fields();

		register_setting(
			$this->option_key,
			$this->option_key,
			array(
				'show_in_rest'      => false,
				'type'              => 'array',
				'sanitize_callback' => array( $this, 'sanitize_settings' ),
				'default'           => $this->get_defaults(),
			)
		);

		foreach ( $sections as $section_name => $section ) {
			if ( empty( $section['fields'] ) ) {
				continue;
			}

			// The general section will not be a tab on its own.
			if ( 'general' === $section_name ) {
				$section_args = array(
					'before_section' => '<div class="pwp-panel">',
					'after_section'  => '</div>',
				);
			} else {
				$section_args = array(
					// Wrap the section to work with the tabbed interface.
					'before_section' => sprintf(
						'<div id="pwp-%1$s" class="tab-panel pwp-panel">',
						$section_name
					),
					'after_section'  => '</div>',
				);
			}

			add_settings_section(
				$section_name,
				null,
				// Check if there is a section description. If so convert to callback, escaping the output.
				! empty( $section['description'] ) ? function () use ( $section ) {
					printf( '<p class="section-description">%s</p>', esc_html( $section['description'] ) );
				} : '__return_false',
				$this->option_key,
				$section_args
			);

			foreach ( $section['fields'] as $field_idx => $field ) {
				// No field type associated, skip, no GUI.
				if ( ! isset( $field['type'] ) ) {
					continue;
				}

				add_settings_field(
					$field['name'],
					$field['title'],
					( isset( $field['callback'] ) ? $field['callback'] : array(
						$this,
						'output_field',
					) ),
					$this->option_key,
					$section_name,
					$field + array(
						'section'   => $section_name,
						'label_for' => sprintf( '%s_%s_%s', $this->option_key, $section_name, $field['name'] ),
					)
				);
			}
		}
	}

	/**
	 * Sanitization callback for settings field values before save
	 *
	 * @param array $input  Raw input.
	 *
	 * @return array
	 */
	public function sanitize_settings( $input ) {
		$output   = array();
		$sections = $this->get_fields();

		foreach ( $sections as $section => $data ) {
			if ( empty( $data['fields'] ) || ! is_array( $data['fields'] ) ) {
				continue;
			}

			foreach ( $data['fields'] as $field ) {
				$type = ! empty( $field['type'] ) ? $field['type'] : null;
				$name = ! empty( $field['name'] ) ? sprintf( '%s_%s', $section, $field['name'] ) : null;

				if ( empty( $type ) || ! isset( $input[ $name ] ) || '' === $input[ $name ] ) {
					continue;
				}

				$output[ $name ] = $this->sanitize_setting_by_field_type( $input[ $name ], $type, $field );
			}
		}

		return $output;
	}

	/**
	 * Sanitizes a setting value based on the field type.
	 *
	 * @since 2.7.0
	 *
	 * @param mixed  $value      The value to be sanitized.
	 * @param string $field_type The type of field.
	 * @param array  $field      Field settings.
	 *
	 * @return mixed The sanitized value.
	 */
	public function sanitize_setting_by_field_type( $value, $field_type, $field ) {

		// Sanitize depending on the type of field.
		switch ( $field_type ) {
			case 'number':
				$sanitized_value = is_numeric( $value ) ? intval( trim( $value ) ) : '';
				break;

			case 'switch':
			case 'checkbox':
				$sanitized_value = is_numeric( $value ) ? absint( trim( $value ) ) : '';
				break;

			case 'select':
				// Allow customised sanitizing.
				if ( isset( $field['sanitize_callback'] ) && is_callable( $field['sanitize_callback'] ) ) {
					$value = call_user_func( $field['sanitize_callback'], $value );
				} else {
					$value = sanitize_text_field( trim( $value ) );
				}

				if ( ! empty( $field['choices'] ) ) {
					if ( in_array( $value, array_keys( $field['choices'] ), true ) ) {
						$sanitized_value = $value;
					} else {
						/* translators: %s expands to the name of setting field. */
						add_settings_error( $field['title'], 'settings_updated', sprintf( __( '%s is an invalid option.', 'personalizewp' ), $field['title'] ), 'error' );
					}
				}
				break;

			default:
				if ( is_array( $value ) ) {
					$sanitized_value = $value;

					// Support all values in multidimentional arrays too.
					array_walk_recursive(
						$sanitized_value,
						function ( &$v ) {
							$v = sanitize_text_field( trim( $v ) );
						}
					);
					// Remove empty values
					$sanitized_value = array_filter( $sanitized_value );
				} else {
					$sanitized_value = sanitize_text_field( trim( $value ) );
				}
		}

		return $sanitized_value;
	}

	/**
	 * Compile HTML needed for displaying the field
	 *
	 * @param array $field Field settings.
	 *
	 * @return string HTML to be displayed
	 */
	public function render_field( $field ) {
		$output      = null;
		$section     = isset( $field['section'] ) ? $field['section'] : null;
		$is_gen_sec  = 'general' === $section; // Customised general section
		$type        = isset( $field['type'] ) ? $field['type'] : null;
		$name        = isset( $field['name'] ) ? $field['name'] : null;
		$class       = isset( $field['class'] ) ? $field['class'] : null;
		$placeholder = isset( $field['placeholder'] ) ? $field['placeholder'] : null;
		$description = isset( $field['description'] ) ? $field['description'] : null;
		$after_field = isset( $field['after_field'] ) ? $field['after_field'] : null;
		$default     = isset( $field['default'] ) ? $field['default'] : null;
		$required    = isset( $field['required'] ) ? $field['required'] : false;

		if ( isset( $field['value'] ) ) {
			$current_value = $field['value'];
		} else {
			$current_value = $this->get_option( $section . '_' . $name );
		}

		$option_key = $this->option_key;

		if ( ! $section || ! $type || ! $name ) {
			return '';
		}

		if ( 'select' === $type && ( empty( $field['choices'] ) || ! is_array( $field['choices'] ) ) ) {
			return '';
		}

		switch ( $type ) {
			case 'text':
			case 'number':
				$output = sprintf(
					'<input type="%1$s" name="%2$s[%3$s_%4$s]" id="%2$s[%3$s_%4$s]" class="%5$s" placeholder="%6$s" value="%7$s" %8$s %9$s /> %10$s',
					esc_attr( $type ),
					esc_attr( $option_key ),
					esc_attr( $section ),
					esc_attr( $name ),
					esc_attr( $class ),
					esc_attr( $placeholder ),
					esc_attr( $current_value ),
					$required ? 'required' : '',
					$is_gen_sec ? 'form="pwp_form_settings" ' : '', // Used to locate fields outside the form, but to still apply inside
					$after_field ? wp_kses_post( $after_field ) : ''
				);
				break;

			case 'switch':
			case 'checkbox':
				if ( isset( $current_value ) ) {
					$value = $current_value;
				} elseif ( isset( $default ) ) {
					$value = $default;
				} else {
					$value = 0;
				}

				if ( 'switch' === $type ) {
					$input_format = '<label for="%4$s"><input type="checkbox" role="switch" name="%1$s[%2$s_%3$s]" id="%4$s" value="1" %5$s %6$s %7$s data-save-alert /> %8$s</label>';
				} else {
					$input_format = '<label for="%4$s"><input type="checkbox" name="%1$s[%2$s_%3$s]" id="%4$s" value="1" %5$s %6$s %7$s data-save-alert /> %8$s</label>';
				}

				$input_id = sprintf(
					'%1$s_%2$s_%3$s',
					$option_key,
					$section,
					$name
				);
				$output   = sprintf(
					$input_format,
					esc_attr( $option_key ),
					esc_attr( $section ),
					esc_attr( $name ),
					esc_attr( $input_id ),
					checked( $value, 1, false ),
					$required ? 'required' : '',
					$is_gen_sec ? 'form="pwp_form_settings" ' : '', // Used to locate fields outside the form, but to still apply inside
					$after_field ? wp_kses_post( $after_field ) : ''
				);
				break;

			case 'select':
				if ( isset( $current_value ) ) {
					$value = $current_value;
				} elseif ( isset( $default ) ) {
					$value = $default;
				} else {
					$value = 0;
				}

				$output = sprintf(
					'<select name="%1$s[%2$s_%3$s]" class="%1$s[%2$s_%3$s]" %4$s %5$s>',
					esc_attr( $option_key ),
					esc_attr( $section ),
					esc_attr( $name ),
					$required ? 'required' : '',
					$is_gen_sec ? 'form="pwp_form_settings" ' : '' // Used to locate fields outside the form, but to still apply inside
				);
				foreach ( $field['choices'] as $field_value => $field_label ) {
					$output .= sprintf(
						'<option value="%1$s" %2$s>%3$s</option>',
						esc_attr( $field_value ),
						selected( $field_value === $value, true, false ),
						esc_html( $field_label )
					);
				}
				$output .= '</select>';
				break;
		}
		if ( ! empty( $description ) ) {
			if ( is_array( $description ) ) {
				foreach ( $description as $paragraph ) {
					$output .= wp_kses_post( sprintf( '<p class="description">%s</p>', $paragraph ) );
				}
			} else {
				$output .= wp_kses_post( sprintf( '<p class="description">%s</p>', $description ) );
			}
		}

		return $output;
	}

	/**
	 * Render callback for field.
	 *
	 * @param array $field  Field to be rendered.
	 *
	 * @return void
	 */
	public function output_field( $field ) {
		$method = 'output_' . $field['name'];

		if ( method_exists( $this, $method ) ) {
			echo call_user_func( array( $this, $method ), $field );  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			return;
		}

		echo $this->render_field( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Escaped within render
	}

	/**
	 * Compile HTML needed for displaying the custom fields.
	 *
	 * @since 3.2.0
	 *
	 * @param array $field Field settings.
	 *
	 * @return string Sanitised HTML to be displayed
	 */
	public function output_custom_fields( $field ) {

		$section     = isset( $field['section'] ) ? $field['section'] : null;
		$name        = isset( $field['name'] ) ? $field['name'] : null;
		$class       = isset( $field['class'] ) ? $field['class'] : null;
		$placeholder = isset( $field['placeholder'] ) ? $field['placeholder'] : null;
		$description = isset( $field['description'] ) ? $field['description'] : null;

		if ( isset( $field['value'] ) ) {
			$current_value = $field['value'];
		} elseif ( isset( $this->options[ $section . '_' . $name ] ) ) {
			$current_value = $this->options[ $section . '_' . $name ];
		} else {
			$current_value = null;
		}

		$option_key = $this->option_key;

		// Add the custom field input, with template for each added field.
		$output = sprintf(
			'<custom-tag-list class="flow" input="%1$s_%2$s_%3$s_input" submit="%1$s_%2$s_%3$s_submit" list="%1$s_%2$s_%3$s_list">
				<div class="cluster">
					<input type="text" name="%1$s[%2$s_%3$s][]" id="%1$s_%2$s_%3$s_input" class="%4$s" placeholder="%5$s" value="" />
					<button hidden id="%1$s_%2$s_%3$s_submit" class="btn primary" type="submit" data-save-alert>%6$s</button>
				</div>
				<template>
					<custom-tag class="tag">
						<label>
							<input type="hidden" name="%1$s[%2$s_%3$s][]" value="" /> <span></span>
						</label>
						<button type="button" data-save-alert data-confirm-modal="#removeProfileFieldModal" aria-label="%7$s">%8$s</button>
					</custom-tag>
				</template>',
			esc_attr( $option_key ),
			esc_attr( $section ),
			esc_attr( $name ),
			esc_attr( $class ),
			esc_attr( $placeholder ),
			esc_html( $field['submit_label'] ),
			esc_attr( $field['aria_label'] ),
			Utils::get_svg_icon( 'cross' )
		);

		// Add all the existing custom fields as hidden inputs
		$tags    = array();
		$output .= sprintf(
			'<div class="tag-list cluster" id="%1$s_%2$s_%3$s_list">',
			esc_attr( $option_key ),
			esc_attr( $section ),
			esc_attr( $name ),
		);
		foreach ( (array) $current_value as $label ) {
			$tags[] = sprintf(
				'<custom-tag class="tag">
					<label for="%7$s">
						<input checked type="checkbox" name="%1$s[%2$s_%3$s][]" id="%7$s" value="%4$s" /> <span>%4$s</span>
					</label>
					<button type="button" hidden data-save-alert data-confirm-modal="#removeProfileFieldModal" aria-label="%5$s">%6$s</button>
				</custom-tag>',
				esc_attr( $option_key ),
				esc_attr( $section ),
				esc_attr( $name ),
				esc_attr( $label ),
				esc_attr( $field['aria_label'] ),
				Utils::get_svg_icon( 'cross' ),
				wp_unique_id( esc_attr( "{$option_key}_{$section}_{$name}_" ) )
			);
		}
		$output .= implode( '', $tags );
		$output .= '</div>';

		if ( ! empty( $description ) ) {
			if ( is_array( $description ) ) {
				foreach ( $description as $paragraph ) {
					$output .= wp_kses_post( sprintf( '<p class="description">%s</p>', $paragraph ) );
				}
			} else {
				$output .= wp_kses_post( sprintf( '<p class="description">%s</p>', $description ) );
			}
		}

		$output .= '</custom-tag-list>';

		// Output the modals only when required.
		add_action( 'personalizewp_after_settings', [ $this, 'output_custom_fields_confirm_modal' ] );

		return $output;
	}

	/**
	 * Display HTML modal to confirm custom field deletion.
	 *
	 * @since 3.2.0
	 *
	 * @return void
	 */
	public function output_custom_fields_confirm_modal() {
		require_once $this->plugin->plugin_path( 'admin/partials/settings/_modal-custom-field-delete.php' );
	}

	/**
	 * Compile HTML needed for displaying the security URL tokens.
	 *
	 * @since 3.1.0
	 *
	 * @param array $field Field settings.
	 *
	 * @return string Sanitised HTML to be displayed
	 */
	public function output_url_tokens( $field ) {

		$api        = API::instance();
		$all_tokens = $api->get_all_api_auth_tokens();
		// Remove automatic home token from display.
		$home_token = $api->get_api_auth_token( set_url_scheme( get_home_url(), 'https' ) ); // Always use https
		unset( $all_tokens[ array_search( $home_token, $all_tokens, true ) ] );

		$description = isset( $field['description'] ) ? $field['description'] : null;

		$output = '';

		if ( ! empty( $description ) ) {
			if ( is_array( $description ) ) {
				foreach ( $description as $paragraph ) {
					$output .= wp_kses_post( sprintf( '<p class="description">%s</p>', $paragraph ) );
				}
			} else {
				$output .= wp_kses_post( sprintf( '<p class="description">%s</p>', $description ) );
			}
		}

		// Add the custom field input, with template for each added field.
		$output .= sprintf(
			'
		<ajax-html id="api-keys" event-name="pwp-ajax-form:success" event-uid="keys-updated">
		<table class="wp-list-table widefat striped api_key_table">
		<caption>%1$s</caption>
			<thead>
			<tr>
				<th scope="col" class="manage-column column-url column-primary">%2$s</th>
				<th scope="col" class="manage-column column-url column-primary">%3$s</th>
			</tr>
			</thead>
			<tbody>
			',
			esc_html__( 'API Keys', 'personalizewp' ),
			esc_html__( 'URL', 'personalizewp' ),
			esc_html__( 'Actions', 'personalizewp' ),
		);

		if ( empty( $all_tokens ) ) {
			$output .= sprintf(
				'
				<tr class="no-items">
					<td colspan="2">%1$s</td>
				</tr>
				',
				esc_html__( "You don't have any API keys. Let's go create one!", 'personalizewp' ),
			);
		} else {
			// Loop existing URLs.
			$base_revoke_url = add_query_arg( array( 'action' => 'pwp_revoke_url_api_authentication' ), admin_url( 'admin-ajax.php' ) );
			foreach ( $all_tokens as $token_url => $token ) {
				$revoke_url = add_query_arg( array( 'url' => $token_url ), $base_revoke_url );
				$output    .= sprintf(
					'
					<tr>
						<td>%1$s</td>
						<td>
							<a class="trash" href="%2$s" data-show-modal="#revokeURLTokenModal">%3$s</a>
						</td>
					</tr>
					',
					esc_url( $token_url ),
					esc_url( wp_nonce_url( $revoke_url, "revoke-url-token-{$token_url}" ) ),
					esc_html__( 'Revoke URL', 'personalizewp' ),
				);
			}
		}
		// Table footer.
		$output .= sprintf(
			'</tbody>
		</table>
		</ajax-html>
		<button type="button" class="button primary" id="api_add_key" data-show-modal="#addURLTokenModal">%1$s</button>
			',
			esc_html__( 'Add New Key', 'personalizewp' )
		);

		// Output the modals only when required.
		add_action( 'personalizewp_after_settings', [ $this, 'output_url_tokens_modals' ] );

		return $output;
	}

	/**
	 * Output the modals used by the URL Tokens within settings screen.
	 *
	 * @since 3.1.0
	 */
	public function output_url_tokens_modals() {
		include_once $this->plugin->plugin_path( 'admin/partials/settings/_modal-add-url.php' );
		include_once $this->plugin->plugin_path( 'admin/partials/settings/_modal-revoke-url.php' );
	}
}
