<?php
/**
 * File Path: includes/admin/class-mbs-genre-engine.php
 *
 * Genre Engine
 *
 * Loads the curated genre map from data/genre-map.json and provides
 * methods for the Classification metabox:
 *   - get_categories()           — top-level category list (Step 1).
 *   - get_genres_for_category()  — genres filtered by category (Step 2).
 *   - get_suggestions()          — deduplicated BISAC + Thema suggestions (Step 3).
 *   - get_label_for_slug()       — resolves slug to display label for taxonomy sync.
 *
 * The genre map is a curated static file maintained by the product team.
 * It is not DB-stored. No business logic belongs in the DAO.
 *
 * genre-map.json structure:
 *   {
 *     "fiction-romance-suspense": {
 *       "label": "Romance / Suspense",
 *       "category": "FICTION",
 *       "bisac_codes": ["FIC027020", "FIC027050"],
 *       "thema_prefix_rules": ["FRH", "FRD"]
 *     }
 *   }
 *
 * @package MyBookShowroom
 * @since   1.1.0
 */

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

class MBS_Genre_Engine { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/**
	 * Loaded genre map. Null until first use.
	 *
	 * @var array<string, array{ label: string, category: string, bisac_codes: string[], thema_prefix_rules: string[] }>|null
	 */
	private ?array $map = null;

	/**
	 * DAO for enriching suggestion codes with descriptions from the DB.
	 *
	 * @var MBS_Classification_DAO
	 */
	private MBS_Classification_DAO $dao;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->dao = new MBS_Classification_DAO();
	}

	// -------------------------------------------------------------------------
	// Public API
	// -------------------------------------------------------------------------

	/**
	 * Return the unique top-level categories for the Step 1 selector.
	 *
	 * @return string[] Unique category strings in the order they first appear
	 *                  in the genre map (e.g. "FICTION", "NONFICTION", ...).
	 */
	public function get_categories(): array {
		$map        = $this->load_map();
		$categories = array();
		foreach ( $map as $genre ) {
			$cat = $genre['category'] ?? '';
			if ( $cat && ! in_array( $cat, $categories, true ) ) {
				$categories[] = $cat;
			}
		}
		return $categories;
	}

	/**
	 * Return genre slugs and labels for a given top-level category.
	 *
	 * @param string $category Category string (e.g. "FICTION").
	 * @return array<string, string> Associative array of slug => label.
	 */
	public function get_genres_for_category( string $category ): array {
		$map    = $this->load_map();
		$genres = array();
		foreach ( $map as $slug => $genre ) {
			if ( isset( $genre['category'] ) && $genre['category'] === $category ) {
				$genres[ $slug ] = $genre['label'] ?? $slug;
			}
		}
		return $genres;
	}

	/**
	 * Return deduplicated BISAC + Thema suggestions for the given genre slugs.
	 *
	 * For BISAC: collects codes from bisac_codes arrays across all genre slugs,
	 * deduplicates, and enriches with descriptions from the DAO.
	 *
	 * For Thema: uses thema_prefix_rules as direct code strings (each rule is
	 * treated as a Thema subject code suggestion), deduplicates across slugs,
	 * and enriches with descriptions + notes from the DAO.
	 *
	 * @param string[] $genre_slugs Array of genre slugs selected by the user.
	 * @return array{
	 *   bisac: array<int, array{ code: string, description: string }>,
	 *   thema: array<int, array{ code: string, description: string, notes: string, is_qualifier: int }>
	 * } Suggestion arrays ready for the metabox JS.
	 */
	public function get_suggestions( array $genre_slugs ): array {
		$map = $this->load_map();

		$bisac_codes = array();
		$thema_codes = array();

		foreach ( $genre_slugs as $slug ) {
			if ( ! isset( $map[ $slug ] ) ) {
				continue;
			}
			$genre = $map[ $slug ];

			// Collect unique BISAC codes.
			foreach ( $genre['bisac_codes'] ?? array() as $code ) {
				$bisac_codes[ $code ] = true;
			}

			// Collect unique Thema codes from prefix rules.
			foreach ( $genre['thema_prefix_rules'] ?? array() as $code ) {
				$thema_codes[ $code ] = true;
			}
		}

		// Enrich BISAC suggestions with descriptions.
		$bisac_suggestions = array();
		foreach ( array_keys( $bisac_codes ) as $code ) {
			$rows = $this->dao->get_bisac_by_prefix( $code, 1 );
			$bisac_suggestions[] = array(
				'code'        => $code,
				'description' => ! empty( $rows ) ? $rows[0]['description'] : '',
			);
		}

		// Enrich Thema suggestions with descriptions + notes.
		$thema_suggestions = array();
		foreach ( array_keys( $thema_codes ) as $code ) {
			$rows = $this->dao->get_thema_by_prefix( $code, 1 );
			$thema_suggestions[] = array(
				'code'         => $code,
				'description'  => ! empty( $rows ) ? $rows[0]['description'] : '',
				'notes'        => ! empty( $rows ) ? $rows[0]['notes'] : '',
				'is_qualifier' => 0,
			);
		}

		return array(
			'bisac' => $bisac_suggestions,
			'thema' => $thema_suggestions,
		);
	}

	/**
	 * Resolve a genre slug to its display label.
	 *
	 * Used by the CLASS-5 save handler to resolve slugs to taxonomy term names
	 * for the mbs_genre taxonomy sync (CLASS-8).
	 *
	 * @param string $slug Genre slug (e.g. "fiction-romance-suspense").
	 * @return string Display label, or an ucwords-formatted fallback from the slug.
	 */
	public function get_label_for_slug( string $slug ): string {
		$map   = $this->load_map();
		$genre = $map[ $slug ] ?? array();
		if ( ! empty( $genre['label'] ) ) {
			return $genre['label'];
		}
		// Fallback: humanise the slug.
		return ucwords( str_replace( '-', ' ', $slug ) );
	}

	// -------------------------------------------------------------------------
	// Static data methods (called from enqueue_assets + render)
	// -------------------------------------------------------------------------

	/**
	 * Return the 6 top-level groups, each with a label and ordered L1 category list.
	 *
	 * Public static so MBS_Metabox_Classification::enqueue_assets() and render()
	 * can call it without constructing an engine instance.
	 *
	 * @return array<string, array{ label: string, categories: string[] }>
	 */
	public static function get_group_map(): array {
		return array(
			'fiction'      => array(
				'label'      => 'FICTION',
				'categories' => array( 'FICTION', 'JUVENILE FICTION', 'YOUNG ADULT FICTION', 'COMICS & GRAPHIC NOVELS' ),
			),
			'nonfiction'   => array(
				'label'      => 'NONFICTION',
				'categories' => array( 'BIOGRAPHY & AUTOBIOGRAPHY', 'HISTORY', 'JUVENILE NONFICTION', 'YOUNG ADULT NONFICTION', 'POLITICAL SCIENCE', 'SOCIAL SCIENCE', 'TRUE CRIME' ),
			),
			'religion'     => array(
				'label'      => 'RELIGION',
				'categories' => array( 'BIBLES', 'MIND, BODY, SPIRIT', 'RELIGION' ),
			),
			'professional' => array(
				'label'      => 'PROFESSIONAL',
				'categories' => array( 'ARCHITECTURE', 'BUSINESS & ECONOMICS', 'COMPUTERS', 'EDUCATION', 'LAW', 'LANGUAGE ARTS & DISCIPLINES', 'LANGUAGE STUDY', 'MATHEMATICS', 'MEDICAL', 'PHILOSOPHY', 'PSYCHOLOGY', 'SCIENCE', 'STUDY AIDS', 'TECHNOLOGY & ENGINEERING' ),
			),
			'hobbies'      => array(
				'label'      => 'HOBBIES & LEISURE',
				'categories' => array( 'ANTIQUES & COLLECTIBLES', 'COOKING', 'CRAFTS & HOBBIES', 'DESIGN', 'FAMILY & RELATIONSHIPS', 'GAMES & ACTIVITIES', 'GARDENING', 'HEALTH & FITNESS', 'HOUSE & HOME', 'NATURE', 'PETS', 'PHOTOGRAPHY', 'SELF-HELP', 'SPORTS & RECREATION', 'TRANSPORTATION', 'TRAVEL' ),
			),
			'general'      => array(
				'label'      => 'GENERAL',
				'categories' => array( 'ART', 'DRAMA', 'HUMOR', 'LITERARY COLLECTIONS', 'LITERARY CRITICISM', 'MUSIC', 'PERFORMING ARTS', 'POETRY', 'REFERENCE' ),
			),
		);
	}

	/**
	 * Build the L1 -> sorted L2 genre list from the bundled bisac-codes.json.
	 *
	 * Passed via wp_localize_script to populate the Layer 2 genre chip grid
	 * in the browser without an AJAX round-trip.
	 *
	 * @return array<string, string[]>
	 */
	public static function build_l2map(): array {
		$path = MBS_DIR . 'data/bisac-codes.json';
		if ( ! file_exists( $path ) ) {
			return array();
		}
		$codes = json_decode( (string) file_get_contents( $path ), true );
		if ( ! is_array( $codes ) ) {
			return array();
		}
		$map = array();
		foreach ( $codes as $entry ) {
			if ( ! isset( $entry['description'] ) ) {
				continue;
			}
			$parts = explode( ' / ', $entry['description'] );
			if ( count( $parts ) < 2 ) {
				continue;
			}
			$l1 = trim( $parts[0] );
			$l2 = trim( $parts[1] );
			if ( ! isset( $map[ $l1 ] ) ) {
				$map[ $l1 ] = array();
			}
			if ( ! in_array( $l2, $map[ $l1 ], true ) ) {
				$map[ $l1 ][] = $l2;
			}
		}
		foreach ( $map as &$l2s ) {
			sort( $l2s );
		}
		unset( $l2s );
		return $map;
	}

	// -------------------------------------------------------------------------
	// Private helpers
	// -------------------------------------------------------------------------

	/**
	 * Load and memoize the genre map from data/genre-map.json.
	 *
	 * Returns an empty array if the file does not exist (pre-curation state).
	 *
	 * @return array<string, array{ label: string, category: string, bisac_codes: string[], thema_prefix_rules: string[] }>
	 */
	private function load_map(): array {
		if ( null !== $this->map ) {
			return $this->map;
		}
		$path = MBS_DIR . 'data/genre-map.json';
		if ( ! file_exists( $path ) ) {
			$this->map = array();
			return $this->map;
		}
		$decoded = json_decode( (string) file_get_contents( $path ), true );
		$this->map = is_array( $decoded ) ? $decoded : array();
		return $this->map;
	}
}
