<?php
/**
 * Index builders for website content.
 *
 * @package Maca_Tjatt
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class Maca_Tjatt_Indexer
 */
class Maca_Tjatt_Indexer {

	const CHUNK_SIZE         = 1500;
	const CHUNK_OVERLAP      = 200;
	const MAX_BODY_CHARS     = 50000;
	const QUERY_BATCH_SIZE   = 100;
	const EMBEDDING_BATCH    = 64;

	/**
	 * @param array<string, mixed> $doc Document.
	 * @return array<string, mixed>|null
	 */
	private static function normalize_document( $doc ) {
		if ( ! is_array( $doc ) ) {
			return null;
		}

		$content   = isset( $doc['content'] ) ? trim( (string) $doc['content'] ) : '';
		$chunk_key = isset( $doc['chunk_key'] ) ? sanitize_text_field( (string) $doc['chunk_key'] ) : '';

		if ( '' === $content || '' === $chunk_key ) {
			return null;
		}

		$meta = isset( $doc['meta'] ) && is_array( $doc['meta'] ) ? $doc['meta'] : array();

		return array(
			'chunk_key'   => $chunk_key,
			'source_type' => isset( $doc['source_type'] ) ? sanitize_key( (string) $doc['source_type'] ) : 'general',
			'source_id'   => isset( $doc['source_id'] ) ? sanitize_text_field( (string) $doc['source_id'] ) : '',
			'lang'        => isset( $doc['lang'] ) ? sanitize_text_field( (string) $doc['lang'] ) : '',
			'title'       => isset( $doc['title'] ) ? sanitize_text_field( (string) $doc['title'] ) : '',
			'content'     => $content,
			'meta_json'   => wp_json_encode( $meta ),
		);
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	public static function collect_documents() {
		$documents = array();
		$batches   = array(
			self::collect_site_documents(),
			self::collect_custom_knowledge_documents(),
			self::collect_taxonomy_documents(),
			self::collect_page_documents(),
			self::collect_post_documents(),
			self::collect_custom_post_type_documents(),
		);

		foreach ( $batches as $batch ) {
			foreach ( $batch as $doc ) {
				$normalized = self::normalize_document( $doc );

				if ( $normalized ) {
					$documents[] = $normalized;
				}
			}
		}

		return $documents;
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	private static function collect_site_documents() {
		$lines = array(
			__( 'Website:', 'maca-tjatt' ) . ' ' . get_bloginfo( 'name' ),
			__( 'Tagline:', 'maca-tjatt' ) . ' ' . get_bloginfo( 'description' ),
			__( 'URL:', 'maca-tjatt' ) . ' ' . home_url( '/' ),
		);

		return array(
			array(
				'chunk_key'   => 'site:info',
				'source_type' => 'site',
				'source_id'   => 'info',
				'title'       => get_bloginfo( 'name' ),
				'content'     => implode( "\n", array_filter( $lines ) ),
				'meta'        => array( 'url' => home_url( '/' ) ),
			),
		);
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	private static function collect_custom_knowledge_documents() {
		$content = Maca_Tjatt_Service::get_custom_knowledge();

		if ( '' === $content ) {
			return array();
		}

		$title  = __( 'Additional knowledge', 'maca-tjatt' );
		$chunks = self::split_text_into_chunks( $title . "\n\n" . $content );
		$docs   = array();

		foreach ( $chunks as $index => $chunk ) {
			$docs[] = array(
				'chunk_key'   => 'custom:knowledge:' . $index,
				'source_type' => 'custom',
				'source_id'   => 'knowledge',
				'title'       => $title,
				'content'     => $chunk,
				'meta'        => array( 'url' => '' ),
			);
		}

		return $docs;
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	private static function collect_taxonomy_documents() {
		$documents = array();
		$taxonomies = array( 'category', 'post_tag' );

		foreach ( $taxonomies as $taxonomy ) {
			if ( ! taxonomy_exists( $taxonomy ) ) {
				continue;
			}

			$terms = get_terms(
				array(
					'taxonomy'   => $taxonomy,
					'hide_empty' => false,
				)
			);

			if ( is_wp_error( $terms ) || empty( $terms ) ) {
				continue;
			}

			foreach ( $terms as $term ) {
				$name        = isset( $term->name ) ? trim( (string) $term->name ) : '';
				$description = isset( $term->description ) ? trim( wp_strip_all_tags( (string) $term->description ) ) : '';

				if ( '' === $name && '' === $description ) {
					continue;
				}

				$content = $name;

				if ( '' !== $description ) {
					$content .= "\n\n" . $description;
				}

				$term_link = get_term_link( $term );
				$term_url  = is_wp_error( $term_link ) ? '' : (string) $term_link;

				$documents[] = array(
					'chunk_key'   => 'term:' . $taxonomy . ':' . (int) $term->term_id,
					'source_type' => 'taxonomy',
					'source_id'   => (string) $term->term_id,
					'title'       => $name,
					'content'     => $content,
					'meta'        => array(
						'taxonomy' => $taxonomy,
						'url'      => $term_url,
					),
				);
			}
		}

		return $documents;
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	private static function collect_page_documents() {
		$page_ids = Maca_Tjatt_Service::get_page_ids();

		if ( Maca_Tjatt_Service::index_all_pages() ) {
			$page_ids = array_values(
				array_unique(
					array_merge( $page_ids, self::get_all_published_ids( 'page', 'menu_order title', 'ASC' ) )
				)
			);
		}

		if ( empty( $page_ids ) ) {
			return array();
		}

		return self::collect_post_type_documents( $page_ids, 'page' );
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	private static function collect_post_documents() {
		if ( ! Maca_Tjatt_Service::index_posts() ) {
			return array();
		}

		$post_ids = self::get_all_published_ids( 'post', 'date', 'DESC' );

		return self::collect_post_type_documents( $post_ids, 'post' );
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	private static function collect_custom_post_type_documents() {
		$documents = array();

		foreach ( self::get_indexable_post_types() as $post_type ) {
			$post_ids  = self::get_all_published_ids( $post_type, 'date', 'DESC' );
			$documents = array_merge( $documents, self::collect_post_type_documents( $post_ids, $post_type ) );
		}

		return $documents;
	}

	/**
	 * @return array<int, string>
	 */
	private static function get_indexable_post_types() {
		$types = get_post_types(
			array(
				'public' => true,
			),
			'names'
		);

		$skip = array( 'post', 'page', 'attachment' );

		return array_values( array_diff( $types, $skip ) );
	}

	/**
	 * @param string $post_type Post type.
	 * @param string $orderby   Orderby field.
	 * @param string $order     ASC|DESC.
	 * @return array<int, int>
	 */
	private static function get_all_published_ids( $post_type, $orderby, $order ) {
		$ids     = array();
		$page    = 1;
		$per_page = self::QUERY_BATCH_SIZE;

		do {
			$batch = get_posts(
				array(
					'post_type'      => $post_type,
					'post_status'    => 'publish',
					'posts_per_page' => $per_page,
					'paged'          => $page,
					'fields'         => 'ids',
					'orderby'        => $orderby,
					'order'          => $order,
					'no_found_rows'  => true,
				)
			);

			if ( empty( $batch ) ) {
				break;
			}

			foreach ( $batch as $id ) {
				$id = absint( $id );

				if ( $id > 0 ) {
					$ids[] = $id;
				}
			}

			++$page;
		} while ( count( $batch ) === $per_page );

		return array_values( array_unique( $ids ) );
	}

	/**
	 * @param array<int, int> $post_ids Post IDs.
	 * @param string          $type     Post type slug.
	 * @return array<int, array<string, mixed>>
	 */
	private static function collect_post_type_documents( $post_ids, $type ) {
		$documents = array();

		foreach ( $post_ids as $post_id ) {
			$post = get_post( absint( $post_id ) );

			if ( ! $post instanceof WP_Post || 'publish' !== $post->post_status || $type !== $post->post_type ) {
				continue;
			}

			$body = wp_strip_all_tags( (string) $post->post_content );
			$body = trim( preg_replace( '/\s+/u', ' ', $body ) );

			$excerpt = trim( wp_strip_all_tags( (string) $post->post_excerpt ) );
			$title   = get_the_title( $post );

			if ( '' === $body && '' === $excerpt ) {
				continue;
			}

			$parts = array( $title );

			if ( '' !== $excerpt ) {
				$parts[] = $excerpt;
			}

			$term_labels = self::get_post_term_labels( $post );

			if ( '' !== $term_labels ) {
				$parts[] = $term_labels;
			}

			if ( '' !== $body ) {
				$parts[] = self::truncate_text( $body, self::MAX_BODY_CHARS );
			}

			$permalink = get_permalink( $post );
			$source_type = in_array( $type, array( 'page', 'post' ), true ) ? 'wp_' . $type : 'wp_cpt';
			$chunks      = self::split_text_into_chunks( implode( "\n\n", $parts ) );

			foreach ( $chunks as $index => $chunk ) {
				$documents[] = array(
					'chunk_key'   => $type . ':' . $post->ID . ':' . $index,
					'source_type' => $source_type,
					'source_id'   => (string) $post->ID,
					'title'       => $title,
					'content'     => $chunk,
					'meta'        => array(
						'url'       => is_string( $permalink ) ? $permalink : '',
						'post_type' => $type,
					),
				);
			}
		}

		return $documents;
	}

	/**
	 * @param WP_Post $post Post object.
	 * @return string
	 */
	private static function get_post_term_labels( $post ) {
		$labels = array();

		foreach ( array( 'category', 'post_tag' ) as $taxonomy ) {
			if ( ! is_object_in_taxonomy( $post->post_type, $taxonomy ) ) {
				continue;
			}

			$terms = get_the_terms( $post, $taxonomy );

			if ( is_wp_error( $terms ) || empty( $terms ) ) {
				continue;
			}

			foreach ( $terms as $term ) {
				if ( isset( $term->name ) && '' !== trim( (string) $term->name ) ) {
					$labels[] = trim( (string) $term->name );
				}
			}
		}

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

		return __( 'Topics:', 'maca-tjatt' ) . ' ' . implode( ', ', array_unique( $labels ) );
	}

	/**
	 * @param string $text Text.
	 * @param int    $size Chunk size.
	 * @param int    $overlap Overlap between chunks.
	 * @return array<int, string>
	 */
	private static function split_text_into_chunks( $text, $size = 0, $overlap = 0 ) {
		$text = trim( (string) $text );

		if ( '' === $text ) {
			return array();
		}

		$size    = $size > 0 ? $size : self::CHUNK_SIZE;
		$overlap = $overlap >= 0 ? $overlap : self::CHUNK_OVERLAP;

		if ( function_exists( 'mb_strlen' ) ) {
			if ( mb_strlen( $text ) <= $size ) {
				return array( $text );
			}
		} elseif ( strlen( $text ) <= $size ) {
			return array( $text );
		}

		$chunks = array();
		$offset = 0;

		while ( true ) {
			$chunk = self::truncate_text( $text, $size, $offset );

			if ( '' === $chunk ) {
				break;
			}

			$chunks[] = $chunk;

			if ( function_exists( 'mb_strlen' ) ) {
				$remaining = mb_strlen( $text ) - ( $offset + mb_strlen( $chunk ) );
			} else {
				$remaining = strlen( $text ) - ( $offset + strlen( $chunk ) );
			}

			if ( $remaining <= 0 ) {
				break;
			}

			$step   = max( 1, $size - $overlap );
			$offset += $step;
		}

		return $chunks;
	}

	/**
	 * @param string $text   Text.
	 * @param int    $length Max length.
	 * @param int    $offset Start offset.
	 * @return string
	 */
	private static function truncate_text( $text, $length, $offset = 0 ) {
		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $text, $offset, $length );
		}

		return substr( $text, $offset, $length );
	}

	/**
	 * @return array{success: bool, message: string, count: int}|WP_Error
	 */
	public static function build_index() {
		$documents = self::collect_documents();

		if ( empty( $documents ) ) {
			Maca_Tjatt_DB::delete_all_chunks();
			Maca_Tjatt_Service::set_index_status(
				array(
					'success'  => false,
					'message'  => __( 'No content found to index.', 'maca-tjatt' ),
					'count'    => 0,
					'built_at' => gmdate( 'c' ),
				)
			);

			return new WP_Error( 'maca_tjatt_empty_index', __( 'No content found to index.', 'maca-tjatt' ) );
		}

		Maca_Tjatt_DB::delete_all_chunks();

		$saved = 0;

		foreach ( $documents as $doc ) {
			$doc['embedding'] = '[]';

			if ( Maca_Tjatt_DB::upsert_chunk( $doc ) ) {
				++$saved;
			}
		}

		$message = sprintf(
			/* translators: %d: number of indexed chunks */
			__( 'Indexed %d content chunks.', 'maca-tjatt' ),
			$saved
		);

		Maca_Tjatt_Service::set_index_status(
			array(
				'success'  => true,
				'message'  => $message,
				'count'    => $saved,
				'built_at' => gmdate( 'c' ),
			)
		);

		return array(
			'success' => true,
			'message' => $message,
			'count'   => $saved,
		);
	}

	/**
	 * @param int     $post_id Post ID.
	 * @param WP_Post $post    Post object.
	 */
	public static function on_content_changed( $post_id, $post ) {
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}

		if ( ! $post instanceof WP_Post ) {
			return;
		}

		if ( ! is_post_type_viewable( $post->post_type ) || 'attachment' === $post->post_type ) {
			return;
		}

		if ( ! Maca_Tjatt_Service::is_enabled() ) {
			return;
		}

		Maca_Tjatt_Service::schedule_reindex();
	}
}
