<?php
/**
 * PK Core - the deterministic classification engine (the free tier).
 *
 * No AI, no network, no cost. Generalises the proven classify_terms path from
 * the Classifier plugin: keyword presence against the site pack's derived
 * vocabulary (PK-CORE-SPEC-00 section 9). Matching order per item, against
 * ACTIVE topics only, multi-label:
 *   (a) TERM MATCH  - the item's raw category/tag names against topic
 *       keywords (exact, lower-cased). Site content usually resolves here.
 *   (b) TITLE SCAN  - topic keywords against the title, word-boundary,
 *       case-insensitive (external/competitor items carry no site terms).
 *   (c) TEXT SCAN   - same over the supplied text (bounded), only when (a)
 *       and (b) produced nothing.
 *
 * Deterministic results are keyword presence, not meaning: method is
 * 'deterministic', confidence is always null. Crude-but-functional on
 * external content is the designed free-tier behaviour.
 *
 * @package PublisherKits\Core
 * @since   0.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'PKCORE_Engine' ) ) {

	/**
	 * Class PKCORE_Engine
	 */
	class PKCORE_Engine {

		/**
		 * Maximum characters of item text scanned in pass (c).
		 */
		const TEXT_SCAN_LIMIT = 5000;

		/**
		 * Classify a batch of items deterministically.
		 *
		 * Signature matches the provider contract (spec 7.1): the same shape
		 * PKCORE_Classify::classify_items() promises its callers.
		 *
		 * @param array $items Map id => { title, terms?, text?, url? }.
		 * @param array $args  { context: 'site'|'external' } (unused here;
		 *                     deterministic matching is context-free).
		 * @param array $pack  The site pack (PKCORE_Pack::get() shape).
		 * @return array Map id => { topics:string[], method:string,
		 *               confidence:null }.
		 */
		public static function classify_items( $items, $args, $pack ) {
			$out = array();
			if ( ! is_array( $items ) || empty( $items ) ) {
				return $out;
			}

			list( $term_index, $scan_list ) = self::build_indexes( $pack );
			$aliases                        = isset( $pack['aliases'] ) && is_array( $pack['aliases'] ) ? $pack['aliases'] : array();

			foreach ( $items as $id => $item ) {
				if ( ! is_array( $item ) ) {
					$out[ $id ] = self::none();
					continue;
				}
				$topics = self::match_terms( $term_index, isset( $item['terms'] ) ? (array) $item['terms'] : array() );

				if ( empty( $topics ) ) {
					$title  = isset( $item['title'] ) ? (string) $item['title'] : '';
					$topics = self::match_text( $scan_list, $title );
				}

				if ( empty( $topics ) && ! empty( $item['text'] ) ) {
					$text   = substr( (string) $item['text'], 0, self::TEXT_SCAN_LIMIT );
					$topics = self::match_text( $scan_list, $text );
				}

				// Alias-resolve and dedupe (merged slugs report the survivor).
				$resolved = array();
				foreach ( $topics as $slug ) {
					$resolved[] = isset( $aliases[ $slug ] ) ? (string) $aliases[ $slug ] : $slug;
				}
				$resolved = array_values( array_unique( $resolved ) );

				$out[ $id ] = array(
					'topics'     => $resolved,
					'method'     => empty( $resolved ) ? 'none' : 'deterministic',
					'confidence' => null,
				);
			}
			return $out;
		}

		/**
		 * Build the match indexes for a pack's ACTIVE topics.
		 *
		 * @param array $pack The site pack.
		 * @return array { 0: array<string,string[]> keyword => topic slugs,
		 *                 1: array[] { slug, patterns[] } for text scans }.
		 */
		private static function build_indexes( $pack ) {
			$term_index = array();
			$scan_list  = array();
			$topics     = isset( $pack['topics'] ) && is_array( $pack['topics'] ) ? $pack['topics'] : array();

			foreach ( $topics as $topic ) {
				if ( empty( $topic['slug'] ) || ( isset( $topic['status'] ) && 'active' !== $topic['status'] ) ) {
					continue;
				}
				$slug     = (string) $topic['slug'];
				$keywords = isset( $topic['keywords'] ) ? (array) $topic['keywords'] : array();
				$label    = isset( $topic['label'] ) ? strtolower( trim( (string) $topic['label'] ) ) : '';
				if ( '' !== $label ) {
					$keywords[] = $label;
				}
				$patterns = array();
				foreach ( array_unique( $keywords ) as $keyword ) {
					$keyword = strtolower( trim( (string) $keyword ) );
					if ( '' === $keyword ) {
						continue;
					}
					if ( ! isset( $term_index[ $keyword ] ) ) {
						$term_index[ $keyword ] = array();
					}
					if ( ! in_array( $slug, $term_index[ $keyword ], true ) ) {
						$term_index[ $keyword ][] = $slug;
					}
					$patterns[] = '/\b' . preg_quote( $keyword, '/' ) . '\b/iu';
				}
				if ( ! empty( $patterns ) ) {
					$scan_list[] = array(
						'slug'     => $slug,
						'patterns' => $patterns,
					);
				}
			}
			return array( $term_index, $scan_list );
		}

		/**
		 * Pass (a): exact term-name lookups.
		 *
		 * @param array $term_index keyword => topic slugs.
		 * @param array $terms      Raw category/tag names.
		 * @return string[] Topic slugs.
		 */
		private static function match_terms( $term_index, $terms ) {
			$topics = array();
			foreach ( $terms as $term ) {
				$key = strtolower( trim( (string) $term ) );
				if ( '' !== $key && isset( $term_index[ $key ] ) ) {
					foreach ( $term_index[ $key ] as $slug ) {
						$topics[] = $slug;
					}
				}
			}
			return array_values( array_unique( $topics ) );
		}

		/**
		 * Passes (b)/(c): word-boundary keyword scan over a string.
		 *
		 * @param array  $scan_list { slug, patterns[] } per topic.
		 * @param string $text      Title or bounded body text.
		 * @return string[] Topic slugs.
		 */
		private static function match_text( $scan_list, $text ) {
			$topics = array();
			$text   = (string) $text;
			if ( '' === $text ) {
				return $topics;
			}
			foreach ( $scan_list as $entry ) {
				foreach ( $entry['patterns'] as $pattern ) {
					if ( preg_match( $pattern, $text ) ) {
						$topics[] = $entry['slug'];
						break;
					}
				}
			}
			return array_values( array_unique( $topics ) );
		}

		/**
		 * The empty (no-match) result row.
		 *
		 * @return array
		 */
		private static function none() {
			return array(
				'topics'     => array(),
				'method'     => 'none',
				'confidence' => null,
			);
		}
	}
}
