<?php

namespace WPDRMS\ASP\Index;

if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

/**
 * Evaluates the date / author / manual-post-ID index exclusions.
 *
 * These three exclusion types test columns that live directly on `wp_posts`
 * (`post_date`/`post_modified`, `post_author`, `ID`), so they are applied two ways:
 *
 *  - {@see self::sqlWhere()} produces WHERE fragments appended to the bulk index
 *    source query, so excluded posts are never fetched (and never written to the
 *    skip table) — important when a date cutoff excludes thousands of old posts.
 *  - {@see self::excludesPost()} is the per-post equivalent used by the incremental
 *    `save_post` path (a single post re-indexed on edit).
 *
 * Taxonomy and custom-field exclusions remain in {@see IndexProcessor} (they need
 * per-post term/meta lookups). Both methods read the flat args produced by
 * {@see ORM\IndexTableOptions::toManagerArgs()}.
 */
class IndexExclusionEvaluator {

	/**
	 * Builds the SQL WHERE fragment (including the leading " AND ") for the bulk
	 * source query, or '' when nothing is configured.
	 *
	 * @param array<string, mixed> $args  Flattened index options.
	 * @param string               $alias `wp_posts` alias used in the query.
	 */
	public static function sqlWhere( array $args, string $alias = 'post' ): string {
		$clauses = array();

		$authors = self::intList( $args['author_exclusion'] ?? array() );
		if ( ! empty( $authors ) ) {
			$clauses[] = "$alias.post_author NOT IN (" . implode( ',', $authors ) . ')';
		}

		$post_ids = self::intList( $args['post_exclusion'] ?? array() );
		if ( ! empty( $post_ids ) ) {
			$clauses[] = "$alias.ID NOT IN (" . implode( ',', $post_ids ) . ')';
		}

		$date_clause = self::dateSqlClause( $args, $alias );
		if ( $date_clause !== '' ) {
			$clauses[] = $date_clause;
		}

		if ( empty( $clauses ) ) {
			return '';
		}

		return ' AND ' . implode( ' AND ', $clauses );
	}

	/**
	 * Per-post check for the incremental path. Returns true if the post should be
	 * excluded from the index by a date / author / manual-post-ID rule.
	 *
	 * @param object               $post A post object (needs ID, post_author, post_type, post_date/post_modified).
	 * @param array<string, mixed> $args Flattened index options.
	 */
	public static function excludesPost( object $post, array $args ): bool {
		$authors = self::intList( $args['author_exclusion'] ?? array() );
		if ( ! empty( $authors ) && in_array( (int) ( $post->post_author ?? 0 ), $authors, true ) ) {
			return true;
		}

		$post_ids = self::intList( $args['post_exclusion'] ?? array() );
		if ( ! empty( $post_ids ) && in_array( (int) ( $post->ID ?? 0 ), $post_ids, true ) ) {
			return true;
		}

		$field  = self::field( $args );
		$cutoff = self::cutoffForType( $args, (string) ( $post->post_type ?? '' ) );
		if ( $cutoff !== '' ) {
			$post_date = (string) ( $post->$field ?? '' );
			if ( $post_date !== '' && strtotime( $post_date ) < strtotime( $cutoff . ' 00:00:00' ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * The applicable date cutoff for a given post type: a per-type override if set,
	 * otherwise the global cutoff ('' when neither is configured).
	 *
	 * @param array<string, mixed> $args
	 * @param string               $post_type
	 */
	public static function cutoffForType( array $args, string $post_type ): string {
		$overrides = self::overrideMap( $args );
		if ( $post_type !== '' && isset( $overrides[ $post_type ] ) ) {
			return $overrides[ $post_type ];
		}
		return self::validateDate( (string) ( $args['date_exclusion_global'] ?? '' ) );
	}

	/**
	 * Builds the "exclude posts older than the applicable cutoff" SQL clause.
	 *
	 * @param array<string, mixed> $args
	 * @param string               $alias
	 */
	protected static function dateSqlClause( array $args, string $alias ): string {
		$field     = self::field( $args );
		$global    = self::validateDate( (string) ( $args['date_exclusion_global'] ?? '' ) );
		$overrides = self::overrideMap( $args );

		if ( $global === '' && empty( $overrides ) ) {
			return '';
		}

		// Each predicate matches posts that ARE older than their applicable cutoff;
		// wrapping the OR-set in NOT(...) keeps everything that is not too old (and
		// any post type with no applicable cutoff).
		$older          = array();
		$override_types = array();
		foreach ( $overrides as $ptype => $date ) {
			$override_types[] = $ptype;
			$older[]          = "($alias.post_type = '" . esc_sql( $ptype ) . "' AND $alias.$field < '" . esc_sql( $date ) . " 00:00:00')";
		}
		if ( $global !== '' ) {
			if ( ! empty( $override_types ) ) {
				$not_in  = "'" . implode( "','", array_map( 'esc_sql', $override_types ) ) . "'";
				$older[] = "($alias.post_type NOT IN ($not_in) AND $alias.$field < '" . esc_sql( $global ) . " 00:00:00')";
			} else {
				$older[] = "($alias.$field < '" . esc_sql( $global ) . " 00:00:00')";
			}
		}

		return 'NOT (' . implode( ' OR ', $older ) . ')';
	}

	/**
	 * Validated date column to test against.
	 *
	 * @param array<string, mixed> $args
	 */
	protected static function field( array $args ): string {
		return ( ( $args['date_exclusion_field'] ?? 'post_date' ) === 'post_modified' ) ? 'post_modified' : 'post_date';
	}

	/**
	 * Parses the "post_type|YYYY-MM-DD" override entries into a [post_type => date] map.
	 *
	 * @param array<string, mixed> $args
	 * @return array<string, string>
	 */
	protected static function overrideMap( array $args ): array {
		$map = array();
		foreach ( (array) ( $args['date_exclusion_overrides'] ?? array() ) as $entry ) {
			$entry = (string) $entry;
			$pos   = strpos( $entry, '|' );
			if ( $pos === false ) {
				continue;
			}
			$ptype = sanitize_key( substr( $entry, 0, $pos ) );
			$date  = self::validateDate( substr( $entry, $pos + 1 ) );
			if ( $ptype !== '' && $date !== '' ) {
				$map[ $ptype ] = $date;
			}
		}
		return $map;
	}

	/**
	 * Returns the value only if it is a strict YYYY-MM-DD date, otherwise ''.
	 *
	 * @param string $value
	 */
	protected static function validateDate( string $value ): string {
		$value = trim( $value );
		return preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ? $value : '';
	}

	/**
	 * Normalises a value to a unique list of positive ints.
	 *
	 * @param mixed $value
	 * @return int[]
	 */
	protected static function intList( $value ): array {
		$out = array();
		foreach ( (array) $value as $v ) {
			$i = (int) $v;
			if ( $i > 0 ) {
				$out[] = $i;
			}
		}
		return array_values( array_unique( $out ) );
	}
}
