<?php
/**
 * Developer reference only (not shown in the settings modal):
 * Default wrapper class `wpps-reading-time`. Filters: `wp_powersuite_reading_time_the_content_priority` (default 12),
 * `wp_powersuite_reading_time_class`, `wp_powersuite_reading_time_wrapper_tag`, `wp_powersuite_reading_time_line`,
 * `wp_powersuite_reading_time_source_text`.
 *
 * @package WP_PowerSuite\Modules\ReadingTimeCalculator
 */
namespace ReadingTimeCalculator;

use WP_PowerSuite\Core\AbstractModule;
use WP_Post;

class Module extends AbstractModule {

	private const MODULE_ID               = 'reading-time-calculator';
	private const MODULE_VERSION          = '1.0.1';
	private const SHORTCODE               = 'wpps_reading_time';
	private const DEFAULT_LABEL_TEMPLATE  = '%minutes% min read';
	private const CONTENT_FILTER_PRIORITY = 12;

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

	/** @var array<string, array{words:int, minutes:int}> */
	private static array $stats_cache = [];

	private static ?int $auto_inserted_for_post = null;

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

	public function getName(): string {
		return __( 'Reading Time Calculator', 'wp-powersuite' );
	}

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

	public function getDescription(): string {
		return __( 'Reading time labels from word count—auto or shortcode, with flexible formatting.', 'wp-powersuite' );
	}

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

	protected function registerHooks(): void {
		if ( is_admin() ) {
			return;
		}

		add_shortcode( self::SHORTCODE, [ $this, 'renderShortcode' ] );

		if ( ! empty( $this->getSettings()['auto_insert'] ) ) {
			$p = (int) apply_filters( 'wp_powersuite_reading_time_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 [
			'auto_insert'        => true,
			'enabled_post_types' => [ 'post' ],
			'position'           => 'before_content',
			'words_per_minute'   => 200,
			'minimum_words'      => 0,
			'label_template'     => self::DEFAULT_LABEL_TEMPLATE,
			'show_word_count'    => false,
			'count_excerpt'      => false,
			'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['auto_insert']        = ! empty( $settings['auto_insert'] );
		$settings['show_word_count']    = ! empty( $settings['show_word_count'] );
		$settings['count_excerpt']      = ! empty( $settings['count_excerpt'] );
		$settings['skip_if_shortcode']  = ! empty( $settings['skip_if_shortcode'] );
		$settings['words_per_minute']   = max( 60, min( 600, (int) ( $settings['words_per_minute'] ?? 200 ) ) );
		$settings['minimum_words']      = max( 0, min( 50000, (int) ( $settings['minimum_words'] ?? 0 ) ) );

		$pos         = isset( $settings['position'] ) ? (string) $settings['position'] : 'before_content';
		$allowed_pos = [ 'before_content', 'after_content', 'after_first_paragraph', 'before_first_paragraph' ];
		$settings['position'] = in_array( $pos, $allowed_pos, true ) ? $pos : 'before_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' ] );
	}

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

		$atts = shortcode_atts(
			[
				'post_id' => '0',
				'id'      => '0',
				'format'  => 'full',
				'wpm'     => '',
			],
			$atts,
			self::SHORTCODE
		);

		$format = strtolower( (string) $atts['format'] );
		if ( ! in_array( $format, [ 'full', 'short', 'number' ], true ) ) {
			$format = 'full';
		}

		$wpm_override = null;
		if ( $atts['wpm'] !== '' && $atts['wpm'] !== null ) {
			$wpm_override = max( 60, min( 600, (int) $atts['wpm'] ) );
		}

		$post_id = (int) $atts['post_id'];
		if ( $post_id <= 0 ) {
			$post_id = (int) $atts['id'];
		}

		$post = $this->resolvePostForDisplay( $post_id );
		if ( ! $post instanceof WP_Post ) {
			return '';
		}

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

		$stats = $this->getStatsForPost( $post, $settings, $wpm_override );
		if ( $stats === null ) {
			return '';
		}

		if ( $format === 'number' ) {
			return esc_html( (string) (int) $stats['minutes'] );
		}

		if ( $format === 'short' ) {
			$m     = (int) $stats['minutes'];
			$label = $m === 1 ? __( 'min', 'wp-powersuite' ) : __( 'mins', 'wp-powersuite' );
			return esc_html( (string) $m . ' ' . $label );
		}

		return $this->buildMarkupFromStats( $post, $settings, $stats, '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 || self::$auto_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->postHasReadingTimeShortcode( $post ) ) {
			return $content;
		}

		$stats = $this->getStatsForPost( $post, $settings, null );
		if ( $stats === null ) {
			return $content;
		}

		$html = $this->buildMarkupFromStats( $post, $settings, $stats, 'p' );
		if ( $html === '' ) {
			return $content;
		}

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

		return $this->insertBlock( $content, $html, (string) ( $settings['position'] ?? 'before_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 postHasReadingTimeShortcode( WP_Post $post ): bool {
		$raw = (string) ( $post->post_content ?? '' );
		return $raw !== '' && str_contains( $raw, '[' . self::SHORTCODE );
	}

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

	private function buildMarkupFromStats( WP_Post $post, array $settings, array $stats, string $default_wrapper ): string {
		$raw      = isset( $settings['label_template'] ) ? (string) $settings['label_template'] : '';
		$template = $this->resolveLabelTemplate( $raw );
		$line     = $this->interpolateTemplate( $template, $stats['minutes'], $stats['words'], ! empty( $settings['show_word_count'] ) );
		$line     = (string) apply_filters( 'wp_powersuite_reading_time_line', $line, $post, $settings );

		$tag = (string) apply_filters( 'wp_powersuite_reading_time_wrapper_tag', $default_wrapper, $post, $settings );
		$tag = in_array( $tag, [ 'p', 'div', 'span' ], true ) ? $tag : $default_wrapper;

		$class = sanitize_html_class( (string) apply_filters( 'wp_powersuite_reading_time_class', 'wpps-reading-time', $post, $settings ) );

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

	private function getStatsForPost( WP_Post $post, array $settings, ?int $wpm_override ): ?array {
		$wpm = $wpm_override ?? max( 60, min( 600, (int) ( $settings['words_per_minute'] ?? 200 ) ) );
		$ex  = ! empty( $settings['count_excerpt'] );
		$key = (int) $post->ID . '|' . $wpm . '|' . ( $ex ? '1' : '0' );

		if ( ! isset( self::$stats_cache[ $key ] ) ) {
			$body  = $this->getBodyTextForCount( $post, $settings );
			$words = $this->countWords( $body );
			$mins  = $words > 0 ? max( 1, (int) ceil( $words / $wpm ) ) : 0;
			self::$stats_cache[ $key ] = [ 'words' => $words, 'minutes' => $mins ];
		}

		$cached = self::$stats_cache[ $key ];
		$min_w  = max( 0, min( 50000, (int) ( $settings['minimum_words'] ?? 0 ) ) );

		if ( ( $min_w > 0 && $cached['words'] < $min_w ) || $cached['words'] <= 0 || $cached['minutes'] <= 0 ) {
			return null;
		}

		return $cached;
	}

	private function getBodyTextForCount( WP_Post $post, array $settings ): string {
		$body = (string) ( $post->post_content ?? '' );

		if ( $this->shouldUseElementorContent( $post, $body ) ) {
			$elementor_text = $this->extractElementorText( (int) $post->ID );
			if ( $elementor_text !== '' ) {
				$body = $elementor_text;
			}
		}

		if ( ! empty( $settings['count_excerpt'] ) && ! empty( $post->post_excerpt ) ) {
			$body .= "\n\n" . (string) $post->post_excerpt;
		}

		return $body;
	}

	private function shouldUseElementorContent( WP_Post $post, string $body ): bool {
		if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
			return false;
		}
		if ( get_post_meta( $post->ID, '_elementor_edit_mode', true ) !== 'builder' ) {
			return false;
		}

		$stripped = trim( wp_strip_all_tags( $body ) );

		return $stripped === '' || str_contains( $body, '[elementor-template' );
	}

	private function extractElementorText( int $post_id ): string {
		$raw = get_post_meta( $post_id, '_elementor_data', true );
		if ( ! is_string( $raw ) || $raw === '' ) {
			return '';
		}

		$data = json_decode( $raw, true );
		if ( ! is_array( $data ) ) {
			return '';
		}

		return trim( $this->collectElementorNodeText( $data ) );
	}

	/**
	 * @param array<int|string, mixed> $nodes
	 */
	private function collectElementorNodeText( array $nodes ): string {
		$parts = [];

		foreach ( $nodes as $node ) {
			if ( ! is_array( $node ) ) {
				continue;
			}

			if ( isset( $node['settings'] ) && is_array( $node['settings'] ) ) {
				foreach ( $node['settings'] as $key => $value ) {
					if ( ! is_string( $value ) || $value === '' ) {
						continue;
					}
					if ( str_starts_with( $value, 'http' ) || str_starts_with( $value, '#' ) ) {
						continue;
					}
					if ( in_array( $key, [ 'link', 'url', 'css_classes', '_css_classes', 'custom_css', 'selected_icon', 'background_image', 'image', 'poster' ], true ) ) {
						continue;
					}
					if ( preg_match( '/(title|text|editor|html|description|caption|content|heading|sub|label|message|quote|bio|name)/i', (string) $key ) ) {
						$parts[] = wp_strip_all_tags( $value );
					}
				}
			}

			if ( ! empty( $node['elements'] ) && is_array( $node['elements'] ) ) {
				$child = $this->collectElementorNodeText( $node['elements'] );
				if ( $child !== '' ) {
					$parts[] = $child;
				}
			}
		}

		return implode( "\n\n", array_filter( $parts ) );
	}

	private function countWords( string $text ): int {
		$text = (string) apply_filters( 'wp_powersuite_reading_time_source_text', $text );
		if ( $text === '' ) {
			return 0;
		}
		if ( function_exists( 'wp_count_words' ) ) {
			return (int) wp_count_words( $text );
		}
		$text = strip_shortcodes( $text );
		$text = preg_replace( '/<!--\s*wp:[\s\S]*?-->/u', ' ', $text ) ?? $text;
		$text = wp_strip_all_tags( $text );
		$text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text ) ?? $text;
		$text = trim( preg_replace( '/\s+/u', ' ', $text ) ?? '' );
		if ( $text === '' ) {
			return 0;
		}
		return count( preg_split( '/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY ) );
	}

	private function interpolateTemplate( string $template, int $minutes, int $words, bool $show_words ): string {
		$template = function_exists( 'mb_substr' ) ? mb_substr( $template, 0, 300 ) : substr( $template, 0, 300 );
		if ( ! $show_words ) {
			$template = str_replace( '%words%', '', $template );
		}
		$parts = preg_split( '/(%minutes%|%words%)/', $template, -1, PREG_SPLIT_DELIM_CAPTURE );
		if ( ! is_array( $parts ) ) {
			return esc_html( $template );
		}
		$out = '';
		foreach ( $parts as $part ) {
			if ( $part === '%minutes%' ) {
				$out .= esc_html( (string) (int) $minutes );
			} elseif ( $part === '%words%' ) {
				$out .= esc_html( (string) (int) $words );
			} else {
				$out .= esc_html( $part );
			}
		}
		return $out;
	}

	private function insertBlock( string $content, string $block, string $position ): string {
		$sep = "\n\n";
		switch ( $position ) {
			case 'after_content':
				return $content . $sep . $block;
			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 '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 'before_content':
			default:
				return $block . $sep . $content;
		}
	}

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