<?php

namespace PostViewsCounter;

use WP_PowerSuite\Core\Contracts\DataLocationsInterface;
use WP_PowerSuite\Core\AbstractModule;
use WP_Post;
use WP_Query;

class Module extends AbstractModule implements DataLocationsInterface {

	public const META_KEY = '_wpps_post_views';

	private const MODULE_ID              = 'post-views-counter';
	private const MODULE_VERSION         = '1.0.3';
	private const SHORTCODE              = 'wpps_post_views';
	private const LEGACY_SHORTCODE       = 'post_views';
	private const COOKIE_CID             = 'wpps_pv_cid';
	private const LIST_COLUMN            = 'wpps_post_views';
	private const DEFAULT_LABEL_TEMPLATE = '%views% views';
	private const CONTENT_FILTER_PRIORITY = 12;

	/** @var array<string, mixed>|null */
	private ?array $settings_cache = null;

	private static ?int $display_inserted_for_post = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Post Views Counter', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Popular-post style view counts with sensible repeat-visit handling and flexible display options.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return 'both';
	}

	public function getDependencies(): array {
		return [];
	}

	public function getDataLocations(): array {
		return [
			'post_meta' => [ self::META_KEY ],
		];
	}

	protected function registerHooks(): void {
		if ( is_admin() ) {
			$this->registerAdminListColumns();
			return;
		}

		add_action( 'template_redirect', [ $this, 'maybeRecordView' ], 99 );

		add_shortcode( self::SHORTCODE, [ $this, 'renderShortcode' ] );
		if ( ! shortcode_exists( self::LEGACY_SHORTCODE ) ) {
			add_shortcode( self::LEGACY_SHORTCODE, [ $this, 'renderShortcode' ] );
		}

		if ( ! empty( $this->getSettings()['auto_insert'] ) ) {
			$p = (int) apply_filters( 'wp_powersuite_post_views_the_content_priority', self::CONTENT_FILTER_PRIORITY );
			$p = max( 1, min( 9999, $p ) );
			add_filter( 'the_content', [ $this, 'filterTheContent' ], $p );
		}
	}

	public static function getDefaultSettings(): array {
		return [
			'enabled_post_types'     => [ 'post' ],
			'count_interval_hours'   => 24,
			'exclude_bots'           => true,
			'exclude_empty_ua'       => false,
			'exclude_own_posts'      => true,
			'exclude_password_posts' => true,
			'auto_insert'            => false,
			'position'               => 'after_content',
			'label_template'         => self::DEFAULT_LABEL_TEMPLATE,
			'skip_if_shortcode'      => true,
		];
	}

	/**
	 * @param array<string, mixed> $raw
	 * @return array<string, mixed>
	 */
	public static function normalizeSettings( array $raw ): array {
		$settings = wp_parse_args( $raw, self::getDefaultSettings() );

		$settings['exclude_bots']           = ! empty( $settings['exclude_bots'] );
		$settings['exclude_empty_ua']       = ! empty( $settings['exclude_empty_ua'] );
		$settings['exclude_own_posts']      = ! empty( $settings['exclude_own_posts'] );
		$settings['exclude_password_posts'] = ! empty( $settings['exclude_password_posts'] );
		$settings['auto_insert']            = ! empty( $settings['auto_insert'] );
		$settings['skip_if_shortcode']      = ! empty( $settings['skip_if_shortcode'] );
		$settings['count_interval_hours']   = max( 1, min( 168, (int) ( $settings['count_interval_hours'] ?? 24 ) ) );

		$pos          = isset( $settings['position'] ) ? (string) $settings['position'] : 'after_content';
		$allowed_pos  = [ 'before_content', 'after_content', 'after_first_paragraph', 'before_first_paragraph' ];
		$settings['position'] = in_array( $pos, $allowed_pos, true ) ? $pos : 'after_content';

		$label = isset( $settings['label_template'] ) ? sanitize_text_field( (string) $settings['label_template'] ) : '';
		$label = function_exists( 'mb_substr' ) ? mb_substr( $label, 0, 300 ) : substr( $label, 0, 300 );
		$settings['label_template'] = $label !== '' ? $label : self::DEFAULT_LABEL_TEMPLATE;

		$settings['enabled_post_types'] = self::sanitizePostTypes( $settings['enabled_post_types'] ?? [] );

		return $settings;
	}

	/**
	 * @param mixed $types
	 * @return list<string>
	 */
	private static function sanitizePostTypes( $types ): array {
		if ( ! is_array( $types ) ) {
			return [ 'post' ];
		}

		$out = [];
		foreach ( $types as $type ) {
			$key = sanitize_key( (string) $type );
			if ( $key !== '' && post_type_exists( $key ) && ! in_array( $key, $out, true ) ) {
				$out[] = $key;
			}
		}

		return $out === [] ? [ 'post' ] : $out;
	}

	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}

		$raw                  = $this->moduleSettings()->all( [] );
		$this->settings_cache = self::normalizeSettings( is_array( $raw ) ? $raw : [] );

		return $this->settings_cache;
	}

	/** @return list<string> */
	private function getTrackedPostTypes(): array {
		return self::sanitizePostTypes( $this->getSettings()['enabled_post_types'] ?? [ 'post' ] );
	}

	private function registerAdminListColumns(): void {
		foreach ( $this->getTrackedPostTypes() as $post_type ) {
			if ( $post_type === 'post' ) {
				add_filter( 'manage_posts_columns', [ $this, 'injectViewsColumn' ] );
				add_action( 'manage_posts_custom_column', [ $this, 'renderViewsColumn' ], 10, 2 );
				add_filter( 'manage_edit-post_sortable_columns', [ $this, 'sortableViewsColumn' ] );
			} elseif ( $post_type === 'page' ) {
				add_filter( 'manage_pages_columns', [ $this, 'injectViewsColumn' ] );
				add_action( 'manage_pages_custom_column', [ $this, 'renderViewsColumn' ], 10, 2 );
				add_filter( 'manage_edit-page_sortable_columns', [ $this, 'sortableViewsColumn' ] );
			} else {
				add_filter( "manage_{$post_type}_posts_columns", [ $this, 'injectViewsColumn' ] );
				add_action( "manage_{$post_type}_posts_custom_column", [ $this, 'renderViewsColumn' ], 10, 2 );
				add_filter( "manage_edit-{$post_type}_sortable_columns", [ $this, 'sortableViewsColumn' ] );
			}
		}

		add_action( 'pre_get_posts', [ $this, 'maybeSortListByViews' ] );
	}

	public function injectViewsColumn( array $columns ): array {
		if ( isset( $columns[ self::LIST_COLUMN ] ) ) {
			return $columns;
		}
		$new    = [];
		$label  = __( 'Views', 'wp-powersuite' );
		$insert = false;
		foreach ( $columns as $key => $title ) {
			$new[ $key ] = $title;
			if ( $key === 'title' ) {
				$new[ self::LIST_COLUMN ] = $label;
				$insert                   = true;
			}
		}
		if ( ! $insert ) {
			$new[ self::LIST_COLUMN ] = $label;
		}
		return $new;
	}

	public function renderViewsColumn( string $column, int $post_id ): void {
		if ( $column !== self::LIST_COLUMN || $post_id <= 0 ) {
			return;
		}
		echo esc_html( number_format_i18n( self::getViewCount( $post_id ) ) );
	}

	public function sortableViewsColumn( array $columns ): array {
		$columns[ self::LIST_COLUMN ] = self::LIST_COLUMN;
		return $columns;
	}

	public function maybeSortListByViews( WP_Query $query ): void {
		if ( ! is_admin() || ! $query->is_main_query() ) {
			return;
		}
		if ( $query->get( 'orderby' ) !== self::LIST_COLUMN ) {
			return;
		}
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( ! $screen || $screen->base !== 'edit' ) {
			return;
		}
		$pt = (string) $screen->post_type;
		if ( ! in_array( $pt, $this->getTrackedPostTypes(), true ) ) {
			return;
		}
		$query->set( 'meta_key', self::META_KEY );
		$query->set( 'orderby', 'meta_value_num' );
	}

	public static function getViewCount( int $post_id ): int {
		if ( $post_id <= 0 ) {
			return 0;
		}
		return max( 0, (int) get_post_meta( $post_id, self::META_KEY, true ) );
	}

	public function maybeRecordView(): void {
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return;
		}
		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
			return;
		}
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return;
		}
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			return;
		}
		if ( wp_is_json_request() ) {
			return;
		}
		if ( ! is_singular() ) {
			return;
		}
		if ( is_preview() || is_customize_preview() ) {
			return;
		}
		if ( is_feed() || is_trackback() ) {
			return;
		}

		$post = get_queried_object();
		if ( ! $post instanceof WP_Post ) {
			return;
		}
		if ( $post->post_status !== 'publish' ) {
			return;
		}

		$settings = $this->getSettings();
		if ( ! in_array( $post->post_type, $this->getTrackedPostTypes(), true ) ) {
			return;
		}

		if ( ! empty( $settings['exclude_password_posts'] ) && post_password_required( $post ) ) {
			return;
		}

		if ( ! empty( $settings['exclude_own_posts'] ) && is_user_logged_in() && current_user_can( 'edit_post', $post->ID ) ) {
			return;
		}

		if ( ! empty( $settings['exclude_bots'] ) && $this->isLikelyBot( ! empty( $settings['exclude_empty_ua'] ) ) ) {
			return;
		}

		if ( $this->isPrefetchRequest() ) {
			return;
		}

		if ( ! (bool) apply_filters( 'wp_powersuite_post_views_should_count', true, $post, $settings ) ) {
			return;
		}

		$hours = max( 1, min( 168, (int) ( $settings['count_interval_hours'] ?? 24 ) ) );
		$key   = $this->getClientKey();
		$tkey  = 'wpps_pvc_' . md5( (string) (int) $post->ID . '|' . $key );

		if ( get_transient( $tkey ) ) {
			return;
		}

		$this->incrementViews( (int) $post->ID );
		set_transient( $tkey, 1, $hours * HOUR_IN_SECONDS );
	}

	private function incrementViews( int $post_id ): void {
		global $wpdb;

		$updated = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->postmeta} SET meta_value = CAST(meta_value AS UNSIGNED) + 1 WHERE post_id = %d AND meta_key = %s",
				$post_id,
				self::META_KEY
			)
		);

		if ( ! $updated ) {
			add_post_meta( $post_id, self::META_KEY, 1, true );
		}
	}

	private function getClientKey(): string {
		if ( is_user_logged_in() ) {
			return 'u' . get_current_user_id();
		}

		$cid = isset( $_COOKIE[ self::COOKIE_CID ] ) ? sanitize_text_field( wp_unslash( (string) $_COOKIE[ self::COOKIE_CID ] ) ) : '';
		if ( $cid === '' || strlen( $cid ) > 64 || ! preg_match( '/^[a-zA-Z0-9_-]+$/', $cid ) ) {
			$cid = wp_generate_password( 24, false, false );
			if ( ! headers_sent() ) {
				$path   = ( defined( 'COOKIEPATH' ) && COOKIEPATH ) ? COOKIEPATH : '/';
				$domain = defined( 'COOKIE_DOMAIN' ) ? COOKIE_DOMAIN : '';
				setcookie(
					self::COOKIE_CID,
					$cid,
					[
						'expires'  => time() + YEAR_IN_SECONDS,
						'path'     => $path,
						'domain'   => $domain,
						'secure'   => is_ssl(),
						'httponly' => true,
						'samesite' => 'Lax',
					]
				);
			}
			$_COOKIE[ self::COOKIE_CID ] = $cid;
		}

		return 'c' . $cid;
	}

	private function isLikelyBot( bool $treat_empty_ua_as_bot ): bool {
		$ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
		$ua = trim( $ua );
		if ( $ua === '' ) {
			return $treat_empty_ua_as_bot;
		}

		if ( (int) apply_filters( 'wp_powersuite_post_views_bot_score', 0, $ua ) >= 100 ) {
			return true;
		}

		return (bool) preg_match(
			'/bot|crawl|spider|slurp|bingpreview|facebookexternalhit|embedly|quora|whatsapp|telegram|discord|linkedin|pinterest|lighthouse|gtmetrix|pingdom|uptimerobot|semrush|ahrefs|petalbot|bytespider|yandex|duckduckbot|baiduspider|applebot|ia_archiver/i',
			$ua
		);
	}

	private function isPrefetchRequest(): bool {
		$sec = isset( $_SERVER['HTTP_SEC_PURPOSE'] ) ? strtolower( (string) $_SERVER['HTTP_SEC_PURPOSE'] ) : '';
		if ( str_contains( $sec, 'prefetch' ) ) {
			return true;
		}
		$purpose = isset( $_SERVER['HTTP_PURPOSE'] ) ? strtolower( (string) $_SERVER['HTTP_PURPOSE'] ) : '';
		return $purpose === 'prefetch';
	}

	public function renderShortcode( array $atts = [], ?string $content = null, string $tag = '' ): string {
		unset( $content );

		$shortcode = ( $tag === self::LEGACY_SHORTCODE || $tag === self::SHORTCODE ) ? $tag : self::SHORTCODE;
		$atts      = shortcode_atts(
			[ 'id' => '0' ],
			$atts,
			$shortcode
		);

		$post = $this->resolvePostForDisplay( (int) $atts['id'] );
		if ( ! $post instanceof WP_Post ) {
			return '';
		}

		$settings = $this->getSettings();
		if ( ! in_array( $post->post_type, $this->getTrackedPostTypes(), true ) ) {
			return '';
		}

		return $this->buildViewsMarkup( $post, $settings, self::getViewCount( (int) $post->ID ), 'span' );
	}

	public function filterTheContent( string $content ): string {
		if ( ! is_singular() || $content === '' ) {
			return $content;
		}

		if ( in_the_loop() && ! is_main_query() ) {
			return $content;
		}

		$post = get_post();
		if ( ! $post instanceof WP_Post ) {
			return $content;
		}

		if ( self::$display_inserted_for_post === (int) $post->ID ) {
			return $content;
		}

		$settings = $this->getSettings();
		if ( ! in_array( $post->post_type, $this->getTrackedPostTypes(), true ) ) {
			return $content;
		}

		if ( ! empty( $settings['skip_if_shortcode'] ) && $this->postHasViewsShortcode( $post ) ) {
			return $content;
		}

		$block = $this->buildViewsMarkup(
			$post,
			$settings,
			self::getViewCount( (int) $post->ID ),
			'p'
		);

		self::$display_inserted_for_post = (int) $post->ID;

		return $this->insertBlock( $content, $block, (string) ( $settings['position'] ?? 'after_content' ) );
	}

	private function resolvePostForDisplay( int $post_id ): ?WP_Post {
		if ( $post_id <= 0 && is_singular() ) {
			$post_id = (int) get_queried_object_id();
		}
		if ( $post_id <= 0 ) {
			return null;
		}

		$post = get_post( $post_id );
		if ( ! $post instanceof WP_Post || $post->post_status !== 'publish' ) {
			return null;
		}

		return $post;
	}

	private function postHasViewsShortcode( WP_Post $post ): bool {
		$raw = (string) ( $post->post_content ?? '' );
		if ( $raw === '' ) {
			return false;
		}

		return str_contains( $raw, '[' . self::SHORTCODE ) || str_contains( $raw, '[' . self::LEGACY_SHORTCODE );
	}

	private function buildViewsMarkup( WP_Post $post, array $settings, int $views, string $default_wrapper ): string {
		$line = $this->interpolateTemplate(
			$this->resolveLabelTemplate( (string) ( $settings['label_template'] ?? '' ) ),
			$views
		);
		$line = (string) apply_filters( 'wp_powersuite_post_views_line', $line, $post, $settings );

		$wrapper = (string) apply_filters( 'wp_powersuite_post_views_wrapper_tag', $default_wrapper, $post, $settings );
		$wrapper = in_array( $wrapper, [ 'p', 'div', 'span' ], true ) ? $wrapper : $default_wrapper;
		$class   = sanitize_html_class( (string) apply_filters( 'wp_powersuite_post_views_class', 'wpps-post-views', $post, $settings ) );

		return sprintf(
			'<%1$s class="%2$s" role="status">%3$s</%1$s>',
			$wrapper,
			esc_attr( $class ),
			wp_kses_post( $line )
		);
	}

	private function resolveLabelTemplate( string $stored ): string {
		$trimmed = trim( $stored );
		if ( $trimmed === '' || $trimmed === self::DEFAULT_LABEL_TEMPLATE ) {
			return __( '%views% views', 'wp-powersuite' );
		}
		return $stored;
	}

	private function interpolateTemplate( string $template, int $views ): string {
		$template = function_exists( 'mb_substr' ) ? mb_substr( $template, 0, 300 ) : substr( $template, 0, 300 );
		$parts    = preg_split( '/(%views%)/', $template, -1, PREG_SPLIT_DELIM_CAPTURE );
		if ( ! is_array( $parts ) ) {
			return esc_html( $template );
		}
		$safe = '';
		foreach ( $parts as $part ) {
			if ( $part === '%views%' ) {
				$safe .= esc_html( (string) (int) $views );
			} else {
				$safe .= esc_html( $part );
			}
		}
		return $safe;
	}

	private function insertBlock( string $content, string $block, string $position ): string {
		$sep = "\n\n";
		switch ( $position ) {
			case 'before_content':
				return $block . $sep . $content;
			case 'before_first_paragraph':
				$pos = $this->findFirstParagraphStart( $content );
				if ( $pos !== null ) {
					return substr( $content, 0, $pos ) . $block . $sep . substr( $content, $pos );
				}
				return $block . $sep . $content;
			case 'after_first_paragraph':
				if ( preg_match( '#</p>#i', $content, $m, PREG_OFFSET_CAPTURE ) ) {
					$at = $m[0][1] + strlen( $m[0][0] );
					return substr( $content, 0, $at ) . $sep . $block . $sep . substr( $content, $at );
				}
				return $block . $sep . $content;
			case 'after_content':
			default:
				return $content . $sep . $block;
		}
	}

	private function findFirstParagraphStart( string $content ): ?int {
		if ( preg_match( '#<p(\s[^>]*)?>#i', $content, $m, PREG_OFFSET_CAPTURE ) ) {
			return (int) $m[0][1];
		}
		return null;
	}
}
