<?php
/**
 * Custom List Table for displaying data in a table format in WordPress plugin.
 *
 * @link       https://personalizewp.com/
 * @since      3.2.0 Merged into PWP
 * @since      1.0.0
 *
 * @package    PersonalizeWP
 * @subpackage PersonalizeWP/Admin
 */

namespace PersonalizeWP\Admin;

use PersonalizeWP\Admin\PageContacts;
use PersonalizeWP\Model\Contact;

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

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * Custom List Table for displaying data in a table format in WordPress plugin.
 */
class ListTableContacts extends \WP_List_Table {

	/**
	 * The screen name.
	 *
	 * @var string
	 */
	public $screen_id = 'pwp_contacts';

	/**
	 * Default number of items per page
	 */
	const ITEMS_PER_PAGE = 20;

	/**
	 * Class constructor
	 */
	public function __construct() {
		parent::__construct(
			[
				'singular' => 'profile',
				'plural'   => 'profiles',
				'ajax'     => false,
				'screen'   => $this->screen_id,
			]
		);
	}

	/**
	 * Retrieve the table columns.
	 *
	 * @return array Associative array of table columns with column name as key and column title as value.
	 */
	public function get_columns() {

		// Base contacts columns
		$columns = array(
			'name'       => __( 'Name', 'personalizewp' ),
			'email'      => __( 'Email', 'personalizewp' ),
			'last_seen'  => __( 'Last Seen', 'personalizewp' ),
			'lead_score' => __( 'Lead Score', 'personalizewp' ),
		);

		/**
		 * Filters the contacts list table columns.
		 *
		 * @since 3.3.0
		 *
		 * @param array $columns Contacts table columns.
		 */
		return apply_filters( 'personalizewp_contacts_table_columns', $columns );
	}

	/**
	 * Get sortable columns
	 *
	 * By default order by creation date descending and lead score descending.
	 *
	 * @return array
	 */
	protected function get_sortable_columns() {

		$sortable = array(
			'name'       => 'last_name', // Map col to db col
			'email'      => 'email',
			'created'    => [ 'created', true, __( 'Created', 'personalizewp' ), __( 'Table ordered by Created date.', 'personalizewp' ), 'desc' ],
			'last_seen'  => [ 'last_seen', true, __( 'Last Seen', 'personalizewp' ), __( 'Table ordered by Last Seen date.', 'personalizewp' ), 'desc' ],
			'lead_score' => [ 'lead_score', false ],
		);

		/**
		 * Filters the sortable columns for contacts list tables.
		 *
		 * @since 3.3.0
		 *
		 * @param array $sortable Contacts table sortable columns.
		 */
		return apply_filters( 'personalizewp_contacts_table_sortable_columns', $sortable );
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @return string Name of the default primary column, in this case, 'first_name'.
	 */
	protected function get_default_primary_column_name() {
		return 'name';
	}

	/**
	 * Generates and displays row action links.
	 *
	 * @param array  $item        Item being acted upon.
	 * @param string $column_name Current column name.
	 * @param string $primary     Primary column name.
	 *
	 * @return string Row actions output for Contact, or an empty string if the current column is not the primary column.
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) {
		if ( $primary !== $column_name ) {
			return '';
		}

		$title = $this->column_default( $item, 'name' );

		$actions = array();

		$actions['view'] = sprintf(
			'<a href="%s" aria-label="%s" rel="bookmark">%s</a>',
			admin_url( PageContacts::$view_url . $item['ID'] ),
			/* translators: %s: Contact name. */
			esc_attr( sprintf( _x( 'View &#8220;%s&#8221;', 'Contact name', 'personalizewp' ), $title ) ),
			esc_html__( 'View', 'personalizewp' )
		);

		$actions['delete'] = sprintf(
			'<a class="trash" data-show-modal="#deleteContactModal" href="%s" aria-label="%s">%s</a>',
			wp_nonce_url( admin_url( PageContacts::$delete_url . $item['ID'] ), 'delete-contact-' . $item['ID'] ),
			/* translators: %s: Contact name. */
			esc_attr( sprintf( _x( 'Delete &#8220;%s&#8221;', 'Contact name', 'personalizewp' ), $title ) ),
			esc_html__( 'Delete', 'personalizewp' )
		);

		/**
		 * Filters the action links for each Contact in the list table.
		 *
		 * @param string[] $actions  An array of action links for each contact.
		 *                           Default 'View', 'Delete'.
		 * @param array    $item     Contact record being acted upon.
		 */
		$actions = apply_filters( 'personalizewp_contacts_row_actions', $actions, $item );

		return $this->row_actions( $actions );
	}

	/**
	 * Handles the user name column output.
	 *
	 * @since 3.3.0
	 *
	 * @param array $item Contact
	 *
	 * @return string
	 */
	protected function column_name( $item ) {
		$title = __( 'Anonymous User', 'personalizewp' );

		if ( ! empty( $item['first_name'] ) || ! empty( $item['last_name'] ) ) {
			$title = sprintf(
				'%1$s %2$s',
				$item['first_name'],
				$item['last_name']
			);
		}

		// Link to contact view
		return sprintf(
			'<a href="%s" aria-label="%s" rel="bookmark">%s</a>',
			admin_url( PageContacts::$view_url . $item['ID'] ),
			/* translators: %s: Contact name. */
			esc_attr( sprintf( _x( 'View &#8220;%s&#8221;', 'Contact name', 'personalizewp' ), $title ) ),
			esc_html( $title )
		);
	}

	/**
	 * Email column renderer
	 *
	 * @since 3.3.0
	 *
	 * @param array $item Contact
	 *
	 * @return string
	 */
	protected function column_email( $item ) {

		if ( empty( $item['email'] ) ) {
			return '-';
		}

		return sprintf(
			'<a href="mailto:%1$s" target="_blank">%2$s</a>',
			esc_attr( $item['email'] ),
			esc_html( $item['email'] )
		);
	}

	/**
	 * Last Seen column renderer
	 *
	 * @since 3.3.0
	 *
	 * @param array $item Contact
	 *
	 * @return string
	 */
	protected function column_last_seen( $item ) {

		$contact = Contact::get_instance( $item['ID'] );
		if ( ! $contact ) {
			return '-';
		}

		$last_seen = date_create( $contact->last_seen );
		if ( ! empty( $last_seen ) ) {
			// $last_seen = date_format( $last_seen, get_option( 'time_format' ) . ' - ' . get_option( 'date_format' ) );
			$last_seen = date_format( $last_seen, 'Y-m-d H:i:s' ); // Fixed format, not setup for i18n
		}
		return esc_html( empty( $last_seen ) ? '-' : $last_seen );
	}

	/**
	 * Default column renderer
	 *
	 * @param array  $item        Contact
	 * @param string $column_name List table column
	 *
	 * @return string
	 */
	protected function column_default( $item, $column_name ) {

		// Check that the column name exists.
		$content = $item[ $column_name ] ?? '';

		/**
		 * Filter each custom column in the Contacts list table.
		 *
		 * Defaults to the column name of the contact item.
		 *
		 * @since 3.3.0
		 *
		 * @param string $content     Content of the $column_name index in $item array
		 * @param string $column_name List table column name
		 * @param array  $item        Contact entry
		 */
		$content = apply_filters( 'personalizewp_contacts_table_custom_column', $content, $column_name, $item );

		return wp_kses(
			$content,
			[
				'a' => [
					'href'       => [],
					'aria-label' => [],
				],
			]
		);
	}

	/**
	 * Adds table filtering
	 *
	 * @since 3.3.0
	 *
	 * @param string $which Top or bottom of table
	 *
	 * @return void
	 */
	public function extra_tablenav( $which ) {
		if ( 'top' !== $which ) {
			return;
		}
		?>
		<div class="alignleft actions">
			<?php
			/**
			 * Action hook to allow additional inputs in the contacts filter dropdown area.
			 *
			 * @since 3.3.0
			 */
			do_action( 'personalizewp_contacts_filter_fields' );

			// If something has added filter inputs, add the necessary form inputs and submit button.
			if ( has_action( 'personalizewp_contacts_filter_fields' ) ) :
				$this->add_form_actions();
				?>
				<input type="submit" class="secondary btn" value="<?php esc_attr_e( 'Filter', 'personalizewp' ); ?>" />
				<?php
			endif;
			?>
		</div>
		<?php
	}

	/**
	 * Generates the table navigation above or below the table
	 *
	 * Overridden to remove bulk actions and nonces
	 *
	 * @param string $which Top or bottom of table
	 *
	 * @return void
	 */
	protected function display_tablenav( $which ) {
		?>
	<div class="tablenav <?php echo esc_attr( $which ); ?>">

		<?php
		$this->extra_tablenav( $which );
		$this->pagination( $which );
		?>

	</div>
		<?php
	}

	/**
	 * Add hidden inputs to trigger correct form processing. Only outputs once.
	 *
	 * @since 3.3.0
	 *
	 * @return void
	 */
	private function add_form_actions() {
		static $displayed;

		if ( isset( $displayed ) ) {
			return;
		}
		$displayed = true;

		$page   = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: nonce verification not required
		$action = isset( $_REQUEST[ Admin::URL_ACTION ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ Admin::URL_ACTION ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: nonce verification not required
		?>
			<input type="hidden" name="page" value="<?php echo esc_attr( $page ); ?>" />
			<input type="hidden" name="<?php echo esc_attr( Admin::URL_ACTION ); ?>" value="<?php echo esc_attr( $action ); ?>" />
		<?php
	}

	/**
	 * Displays the search box.
	 *
	 * @param string $text     The 'submit' button label.
	 * @param string $input_id ID attribute value for the search input field.
	 */
	public function search_box( $text, $input_id ) {

		$this->add_form_actions();

		$search = isset( $_REQUEST['search'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['search'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: nonce verification not required
		?>
		<p class="search-box">

			<?php
			/**
			 * Fires during the Contacts search form.
			 */
			do_action( 'personalizewp_contacts_search_fields' );
			?>

			<label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo esc_html( $text ); ?>:</label>
			<input type="text" class="search-icon" id="<?php echo esc_attr( $input_id ); ?>" name="search" value="<?php echo esc_attr( $search ); ?>"
				placeholder="<?php esc_html_e( 'Search name or email', 'personalizewp' ); ?>" />
			<input type="submit" class="secondary btn" value="<?php echo esc_html( $text ); ?>" />
		</p>
		<?php
	}

	/**
	 * Prepare the table items. Retrieve and set the data for the table.
	 *
	 * @return void
	 */
	public function prepare_items() {
		global $wpdb;

		$join  = '';
		$where = '';
		// Process Searching
		$search = isset( $_REQUEST['search'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['search'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: used as flag
		if ( $search ) {
			$search_cols    = array(
				'first_name',
				'last_name',
				'email',
				'uid',
			);
			$search_clauses = array();

			foreach ( $search_cols as $col ) {
				$search_clauses[] = $wpdb->prepare( "{$wpdb->prefix}pwp_contacts.%i LIKE %s", $col, '%' . $wpdb->esc_like( $search ) . '%' );
			}
			$where .= ' AND (' . implode( ' OR ', $search_clauses ) . ')';
		}

		// Process Ordering
		$orderby = 'last_seen DESC, lead_score '; // Default when not custom, 2nd order is added below
		if ( ! empty( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$_orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: verified elsewhere
			$parsed   = $this->parse_orderby( $_orderby );
			if ( $parsed ) {
				$orderby = $parsed;
			}
		}
		$order   = ! empty( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: verified elsewhere
		$order   = $this->parse_order( $order );
		$orderby = 'ORDER BY ' . $orderby . ' ' . $order;

		// Process pagination
		$per_page = $this->get_items_per_page( "edit_{$this->screen_id}_per_page", self::ITEMS_PER_PAGE );
		$page     = $this->get_pagenum();
		$offset   = absint( ( $page - 1 ) * $per_page ) . ', ';
		$limits   = 'LIMIT ' . $offset . $per_page;

		$pieces = array( 'join', 'where' );
		/**
		 * Filters Contacts query clauses at once, for convenience.
		 *
		 * Covers the JOIN and WHERE clauses.
		 *
		 * @param string[] $clauses {
		 *     Associative array of the clauses for the query.
		 *
		 *     @type string $join     The JOIN clause of the query.
		 *     @type string $where    The WHERE clause of the query.
		 * }
		 */
		$clauses = (array) apply_filters_ref_array( 'personalizewp_contacts_clauses_request', array( compact( $pieces ) ) );
		$join    = isset( $clauses['join'] ) ? $clauses['join'] : '';
		$where   = isset( $clauses['where'] ) ? $clauses['where'] : '';

		// Get the results from the options above
		$this->items = $wpdb->get_results(
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: inputs sanitised/validated above
			"SELECT SQL_CALC_FOUND_ROWS * FROM {$wpdb->prefix}pwp_contacts $join WHERE 1=1 $where $orderby $limits", // nosemgrep: audit.php.wp.security.sqli.input-in-sinks -- Reason: inputs sanitised/validated above
			ARRAY_A
			// phpcs:enable
		);

		$total_items = (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' );

		$this->set_pagination_args(
			array(
				'total_items' => $total_items,
				'per_page'    => $per_page,
			)
		);
	}

	/**
	 * Validates that the 'orderby' is valid
	 *
	 * @param string $orderby The 'orderby' query variable.
	 * @return string The sanitized 'orderby' query variable.
	 */
	protected function parse_orderby( $orderby ) {

		$orderby = strtolower( $orderby );

		// Used to filter values.
		$allowed_keys = [];
		$_sortable    = $this->get_sortable_columns();
		foreach ( $_sortable as $data ) {
			if ( is_string( $data ) ) {
				$allowed_keys[] = $data;
			} elseif ( is_array( $data ) && is_string( $data[0] ) ) {
				$allowed_keys[] = $data[0];
			}
		}

		if ( ! in_array( $orderby, $allowed_keys, true ) ) {
			return false;
		}

		return $orderby;
	}

	/**
	 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
	 *
	 * @param string $order The 'order' query variable.
	 * @return string The sanitized 'order' query variable.
	 */
	protected function parse_order( $order ) {
		if ( ! is_string( $order ) || empty( $order ) ) {
			return 'DESC';
		}

		if ( 'ASC' === strtoupper( $order ) ) {
			return 'ASC';
		} else {
			return 'DESC';
		}
	}
}
