<?php

namespace Limb_Chatbot\Includes\Services;

use Exception;
use Limb_Chatbot\Includes\Services\Knowledge\Post_Content_Config_Service;

/**
 * Service class for managing post-related operations
 *
 * @since 1.0.0
 */
class Post_Types_Service {
	/**
	 * Supported meta comparison operators accepted by API filter payload.
	 *
	 * @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'     => '<',
	];

	/**
	 * Retrieves a collection of posts for a specific custom post type.
	 *
	 * Supports filtering by search keyword, pagination, and other WP_Query parameters.
	 *
	 * @param  string  $cpt_name  Custom post type name.
	 * @param  array  $params  Query parameters including page, per_page, and s (search).
	 *
	 * @return Collection Collection of posts with required fields.
	 * @throws Exception
	 * @since 1.0.0
	 */
	public function get_posts( string $cpt_name, array $params = [] ): Collection {
		// Handle title search with direct SQL query
		if ( ! empty( $params['lbaic_title_search'] ) ) {
			return $this->get_posts_by_title_search( $cpt_name, $params );
		}

		// Standard WordPress query without title search
		$default_params = [
			'post_type'      => sanitize_key( $cpt_name ),
			'posts_per_page' => isset( $params['per_page'] ) ? absint( $params['per_page'] ) : 10,
			'paged'          => isset( $params['page'] ) ? absint( $params['page'] ) : 1,
			'post_status'    => 'publish',
			'orderby'        => ! empty( $params['orderby'] ) ? sanitize_key( $params['orderby'] ) : 'modified',
			'numberposts'    => - 1,
			'order'          => ! empty( $params['order'] ) && in_array( strtoupper( $params['order'] ),
				[ 'ASC', 'DESC' ],
				true ) ? strtoupper( $params['order'] ) : 'DESC',
		];

		// Get posts
		$posts = get_posts( $default_params );

		// Transform posts to collection with required fields
		$items = array_map( function ( $post ) {
			return [
				'id'           => $post->ID,
				'title'        => $post->post_title,
				'link'         => get_permalink( $post->ID ),
				'modified_gmt' => $post->post_modified_gmt,
			];
		}, $posts );

		$collection = new Collection( $items );

		// Set total count for pagination
		$total = wp_count_posts( $cpt_name )->publish ?? 0;
		$collection->set_total( (int) $total );
		$this->append_included_data( $collection, $cpt_name, $params );

		// Handle include parameter for additional data
		$include = ! empty( $params['include'] ) ? $params['include'] : [];
		if ( ! empty( $include ) ) {
			/**
			 * Filter: Allow integrations to add additional properties to post types collection.
			 *
			 * @param  Collection  $collection  The collection to modify.
			 * @param  string  $cpt_name  Post type name.
			 * @param  array  $include  Include parameter value.
			 * @param  array  $params  Original query parameters.
			 */
			apply_filters( 'lbaic_post_types_collection_include', $collection, $cpt_name, $include, $params );
		}

		return $collection;
	}

	/**
	 * Retrieves posts filtered by title search using WP_Query.
	 *
	 * Searches only in post_title field for more precise results.
	 * Uses a temporary filter to modify the WHERE clause for this specific query.
	 *
	 * @param  string  $cpt_name  Custom post type name.
	 * @param  array  $params  Query parameters including lbaic_title_search, pagination, etc.
	 *
	 * @return Collection Collection of posts matching the title search.
	 * @since 1.0.0
	 */
	private function get_posts_by_title_search( string $cpt_name, array $params ): Collection {
		$search_term = sanitize_text_field( $params['lbaic_title_search'] );

		// Create a closure for the filter to avoid polluting global scope
		$title_search_filter = function ( $where ) use ( $search_term ) {
			global $wpdb;
			$like  = '%' . $wpdb->esc_like( $search_term ) . '%';
			$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_title LIKE %s", $like );

			return $where;
		};

		// Add filter temporarily
		add_filter( 'posts_where', $title_search_filter );

		// Build query args
		$query_args = [
			'post_type'      => sanitize_key( $cpt_name ),
			'post_status'    => 'publish',
			'posts_per_page' => isset( $params['per_page'] ) ? absint( $params['per_page'] ) : 10,
			'paged'          => isset( $params['page'] ) ? absint( $params['page'] ) : 1,
			'orderby'        => ! empty( $params['orderby'] ) ? sanitize_key( $params['orderby'] ) : 'modified',
			'order'          => ! empty( $params['order'] ) && in_array( strtoupper( $params['order'] ),
				[ 'ASC', 'DESC' ],
				true )
				? strtoupper( $params['order'] )
				: 'DESC',
		];

		// Execute query
		$query = new \WP_Query( $query_args );

		// Remove filter immediately after query
		remove_filter( 'posts_where', $title_search_filter );

		// Transform posts to collection with required fields
		$items = array_map( function ( $post ) {
			return [
				'id'           => $post->ID,
				'title'        => $post->post_title,
				'link'         => get_permalink( $post->ID ),
				'modified_gmt' => $post->post_modified_gmt,
			];
		}, $query->posts );

		$collection = new Collection( $items );

		// Set total from WP_Query
		$collection->set_total( (int) $query->found_posts );

		$this->append_included_data( $collection, $cpt_name, $params );

		// Reset post data
		wp_reset_postdata();

		// Handle include parameter for additional data
		$include = ! empty( $params['include'] ) ? sanitize_text_field( $params['include'] ) : '';
		if ( ! empty( $include ) ) {
			/**
			 * Filter: Allow integrations to add additional properties to post types collection.
			 *
			 * @param  Collection  $collection  The collection to modify.
			 * @param  string  $post_type  Post type name.
			 * @param  array  $include  Include parameter values.
			 * @param  array  $params  Original query parameters.
			 */
			apply_filters( 'lbaic_post_types_collection_include', $collection, $cpt_name, $include, $params );
		}

		return $collection;
	}

	/**
	 * Append include-based metadata to the posts collection.
	 *
	 * @param  Collection  $collection  Collection that should be enriched.
	 * @param  string  $cpt_name  Current post type name.
	 * @param  array  $params  Request query params.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function append_included_data( Collection $collection, string $cpt_name, array $params ): void {
		$include = ! empty( $params['include'] ) ? (array) $params['include'] : [];
		$include = array_map( 'sanitize_key', $include );
		$include = array_filter( array_unique( $include ) );

		if ( empty( $include ) ) {
			return;
		}

		if ( in_array( 'taxonomies', $include, true ) ) {
			$collection->push_property( 'taxonomies', $this->get_taxonomies_with_terms( $cpt_name ) );
		}

		if ( in_array( 'meta_fields', $include, true ) ) {
			$collection->push_property( 'meta_fields', $this->get_meta_fields( $cpt_name ) );
		}

		if ( in_array( 'authors', $include, true ) ) {
			$collection->push_property( 'authors', $this->get_authors( $cpt_name ) );
		}

		if ( in_array( 'content_collection', $include, true ) ) {
			$collection->push_property( 'content_collection', $this->get_content_collection( $cpt_name ) );
		}

		if ( in_array( 'native_fields', $include, true ) ) {
			$collection->push_property( 'native_fields', $this->get_native_fields( $cpt_name ) );
		}
		/**
		 * Filter: Allow integrations to add additional properties to post types collection.
		 *
		 * @param  Collection  $collection  The collection to modify.
		 * @param  string  $cpt_name  Post type name.
		 * @param  array  $include  Include parameter value.
		 * @param  array  $params  Original query parameters.
		 */
		apply_filters( 'lbaic_post_types_collection_include', $collection, $cpt_name, $include, $params );
	}

	/**
	 * Get all taxonomies attached to a post type with their available terms.
	 *
	 * @param  string  $cpt_name  Post type name.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	private function get_taxonomies_with_terms( string $cpt_name ): array {
		$taxonomy_objects = get_object_taxonomies( $cpt_name, 'objects' );

		if ( empty( $taxonomy_objects ) ) {
			return [];
		}

		$taxonomies = [];

		foreach ( $taxonomy_objects as $taxonomy ) {
			$terms = get_terms( [
				'taxonomy'   => $taxonomy->name,
				'hide_empty' => false,
			] );

			if ( is_wp_error( $terms ) ) {
				$terms = [];
			}

			$taxonomies[] = [
				'name'           => $taxonomy->name,
				'label'          => $taxonomy->label,
				'singular_label' => $taxonomy->labels->singular_name ?? $taxonomy->label,
				'terms'          => array_map( static function ( $term ) {
					return [
						'id'   => (int) $term->term_id,
						'name' => $term->name,
						'slug' => $term->slug,
					];
				}, $terms ),
			];
		}

		return $taxonomies;
	}

	/**
	 * Get all distinct post meta keys used by a post type.
	 *
	 * @param  string  $cpt_name  Post type name.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	private function get_meta_fields( string $cpt_name ): array {
		global $wpdb;

		$query = $wpdb->prepare(
			"SELECT DISTINCT pm.meta_key
			FROM {$wpdb->postmeta} pm
			INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
			WHERE p.post_type = %s
			AND p.post_status <> 'auto-draft'
			ORDER BY pm.meta_key ASC",
			$cpt_name
		);

		$meta_fields = $wpdb->get_col( $query );

		if ( empty( $meta_fields ) ) {
			return [];
		}

		return array_values( array_filter( $meta_fields, static function ( $meta_key ) {
			return is_string( $meta_key ) && '' !== $meta_key;
		} ) );
	}

	/**
	 * Get unique authors that have posts in the given post type.
	 *
	 * @param  string  $cpt_name  Post type name.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	private function get_authors( string $cpt_name ): array {
		global $wpdb;

		$query = $wpdb->prepare(
			"SELECT DISTINCT post_author
			FROM {$wpdb->posts}
			WHERE post_type = %s
			AND post_author > 0
			AND post_status <> 'auto-draft'
			ORDER BY post_author ASC",
			$cpt_name
		);

		$author_ids = array_map( 'absint', $wpdb->get_col( $query ) );
		$author_ids = array_filter( array_unique( $author_ids ) );

		if ( empty( $author_ids ) ) {
			return [];
		}

		$authors = get_users( [
			'include' => $author_ids,
			'orderby' => 'display_name',
			'order'   => 'ASC',
		] );

		return array_map( static function ( $author ) {
			return [
				'id'           => (int) $author->ID,
				'display_name' => $author->display_name,
				'avatar_url'   => get_avatar_url( $author->ID ),
			];
		}, $authors );
	}

	private function get_content_collection( string $cpt_name ) {
		$config = ( new Post_Content_Config_Service() )->resolve_for_post_type( $cpt_name, [] );

		// API consumers expect taxonomy keys (e.g. category, post_tag), while
		// indexing internals persist selected term IDs. Convert IDs to taxonomy keys
		// only for this response payload.
		if ( ! empty( $config['taxonomies'] ) && is_array( $config['taxonomies'] ) ) {
			$config['taxonomies'] = $this->map_term_ids_to_taxonomy_names( $config['taxonomies'] );
		}

		return $config;
	}

	/**
	 * Convert selected term IDs into unique taxonomy names.
	 *
	 * @param  array  $selected_taxonomies  Flat list that may contain term IDs or taxonomy names.
	 *
	 * @return string[]
	 * @since 1.0.0
	 */
	private function map_term_ids_to_taxonomy_names( array $selected_taxonomies ): array {
		$taxonomy_names = [];

		foreach ( $selected_taxonomies as $taxonomy_value ) {
			if ( is_numeric( $taxonomy_value ) ) {
				$term_id = absint( $taxonomy_value );
				if ( $term_id <= 0 ) {
					continue;
				}

				$term = get_term( $term_id );
				if ( ! $term || is_wp_error( $term ) || empty( $term->taxonomy ) ) {
					continue;
				}

				$taxonomy_names[] = sanitize_key( (string) $term->taxonomy );
				continue;
			}

			if ( is_string( $taxonomy_value ) ) {
				$taxonomy_name = sanitize_key( $taxonomy_value );
				if ( '' !== $taxonomy_name ) {
					$taxonomy_names[] = $taxonomy_name;
				}
			}
		}

		return array_values( array_filter( array_unique( $taxonomy_names ) ) );
	}

	/**
	 * Get native post fields supported by a given post type.
	 *
	 * Shape is intentionally aligned with ACF/JetEngine include payloads:
	 * Collection of items with `key` and `label`.
	 *
	 * @param  string  $cpt_name  Post type name.
	 *
	 * @return Collection
	 * @since 1.0.0
	 */
	private function get_native_fields( string $cpt_name ): Collection {
		$post_type = get_post_type_object( sanitize_key( $cpt_name ) );

		if ( ! $post_type ) {
			return new Collection( [] );
		}

		$fields = [];

		if ( post_type_supports( $post_type->name, 'title' ) ) {
			$fields[] = [ 'key' => 'post_title', 'label' => __( 'Post Title', 'limb-chatbot' ) ];
		}

		if ( post_type_supports( $post_type->name, 'editor' ) ) {
			$fields[] = [ 'key' => 'post_content', 'label' => __( 'Post Content', 'limb-chatbot' ) ];
		}

		if ( post_type_supports( $post_type->name, 'excerpt' ) ) {
			$fields[] = [ 'key' => 'post_excerpt', 'label' => __( 'Post Excerpt', 'limb-chatbot' ) ];
		}

		// Native DB post date is always available for WP posts.
		$fields[] = [ 'key' => 'post_date', 'label' => __( 'Post Date', 'limb-chatbot' ) ];

		if ( post_type_supports( $post_type->name, 'author' ) ) {
			$fields[] = [ 'key' => 'author_name', 'label' => __( 'Author Name', 'limb-chatbot' ) ];
		}

		if ( $post_type->public || $post_type->publicly_queryable ) {
			$fields[] = [ 'key' => 'permalink', 'label' => __( 'Post URL/Permalink', 'limb-chatbot' ) ];
		}

		if ( post_type_supports( $post_type->name, 'thumbnail' ) ) {
			$fields[] = [ 'key' => 'featured_image', 'label' => __( 'Featured Image', 'limb-chatbot' ) ];
		}

		return new Collection( $fields );
	}

	/**
	 * Count posts for a post type using advanced filters.
	 *
	 * This method is intentionally reusable so the same filter parsing can later
	 * be used for fetching filtered items, not only counting them.
	 *
	 * @param  string  $cpt_name  Post type name.
	 * @param  array  $filters  Filters payload.
	 *
	 * @return int
	 * @since 1.0.0
	 */
	public function get_filtered_posts_count( string $cpt_name, array $filters = [] ): int {
		$query_args = $this->build_filtered_posts_query_args( $cpt_name, $filters, [
			'fields'         => 'ids',
			'posts_per_page' => 1,
			'paged'          => 1,
			'no_found_rows'  => false,
		] );

		$query = new \WP_Query( $query_args );

		return (int) $query->found_posts;
	}

	/**
	 * Build WP_Query args from the structured filters payload.
	 *
	 * @param  string  $cpt_name  Post type name.
	 * @param  array  $filters  Raw filters from API request.
	 * @param  array  $overrides  Query args to override defaults.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	private function build_filtered_posts_query_args(
		string $cpt_name,
		array $filters = [],
		array $overrides = []
	): array {
		$filters = $this->normalize_filters_payload( $filters );

		$args = [
			'post_type'   => sanitize_key( $cpt_name ),
			'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 UI list format to grouped query format.
	 *
	 * Supports both:
	 * - Grouped object: { taxonomies, meta_fields, author }
	 * - Rule list: [ { type: taxonomy|meta_field|authors, ... } ]
	 *
	 * @param  array  $filters  Raw filters payload.
	 *
	 * @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;
	}

	/**
	 * Convert taxonomy filters to a WP tax_query array.
	 *
	 * @param  mixed  $raw_taxonomies  Raw taxonomy filters.
	 *
	 * @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 : [];
	}

	/**
	 * Convert meta field filters to a WP meta_query array.
	 *
	 * @param  mixed  $raw_meta_fields  Raw meta filters.
	 *
	 * @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 : [];
	}

	/**
	 * Sanitize meta value while preserving numeric types for comparisons.
	 *
	 * @param  mixed  $value  Meta 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 );
	}

	/**
	 * Normalize author filter values to integer user IDs.
	 *
	 * @param  mixed  $raw_authors  Raw author filters.
	 *
	 * @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 );
	}
}

