<?php

namespace Limb_Chatbot\Includes\Services\Knowledge\Source_Validators;

use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Interfaces\Source_Validator_Interface;

class Posts_Validator implements Source_Validator_Interface {
	/**
	 * Supported meta operators for filter-based retrieval.
	 *
	 * @var array<string, string>
	 * @since 1.0.0
	 */
	private const META_FILTER_OPERATORS = [
		'equal'         => '=',
		'not_equal'     => '!=',
		'contains'      => 'LIKE',
		'greather_than' => '>',
		'greater_than'  => '>',
		'less_then'     => '<',
		'less_than'     => '<',
	];

	/**
	 * Validates the parameters for post-type-based datasets.
	 *
	 * Ensures the selected post type exists in WordPress.
	 *
	 * @param  array  $config  Configuration array containing post type.
	 *
	 * @throws Exception If the post type does not exist.
	 *
	 * @since 1.0.0
	 */
	public function validate( array $config ): void {
		$post_type = sanitize_key( (string) ( $config['post_type'] ?? '' ) );

		if ( empty( get_post_type_object( $post_type ) ) ) {
			throw new Exception( Error_Codes::VALIDATION_INVALID_VALUE,
				sprintf( __( 'Invalid Post type %s', 'limb-chatbot' ), $post_type ) );
		}

		$has_limit   = array_key_exists( 'limit', $config ) && null !== $config['limit'] && '' !== $config['limit'];
		$has_filters = ! empty( $config['filters'] ) && is_array( $config['filters'] );

		if ( ! $has_limit && ! $has_filters ) {
			throw new Exception(
				Error_Codes::MISSING_VALUE,
				__( 'Either limit or filters must be provided for post source.', 'limb-chatbot' )
			);
		}

		if ( $has_limit ) {
			$this->validate_limit( $config['limit'] );
		}

		if ( $has_filters ) {
			$this->validate_filters( $config['filters'] );
		}
	}

	/**
	 * Gets the total number of posts to be included in the dataset.
	 *
	 * @param  array  $config  Configuration array containing post type and limit.
	 *
	 * @return int Total number of posts.
	 *
	 * @throws Exception If the limit is missing or total is empty.
	 *
	 * @since 1.0.0
	 */
	public function get_total( array $config ): int {
		$post_type = sanitize_key( (string) $config['post_type'] );
		$has_limit = array_key_exists( 'limit', $config ) && null !== $config['limit'] && '' !== $config['limit'];

		if ( $has_limit ) {
			$limit = $config['limit'];
			if ( $limit === 'all' ) {
				$total = wp_count_posts( $post_type )->publish;
			} elseif ( is_numeric( $limit ) ) {
				$count = wp_count_posts( $post_type )->publish;
				$total = min( absint( $limit ), $count );
			} elseif ( is_array( $limit ) ) {
				$posts = get_posts( [
					'post_type'   => $post_type,
					'numberposts' => - 1,
					'fields'      => 'ids',
					'post__in'    => array_values( array_filter( array_map( 'absint', $limit ) ) ),
				] );
				$count = ! empty( $posts ) ? count( $posts ) : 0;
				$total = $count;
			} else {
				throw new Exception( Error_Codes::MISSING_VALUE, __( 'Limit is invalid.', 'limb-chatbot' ) );
			}
		} else {
			$filters = isset( $config['filters'] ) && is_array( $config['filters'] ) ? $config['filters'] : [];
			$query   = new \WP_Query( $this->build_filtered_query_args( $post_type, $filters, [
				'fields'         => 'ids',
				'posts_per_page' => 1,
				'paged'          => 1,
				'no_found_rows'  => false,
			] ) );
			$total   = (int) $query->found_posts;
		}

		if ( empty( $total ) ) {
			throw new Exception( Error_Codes::EMPTY_VALUE, __( 'Total is empty', 'limb-chatbot' ) );
		}

		return $total;
	}

	/**
	 * Validate limit payload shape.
	 *
	 * @param  mixed  $limit
	 *
	 * @return void
	 * @throws Exception
	 * @since 1.0.0
	 */
	private function validate_limit( $limit ): void {
		if ( 'all' === $limit ) {
			return;
		}

		if ( is_numeric( $limit ) ) {
			return;
		}

		if ( is_array( $limit ) ) {
			foreach ( $limit as $post_id ) {
				if ( ! is_numeric( $post_id ) ) {
					throw new Exception(
						Error_Codes::VALIDATION_INVALID_VALUE,
						__( 'Limit array must contain only post IDs.', 'limb-chatbot' )
					);
				}
			}

			return;
		}

		throw new Exception(
			Error_Codes::VALIDATION_INVALID_VALUE,
			__( 'Limit must be "all", a number, or array of post IDs.', 'limb-chatbot' )
		);
	}

	/**
	 * Validate filters payload shape.
	 *
	 * @param  array  $filters
	 *
	 * @return void
	 * @throws Exception
	 * @since 1.0.0
	 */
	private function validate_filters( array $filters ): void {
		$filters = $this->normalize_filters_payload( $filters );

		if ( isset( $filters['taxonomies'] ) && ! is_array( $filters['taxonomies'] ) ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				__( 'filters.taxonomies must be an object.', 'limb-chatbot' )
			);
		}

		if ( isset( $filters['meta_fields'] ) && ! is_array( $filters['meta_fields'] ) ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				__( 'filters.meta_fields must be an object.', 'limb-chatbot' )
			);
		}

		if ( isset( $filters['author'] ) && ! is_array( $filters['author'] ) ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				__( 'filters.author must be an array of author IDs.', 'limb-chatbot' )
			);
		}

		if ( ! empty( $filters['meta_fields'] ) ) {
			foreach ( $filters['meta_fields'] as $meta_key => $rule ) {
				if ( ! is_array( $rule ) || ! array_key_exists( 'value', $rule ) ) {
					throw new Exception(
						Error_Codes::VALIDATION_INVALID_VALUE,
						sprintf( __( 'Invalid meta filter for key %s.', 'limb-chatbot' ), $meta_key )
					);
				}
			}
		}
	}

	/**
	 * Build filtered query args for totals and retrieval.
	 *
	 * @param  string  $post_type
	 * @param  array  $filters
	 * @param  array  $overrides
	 *
	 * @return array
	 * @since 1.0.0
	 */
	private function build_filtered_query_args( string $post_type, array $filters, array $overrides = [] ): array {
		$filters = $this->normalize_filters_payload( $filters );

		$args = [
			'post_type'   => $post_type,
			'post_status' => 'publish',
		];

		$tax_query = $this->build_tax_query( $filters['taxonomies'] ?? [] );
		if ( ! empty( $tax_query ) ) {
			$args['tax_query'] = $tax_query;
		}

		$meta_query = $this->build_meta_query( $filters['meta_fields'] ?? [] );
		if ( ! empty( $meta_query ) ) {
			$args['meta_query'] = $meta_query;
		}

		$author_ids = $this->sanitize_author_ids( $filters['author'] ?? [] );
		if ( ! empty( $author_ids ) ) {
			$args['author__in'] = $author_ids;
		}

		return array_merge( $args, $overrides );
	}

	/**
	 * Normalize filters payload from rule-list shape to grouped query shape.
	 *
	 * @param  array  $filters
	 *
	 * @return array
	 * @since 1.0.0
	 */
	private function normalize_filters_payload( array $filters ): array {
		if ( isset( $filters['taxonomies'] ) || isset( $filters['meta_fields'] ) || isset( $filters['author'] ) ) {
			return $filters;
		}

		$normalized = [
			'taxonomies' => [],
			'meta_fields' => [],
			'author' => [],
		];

		foreach ( $filters as $filter ) {
			if ( ! is_array( $filter ) || empty( $filter['type'] ) ) {
				continue;
			}

			$type = sanitize_key( (string) $filter['type'] );

			if ( 'taxonomy' === $type ) {
				$taxonomy = sanitize_key( (string) ( $filter['taxonomy'] ?? '' ) );
				$terms    = is_array( $filter['terms'] ?? null ) ? $filter['terms'] : [];
				if ( '' !== $taxonomy && ! empty( $terms ) ) {
					$normalized['taxonomies'][ $taxonomy ] = $terms;
				}
				continue;
			}

			if ( 'meta_field' === $type ) {
				$meta_key = sanitize_key( (string) ( $filter['meta_field'] ?? '' ) );
				if ( '' === $meta_key ) {
					continue;
				}
				$normalized['meta_fields'][ $meta_key ] = [
					'operator' => sanitize_key( (string) ( $filter['operator'] ?? 'equal' ) ),
					'value'    => $filter['meta_value'] ?? null,
				];
				continue;
			}

			if ( 'authors' === $type ) {
				$authors = is_array( $filter['authors'] ?? null ) ? $filter['authors'] : [];
				if ( ! empty( $authors ) ) {
					$normalized['author'] = array_values( array_unique( array_merge( $normalized['author'], $authors ) ) );
				}
			}
		}

		return $normalized;
	}

	/**
	 * Build WP tax_query from payload.
	 *
	 * @param  mixed  $raw_taxonomies
	 *
	 * @return array
	 * @since 1.0.0
	 */
	private function build_tax_query( $raw_taxonomies ): array {
		if ( ! is_array( $raw_taxonomies ) || empty( $raw_taxonomies ) ) {
			return [];
		}

		$tax_query = [ 'relation' => 'AND' ];

		foreach ( $raw_taxonomies as $taxonomy_name => $terms ) {
			$taxonomy_name = sanitize_key( (string) $taxonomy_name );

			if ( '' === $taxonomy_name || ! taxonomy_exists( $taxonomy_name ) || ! is_array( $terms ) || empty( $terms ) ) {
				continue;
			}

			$sanitized_terms = array_values( array_filter( array_map( static function ( $term ) {
				if ( is_numeric( $term ) ) {
					return absint( $term );
				}

				if ( is_string( $term ) ) {
					$slug = sanitize_title( $term );

					return '' !== $slug ? $slug : null;
				}

				return null;
			}, $terms ) ) );

			if ( empty( $sanitized_terms ) ) {
				continue;
			}

			$all_are_ids = count( array_filter( $sanitized_terms, 'is_int' ) ) === count( $sanitized_terms );

			$tax_query[] = [
				'taxonomy' => $taxonomy_name,
				'field'    => $all_are_ids ? 'term_id' : 'slug',
				'terms'    => $sanitized_terms,
				'operator' => 'IN',
			];
		}

		return count( $tax_query ) > 1 ? $tax_query : [];
	}

	/**
	 * Build WP meta_query from payload.
	 *
	 * @param  mixed  $raw_meta_fields
	 *
	 * @return array
	 * @since 1.0.0
	 */
	private function build_meta_query( $raw_meta_fields ): array {
		if ( ! is_array( $raw_meta_fields ) || empty( $raw_meta_fields ) ) {
			return [];
		}

		$meta_query = [ 'relation' => 'AND' ];

		foreach ( $raw_meta_fields as $meta_key => $meta_rule ) {
			$meta_key = sanitize_key( (string) $meta_key );
			if ( '' === $meta_key || ! is_array( $meta_rule ) ) {
				continue;
			}

			$operator_key = isset( $meta_rule['operator'] ) ? sanitize_key( (string) $meta_rule['operator'] ) : 'equal';
			$compare      = self::META_FILTER_OPERATORS[ $operator_key ] ?? self::META_FILTER_OPERATORS['equal'];
			$value        = $meta_rule['value'] ?? null;

			if ( null === $value ) {
				continue;
			}

			if ( 'LIKE' === $compare ) {
				$value = '%' . sanitize_text_field( (string) $value ) . '%';
			}

			$meta_query[] = [
				'key'     => $meta_key,
				'value'   => $this->sanitize_meta_value( $value ),
				'compare' => $compare,
			];
		}

		return count( $meta_query ) > 1 ? $meta_query : [];
	}

	/**
	 * Normalize author filters.
	 *
	 * @param  mixed  $raw_authors
	 *
	 * @return int[]
	 * @since 1.0.0
	 */
	private function sanitize_author_ids( $raw_authors ): array {
		if ( ! is_array( $raw_authors ) || empty( $raw_authors ) ) {
			return [];
		}

		$author_ids = array_map( 'absint', $raw_authors );
		$author_ids = array_filter( array_unique( $author_ids ) );

		return array_values( $author_ids );
	}

	/**
	 * Sanitize meta values while preserving numeric comparison.
	 *
	 * @param  mixed  $value
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	private function sanitize_meta_value( $value ) {
		if ( is_numeric( $value ) ) {
			return strpos( (string) $value, '.' ) !== false ? (float) $value : (int) $value;
		}

		if ( is_bool( $value ) ) {
			return $value ? '1' : '0';
		}

		return sanitize_text_field( (string) $value );
	}
}