<?php

namespace Limb_Chatbot\Includes\Services\Knowledge\Knowledge_Mappers;

use Limb_Chatbot\Includes\Data_Objects\Dataset;
use Limb_Chatbot\Includes\Data_Objects\Dataset_Meta;
use Limb_Chatbot\Includes\Interfaces\Knowledge_Mapper_Interface;
use WP_Post;

/**
 * Knowledge mapper for Custom Post Type (CPT) actionable knowledge.
 *
 * Routing via WordPress filters
 * ─────────────────────────────
 * Before building any content this mapper fires two per-post-type filters that
 * allow integrations to take full ownership of content generation without
 * touching this class (Open/Closed Principle):
 *
 *   lbaic_cpt_knowledge_input_{post_type}
 *       (string $default_input, WP_Post $post, Dataset|null $dataset)
 *
 *   lbaic_cpt_knowledge_output_{post_type}
 *       (string $default_output, WP_Post $post, Dataset|null $dataset)
 *
 * An integration (e.g. WooCommerce for "product") hooks into these filters and
 * returns its own rich content, completely replacing the generic fallback.
 *
 * Generic fallback
 * ─────────────────
 * When no integration filter is registered the mapper builds content from:
 *  - Post title, excerpt, and body (truncated).
 *  - Selected taxonomies stored in dataset metadata.
 *  - ACF custom fields  (via the lbaic_dataset_entry_custom_fields_content filter).
 *  - JetEngine custom fields (via get_post_meta, keys from dataset meta).
 *  - Raw meta_fields           (via get_post_meta, keys from dataset meta).
 *
 * @since 1.0.0
 */
class CPT_Knowledge_Mapper implements Knowledge_Mapper_Interface {

	/**
	 * Maximum characters used from post_content in the generic fallback.
	 *
	 * Keeping this finite prevents token overflow while still providing good
	 * context for hybrid search.
	 */
	private const MAX_CONTENT_LENGTH = 1500;

	// -------------------------------------------------------------------------
	// Knowledge_Mapper_Interface
	// -------------------------------------------------------------------------

	public function get_entry_input( $object ): string {
		if ( ! $object instanceof WP_Post ) {
			return '';
		}

		$dataset = $this->resolve_dataset_context();
		$default = get_the_title( $object );

		/**
		 * Allow integrations to supply a richer input (search-query form) for
		 * a specific post type. If no filter returns a non-empty string the
		 * post title is used as-is.
		 *
		 * @param string       $default  Post title.
		 * @param WP_Post      $object   Source post.
		 * @param Dataset|null $dataset  Associated dataset or null.
		 */
		$input = (string) apply_filters(
			'lbaic_cpt_knowledge_input_' . $object->post_type,
			$default,
			$object,
			$dataset
		);

		return '' !== trim( $input ) ? $input : $default;
	}

	public function get_entry_output( $object ): string {
		if ( ! $object instanceof WP_Post ) {
			return '';
		}

		$dataset = $this->resolve_dataset_context();
		$default = $this->build_generic_output( $object, $dataset );

		/**
		 * Allow integrations to supply rich, complete output for a specific post
		 * type. When a non-empty string is returned the generic output is not used.
		 *
		 * @param string       $default  Generic output.
		 * @param WP_Post      $object   Source post.
		 * @param Dataset|null $dataset  Associated dataset or null.
		 */
		$output = (string) apply_filters(
			'lbaic_cpt_knowledge_output_' . $object->post_type,
			$default,
			$object,
			$dataset
		);

		return '' !== trim( $output ) ? $output : $default;
	}

	// -------------------------------------------------------------------------
	// Generic fallback content builder
	// -------------------------------------------------------------------------

	/**
	 * Build a generic embeddable output when no integration override is active.
	 *
	 * Sections are separated by blank lines so the hybrid keyword scorer in
	 * Vector_Db_Searcher can tokenise them correctly.
	 */
	private function build_generic_output( WP_Post $object, ?Dataset $dataset ): string {
		$title   = $this->clean_text( get_the_title( $object ) );
		$excerpt = $this->clean_text( get_the_excerpt( $object ) );
		$content = $this->clean_text( apply_filters( 'the_content', $object->post_content ) );
		$content = substr( $content, 0, self::MAX_CONTENT_LENGTH );

		$selected_taxonomies = $this->get_selected_taxonomies( $dataset );
		$taxonomy_context    = $this->build_taxonomy_context( $object, $selected_taxonomies );
		$custom_fields_text  = $this->build_custom_fields_text( $object, $dataset );

		$sections = [];

		if ( '' !== $title ) {
			$sections[] = 'Title: ' . $title;
		}
		if ( '' !== $excerpt ) {
			$sections[] = 'Summary: ' . $excerpt;
		}
		if ( '' !== $content ) {
			$sections[] = 'Content: ' . $content;
		}
		if ( ! empty( $taxonomy_context['lines'] ) ) {
			$sections[] = "Taxonomy Context:\n" . implode( "\n", $taxonomy_context['lines'] );
		}
		if ( '' !== $custom_fields_text ) {
			$sections[] = "Custom Fields:\n" . $custom_fields_text;
		}
		if ( ! empty( $taxonomy_context['keywords'] ) ) {
			$sections[] = 'Search Keywords: ' . implode( ', ', $taxonomy_context['keywords'] );
		}

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

	// -------------------------------------------------------------------------
	// Custom fields (ACF / JetEngine / raw meta)
	// -------------------------------------------------------------------------

	/**
	 * Aggregate content from all configured custom field sources.
	 *
	 * Sources consulted (in order):
	 *  1. ACF custom fields  — delegated to the ACF integration filter.
	 *  2. JetEngine fields   — raw get_post_meta() per key stored in dataset meta.
	 *  3. Raw meta_fields    — raw get_post_meta() per key stored in dataset meta.
	 */
	private function build_custom_fields_text( WP_Post $object, ?Dataset $dataset ): string {
		if ( ! $dataset instanceof Dataset ) {
			return '';
		}

		$parts = [];

		// 1. ACF custom fields — the ACF integration listens to this filter.
		$acf_content = (string) apply_filters(
			'lbaic_dataset_entry_custom_fields_content',
			'',
			$object,
			$dataset
		);
		if ( '' !== trim( $acf_content ) ) {
			$parts[] = trim( $acf_content );
		}

		// 2. JetEngine custom fields — stored in dataset meta as an array of keys.
		$jet_keys = $this->decode_meta_array( $dataset->get_meta_value( 'jet_engine_custom_fields' ) );
		foreach ( $jet_keys as $meta_key ) {
			$line = $this->format_meta_field( $object->ID, $meta_key );
			if ( '' !== $line ) {
				$parts[] = $line;
			}
		}

		// 3. Raw meta_fields — stored in dataset meta as an array of keys.
		$meta_keys = $this->decode_meta_array( $dataset->get_meta_value( 'meta_fields' ) );
		foreach ( $meta_keys as $meta_key ) {
			$line = $this->format_meta_field( $object->ID, $meta_key );
			if ( '' !== $line ) {
				$parts[] = $line;
			}
		}

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

	/**
	 * Read a single post meta key and format it as "Label: Value".
	 * Returns an empty string when the value is empty or unreadable.
	 */
	private function format_meta_field( int $post_id, string $meta_key ): string {
		$meta_key = trim( $meta_key );
		if ( '' === $meta_key ) {
			return '';
		}

		$value = get_post_meta( $post_id, $meta_key, true );
		if ( '' === $value || null === $value || false === $value ) {
			return '';
		}

		if ( is_array( $value ) || is_object( $value ) ) {
			$value = wp_json_encode( $value );
		}

		$label = ucwords( str_replace( [ '_', '-' ], ' ', $meta_key ) );
		$clean = $this->clean_text( (string) $value );

		return '' !== $clean ? "{$label}: {$clean}" : '';
	}

	/**
	 * Normalise a dataset meta value into a plain string array.
	 * Handles raw arrays, JSON-encoded arrays, and comma-separated strings.
	 *
	 * @param mixed $value
	 * @return string[]
	 */
	private function decode_meta_array( $value ): array {
		if ( is_array( $value ) ) {
			return array_values( array_filter( array_map( 'strval', $value ) ) );
		}

		if ( is_string( $value ) && '' !== $value ) {
			$decoded = json_decode( $value, true );
			if ( is_array( $decoded ) ) {
				return array_values( array_filter( array_map( 'strval', $decoded ) ) );
			}
		}

		return [];
	}

	// -------------------------------------------------------------------------
	// Taxonomy context
	// -------------------------------------------------------------------------

	/**
	 * Retrieve the taxonomy whitelist stored in dataset metadata.
	 *
	 * When the list is empty all registered taxonomies for the post type are
	 * included (minus the WooCommerce native ones, which are always included
	 * anyway — see is_native_woocommerce_product_taxonomy()).
	 */
	private function get_selected_taxonomies( ?Dataset $dataset ): array {
		if ( ! $dataset instanceof Dataset ) {
			return [];
		}

		$tax_meta = $dataset->get_meta( 'taxonomies' );
		if ( ! $tax_meta instanceof Dataset_Meta ) {
			return [];
		}

		$selected = $tax_meta->get_meta_value();

		return is_array( $selected ) ? $selected : [];
	}

	/**
	 * Build human-readable taxonomy lines and a flat keyword list for the
	 * entry output.
	 *
	 * @return array{lines: string[], keywords: string[]}
	 */
	private function build_taxonomy_context( WP_Post $object, array $selected_taxonomies ): array {
		$taxonomies = get_object_taxonomies( $object->post_type, 'objects' );
		$lines      = [];
		$keywords   = [];

		foreach ( $taxonomies as $taxonomy ) {
			if (
				! empty( $selected_taxonomies ) &&
				! in_array( $taxonomy->name, $selected_taxonomies, true ) &&
				! $this->is_native_woocommerce_product_taxonomy( $object, (string) $taxonomy->name )
			) {
				continue;
			}

			$terms = get_the_terms( $object, $taxonomy->name );
			if ( empty( $terms ) || is_wp_error( $terms ) ) {
				continue;
			}

			$term_names = array_values( array_filter( array_map( 'strval', wp_list_pluck( $terms, 'name' ) ) ) );
			$term_slugs = array_values( array_filter( array_map( 'strval', wp_list_pluck( $terms, 'slug' ) ) ) );

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

			$taxonomy_label = $this->clean_text( (string) ( $taxonomy->label ?: $taxonomy->name ) );
			$lines[]        = sprintf(
				'%s (%s): %s',
				$taxonomy_label,
				$taxonomy->name,
				implode( ', ', $term_names )
			);

			$keywords = array_merge( $keywords, $term_names, $term_slugs );
		}

		$keywords = array_values(
			array_unique(
				array_filter(
					array_map( [ $this, 'clean_text' ], $keywords )
				)
			)
		);

		return [
			'lines'    => $lines,
			'keywords' => $keywords,
		];
	}

	/**
	 * Detect WooCommerce native product taxonomies that are always included
	 * regardless of the selected_taxonomies whitelist.
	 *
	 * Note: this is a guard in the generic fallback. The WooCommerce integration
	 * builds its own rich taxonomy context via the filter override.
	 */
	private function is_native_woocommerce_product_taxonomy( WP_Post $object, string $taxonomy ): bool {
		if ( 'product' !== $object->post_type ) {
			return false;
		}

		static $native = [
			'product_cat',
			'product_tag',
			'product_type',
			'product_visibility',
			'product_shipping_class',
			'product_brand',
			'brand',
			'pwb-brand',
			'yith_product_brand',
		];

		if ( in_array( $taxonomy, $native, true ) ) {
			return true;
		}

		// WooCommerce global attribute taxonomies are registered as pa_*.
		if ( 0 === strpos( $taxonomy, 'pa_' ) ) {
			return true;
		}

		return false !== strpos( $taxonomy, 'brand' );
	}

	// -------------------------------------------------------------------------
	// Dataset context bridge
	// -------------------------------------------------------------------------

	/**
	 * Resolve the dataset that CPT_Knowledge_Generator injected via a filter.
	 *
	 * CPT_Knowledge_Generator calls:
	 *   add_filter('lbaic_cpt_mapper_dataset_context', fn() => $dataset);
	 *
	 * We read it here so the mapper can access job-time metadata (custom-field
	 * keys, taxonomy selections, etc.) without requiring a DB lookup.
	 *
	 * @return Dataset|null
	 */
	private function resolve_dataset_context(): ?Dataset {
		$dataset = apply_filters( 'lbaic_cpt_mapper_dataset_context', null );

		return ( $dataset instanceof Dataset ) ? $dataset : null;
	}

	// -------------------------------------------------------------------------
	// Utilities
	// -------------------------------------------------------------------------

	/**
	 * Normalise a string to plain, search-safe text.
	 */
	private function clean_text( string $value ): string {
		return trim( wp_strip_all_tags( $value, true ) );
	}
}
