<?php
/**
 * A class that adds a stats column to wp-admin Post List.
 *
 * @package automattic/jetpack-stats-admin
 */

namespace Automattic\Jetpack\Stats_Admin;

use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Stats\Options as Stats_Options;
use Automattic\Jetpack\Stats\WPCOM_Stats;
use Automattic\Jetpack\Status\Host;
use NumberFormatter;

/**
 * Add a Stats column in the post and page lists.
 */
class Admin_Post_List_Column {

	/**
	 * Create the object.
	 *
	 * @return self
	 */
	public static function register() {
		return new self();
	}

	/**
	 * A list of NumberFormatters.
	 *
	 * @var \NumberFormatter[]
	 */
	private $formatter;

	/**
	 * The current locale.
	 *
	 * @var string
	 */
	private $locale;

	/**
	 * The constructor.
	 */
	public function __construct() {
		// Add an icon to see stats in WordPress.com for a particular post.
		add_action( 'admin_print_styles-edit.php', array( $this, 'stats_load_admin_css' ) );

		add_filter( 'manage_posts_columns', array( $this, 'add_stats_post_table' ) );
		add_filter( 'manage_pages_columns', array( $this, 'add_stats_post_table' ) );

		add_action( 'manage_posts_custom_column', array( $this, 'add_stats_post_table_cell' ), 10, 2 );
		add_action( 'manage_pages_custom_column', array( $this, 'add_stats_post_table_cell' ), 10, 2 );
	}

	/**
	 * Load CSS needed for Stats column width in WP-Admin area.
	 *
	 * @since 4.7.0
	 */
	public function stats_load_admin_css() {
		?>
		<style type="text/css">
			.wp-list-table.fixed .column-stats {
				width: 9em;
				white-space: nowrap;
			}
		</style>
		<?php
	}

	/**
	 * Set content for cell with link to an entry's stats in Odyssey Stats.
	 *
	 * @param string $column  The name of the column to display.
	 * @param int    $post_id The current post ID.
	 *
	 * @since 4.7.0
	 */
	public function add_stats_post_table_cell( $column, $post_id ) {
		if ( 'stats' === $column ) {
			if ( 'publish' !== get_post_status( $post_id ) ) {
				printf(
					'<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
					esc_html__( 'No stats', 'jetpack-stats-admin' )
				);
			} else {
				// Link to the wp-admin stats page.
				$query_args = array(
					'from'         => 'postList',
					'jp_post_type' => get_post_type( $post_id ),
				);

				$list_criteria_params = array(
					's'             => sanitize_text_field( get_search_query() ),
					'paged'         => absint( get_query_var( 'paged' ) ),
					'post_status'   => sanitize_text_field( get_query_var( 'post_status' ) ),
					'orderby'       => sanitize_text_field( get_query_var( 'orderby' ) ),
					'order'         => sanitize_text_field( get_query_var( 'order' ) ),
					'author'        => absint( get_query_var( 'author' ) ),
					'cat'           => absint( get_query_var( 'cat' ) ), // 'cat' is the query var for category ID
					'm'             => absint( get_query_var( 'm' ) ),   // 'm' is the query var for YYYYMM
					'category_name' => sanitize_text_field( get_query_var( 'category_name' ) ),
				);

				foreach ( $list_criteria_params as $key => $value ) {
					if ( isset( $_GET[ $key ] ) && $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking if the key existts and not reading the value from the request.
						$query_args[ 'jp_' . $key ] = $value;
					}
				}

				$stats_post_url = add_query_arg( $query_args, admin_url( 'admin.php?page=stats#!/stats/post/' . $post_id . '/' . \Jetpack_Options::get_option( 'id', 0 ) ) );
				// Unless the user is on a Default style WOA site, in which case link to Calypso.
				if ( ( new Host() )->is_woa_site() && Stats_Options::get_option( 'enable_odyssey_stats' ) && 'wp-admin' !== get_option( 'wpcom_admin_interface' ) ) {
					$stats_post_url = Redirect::get_url(
						'calypso-stats-post',
						array(
							'path' => $post_id,
						)
					);
				}

				static $post_views = null;

				/**
				 * Jetpack_stats_get_post_page_views_for_current_list makes a request with all post ids in the current $wp_query.
				 * This way, we'll make a single API request instead of making one for each post.
				 *
				 * For this reason, we'll cache the result with the static $post_views variable.
				 */
				if ( null === $post_views ) {
					$post_views = $this->get_post_page_views_for_current_list();
				}

				$views = $post_views[ $post_id ] ?? null;

				$current_locale = get_locale();

				if ( null !== $views ) {
					$formatted_views = class_exists( '\NumberFormatter' )
						? $this->get_formatter( $current_locale )->format( $views )
						: $this->get_fallback_format_to_compact_version( $views );
				} else {
					$formatted_views = '';
				}

				?>
				<a href="<?php echo esc_url( $stats_post_url ); ?>"
					title="<?php echo esc_html__( 'Views for the last thirty days. Click for detailed stats', 'jetpack-stats-admin' ); ?>">
					<span
						class="dashicons dashicons-visibility"></span>&nbsp;<span><?php echo null !== $views ? esc_html( $formatted_views ) : ''; ?></span>
				</a>
				<?php
			}
		}
	}

	/**
	 * Set header for column that allows to view an entry's stats.
	 *
	 * @param array $columns An array of column names.
	 *
	 * @return mixed
	 */
	public function add_stats_post_table( $columns ) {
		// Skip stats column for non-public post types when screen info is available.
		if ( function_exists( 'get_current_screen' ) ) {
			$screen = get_current_screen();
			if ( $screen && $screen->post_type ) {
				$post_type_object = get_post_type_object( $screen->post_type );
				if ( $post_type_object && ! $post_type_object->public ) {
					return $columns;
				}
			}
		}

		/**
		 * The manage_options capability is a fallback for Simple.
		 * This should be updated with a proper fix. Implemented based on this PR: https://github.com/Automattic/jetpack/pull/41549.
		 */
		$has_access = current_user_can( 'view_stats' ) || current_user_can( 'manage_options' );

		/*
		 * Stats can be accessed in wp-admin or in Calypso,
		 * depending on what version of the stats screen is enabled on your site.
		 *
		 * In both cases, the user must be allowed to access stats.
		 *
		 * If the Odyssey Stats experience isn't enabled, the user will need to go to Calypso,
		 * so they need to be connected to WordPress.com to be able to access that page.
		 */
		if (
			! $has_access
			|| (
				! Stats_Options::get_option( 'enable_odyssey_stats' )
				&& ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected()
			)
		) {
			return $columns;
		}

		// Array-Fu to add before comments.
		$pos = array_search( 'comments', array_keys( $columns ), true );

		// Fallback to the last position if the post type does not support comments.
		if ( ! is_int( $pos ) ) {
			$pos = count( $columns );
		}

		// If comments position is 0, then prepend the element at the beginning of the array.
		if ( 0 === $pos ) {
			return array_merge(
				array( 'stats' => esc_html__( 'Views: 30 days', 'jetpack-stats-admin' ) ),
				$columns
			);
		}

		$chunks             = array_chunk( $columns, $pos, true );
		$chunks[0]['stats'] = esc_html__( 'Views: 30 days', 'jetpack-stats-admin' );

		return call_user_func_array( 'array_merge', $chunks );
	}

	/**
	 * Get a list of post views for each post id from the global $wp_query.
	 *
	 * @return array
	 */
	public function get_post_page_views_for_current_list(): array {
		global $wp_query;

		if ( $wp_query->posts ) {
			$post_ids = wp_list_pluck( $wp_query->posts, 'ID' );
		} elseif ( wp_doing_ajax() && ! empty( $_POST['action'] ) && 'inline-save' === $_POST['action'] && ! empty( $_POST['post_ID'] ) && check_ajax_referer( 'inlineeditnonce', '_inline_edit' ) ) {
			$post_ids = array( absint( wp_unslash( $_POST['post_ID'] ) ) );
		} else {
			return array();
		}

		$wpcom_stats = $this->get_stats();
		$post_views  = $wpcom_stats->get_total_post_views(
			array(
				'num'      => 30,
				'post_ids' => implode( ',', $post_ids ),
			)
		);

		if ( is_wp_error( $post_views ) || empty( $post_views ) ) {
			return array();
		}

		$views = array();

		foreach ( $post_views['posts'] as $post ) {
			$views[ $post['ID'] ] = $post['views'];
		}

		return $views;
	}

	/**
	 * Get the stats object.
	 *
	 * @return WPCOM_Stats
	 */
	protected function get_stats() {
		return new WPCOM_Stats();
	}

	/**
	 * Get and validate the locale.
	 *
	 * @param string $locale The locale to validate.
	 *
	 * @return string The validated locale.
	 */
	public function get_validated_locale( string $locale ): string {
		if ( isset( $this->locale ) ) {
			return $this->locale;
		}

		/*
		 * Check if the locale is valid and available.
		 * If not, fallback to en_US.
		 */
		if ( ! in_array( $locale, \IntlCalendar::getAvailableLocales(), true ) ) {
			$locale = 'en_US';
		}

		$this->locale = $locale;
		return $locale;
	}

	/**
	 * Get the NumberFormatter instance.
	 *
	 * @param string $locale The current locale.
	 *
	 * @return NumberFormatter
	 */
	protected function get_formatter( string $locale ): \NumberFormatter {
		if ( isset( $this->formatter[ $locale ] ) ) {
			return $this->formatter[ $locale ];
		}

		$locale = $this->get_validated_locale( $locale );

		/**
		 * PHP's NumberFormatter is just a wrapper over the ICU C library. The library does support decimal compact short formatter, but PHP doesn't have a stub for it (=< PHP 8.4).
		 *
		 * @see https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/unum_8h.html UNUM_DECIMAL_COMPACT_SHORT constant.
		 */
		$compact_decimal_short = 14;

		/**
		 * NumberFormatter::DECIMAL_COMPACT_SHORT only exists in PHP 8.5 and later. At this time, NumberFormatter::DECIMAL_COMPACT_SHORT only exists in PHP `main` branch.
		 *
		 * Use the constant if it's defined since it's safer.
		 */
		if ( defined( '\NumberFormatter::DECIMAL_COMPACT_SHORT' ) ) {
			// @phan-suppress-next-line PhanUndeclaredConstantOfClass
			$compact_decimal_short = NumberFormatter::DECIMAL_COMPACT_SHORT;
		}

		try {
			$formatter = new \NumberFormatter( $locale, $compact_decimal_short );
			$formatter->setAttribute( \NumberFormatter::MAX_FRACTION_DIGITS, 1 );
		} catch ( \Exception $e ) {
			// Fallback to decimal if for some reason it fails to work.
			$formatter = new \NumberFormatter( $locale, \NumberFormatter::DECIMAL );
		}

		$this->formatter[ $locale ] = $formatter;

		return $formatter;
	}

	/**
	 * Fallback Format a number to a compact version if the Intl extension is not available.
	 *
	 * @param int $views The given number.
	 *
	 * @return string
	 */
	public function get_fallback_format_to_compact_version( $views ) {
		if ( $views >= 10000000 ) {
			return round( $views / 1000000 ) . 'M';
		} elseif ( $views >= 1000000 ) {
			$views = round( $views / 1000000, 1 );
			return preg_replace( '/\.0$/', '', (string) $views ) . 'M';
		} elseif ( $views >= 10000 ) {
			return round( $views / 1000 ) . 'K';
		} elseif ( $views >= 1000 ) {
			$views = round( $views / 1000, 1 );
			return preg_replace( '/\.0$/', '', (string) $views ) . 'K';
		}

		return (string) $views;
	}
}
