<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * The site's inventory, for the AI.
 *
 * The template engine ALREADY accepts any post type in a loop (see
 * {@see AIPB_Template_Engine::query_posts}): the type is not allow-listed, only the orderby is. The
 * bottleneck was never permission, it was KNOWLEDGE: without knowing that this site has "product",
 * "portfolio" or a menu called "primary", the model writes invented links and fake grids instead of
 * hooking into the real data.
 *
 * This class lists what is really there — post types with how much content, their taxonomies with a
 * few sample terms, the registered menus, the installed shortcodes — as DATA.
 *
 * THE PROSE IS WRITTEN BY THE SERVICE. The site is the only one that can know what exists, but HOW
 * that gets told to a model changes often, and while the plugin composed it every improvement
 * needed an update on every installed site. That was the point of the migration: see
 * `Aura\Prompt\Inventory` on the server, and the `section-inventario` parity case that pins it.
 */
class AIPB_Site_Context {

	const DATA_KEY = 'aipb_site_inventory';

	/** Five minutes: the counts change, but not on every generation. */
	const TTL = 300;

	/** How many shortcode names are listed before it becomes noise. */
	const SHORTCODE_CAP = 60;

	/** How many sample terms are given per taxonomy. */
	const TERM_SAMPLE = 5;

	/**
	 * The inventory as DATA: post types with their counts and taxonomies, menus, shortcodes.
	 *
	 * @return array{types:array,menus:array,shortcodes:array}
	 */
	public static function data() {
		$cached = get_transient( self::DATA_KEY );
		if ( is_array( $cached ) ) {
			return $cached;
		}

		$out = array(
			'types'      => self::types_data(),
			'menus'      => self::menus_data(),
			'shortcodes' => self::shortcodes_data(),
		);

		set_transient( self::DATA_KEY, $out, self::TTL );

		return $out;
	}

	public static function flush() {
		delete_transient( self::DATA_KEY );
	}

	/* ----------------------------------- Types --------------------------------- */

	protected static function types_data() {
		$out = array();

		foreach ( self::listable_types() as $slug => $object ) {
			$counts    = wp_count_posts( $slug );
			$published = isset( $counts->publish ) ? (int) $counts->publish : 0;

			// An empty type is noise: the model cannot build anything on it.
			if ( ! $published ) {
				continue;
			}

			$out[] = array(
				'slug'         => $slug,
				'label'        => isset( $object->labels->name ) ? $object->labels->name : $slug,
				'count'        => $published,
				'extra_fields' => array_values( self::extra_fields( $slug ) ),
				'taxonomies'   => self::taxonomies_data( $slug ),
			);
		}

		return $out;
	}

	protected static function taxonomies_data( $post_type ) {
		$out = array();

		foreach ( get_object_taxonomies( $post_type, 'objects' ) as $taxonomy ) {
			if ( empty( $taxonomy->public ) ) {
				continue;
			}

			$terms = get_terms( array(
				'taxonomy'   => $taxonomy->name,
				'number'     => self::TERM_SAMPLE,
				'hide_empty' => true,
				'fields'     => 'names',
			) );

			$out[] = array(
				'slug'  => $taxonomy->name,
				'terms' => ( $terms && ! is_wp_error( $terms ) ) ? array_values( $terms ) : array(),
				// The TERM's fields, when declared: on an archive they print with term.acf_… .
				// Without saying so, the model guesses them — it writes term.acf_immagine where the
				// field is called `copertina` — and the placeholder comes out empty with nothing to
				// flag it. Same reason the content's fields are given, one level down.
				'fields' => self::term_fields( $taxonomy->name ),
			);
		}

		return $out;
	}

	/** Post types worth showing in a loop: public, and with content of their own. */
	protected static function listable_types() {
		$types   = get_post_types( array( 'public' => true ), 'objects' );
		$exclude = apply_filters( 'aipb_context_exclude_types', array( 'attachment', AIPB_CPT::POST_TYPE ) );

		foreach ( $exclude as $slug ) {
			unset( $types[ $slug ] );
		}

		return $types;
	}

	/* ----------------------------------- Menus --------------------------------- */

	/**
	 * Every real navigation source, and which one comes out with no attributes.
	 *
	 * A block theme (FSE) has neither registered locations nor, often, classic menus: the menu is a
	 * `wp_navigation` post. Listing only the locations, as it used to, on Twenty Twenty-Five means
	 * telling the model "there are no menus here" — and then it goes back to writing links by hand.
	 */
	protected static function menus_data() {
		if ( ! class_exists( 'AIPB_Nav' ) ) {
			return array( 'sources' => array(), 'default' => array() );
		}

		$sources  = array();
		$assigned = (array) get_nav_menu_locations();

		// Block navigations (FSE themes): these are what the theme's header really shows.
		foreach ( AIPB_Nav::navigation_posts( 10 ) as $navigation ) {
			$sources[] = array(
				'kind'  => 'block',
				'ref'   => (int) $navigation->ID,
				'label' => (string) $navigation->post_title,
				'items' => count( AIPB_Nav::items_from_navigation_post( $navigation->ID ) ),
			);
		}

		// Locations registered by the theme (classic themes).
		$registered = function_exists( 'get_registered_nav_menus' ) ? get_registered_nav_menus() : array();
		foreach ( $registered as $location => $description ) {
			$menu = ! empty( $assigned[ $location ] ) ? wp_get_nav_menu_object( $assigned[ $location ] ) : false;

			$sources[] = array(
				'kind'     => 'location',
				'location' => (string) $location,
				'label'    => (string) $description,
				'menu'     => $menu ? (string) $menu->name : '',
				'items'    => $menu ? (int) $menu->count : 0,
			);
		}

		// Classic menus with no location: on a site migrated to a block theme they all end up here.
		$menus = wp_get_nav_menus();
		if ( $menus && ! is_wp_error( $menus ) ) {
			foreach ( $menus as $menu ) {
				if ( ! in_array( (int) $menu->term_id, array_map( 'intval', $assigned ), true ) ) {
					$sources[] = array(
						'kind'  => 'classic',
						'label' => (string) $menu->name,
						'items' => (int) $menu->count,
					);
				}
			}
		}

		$default = AIPB_Nav::resolve( array() );

		return array(
			'sources' => $sources,
			'default' => array(
				'kind'  => (string) $default['kind'],
				'label' => (string) $default['label'],
				'items' => AIPB_Nav::count_for( $default ),
			),
		);
	}

	/* -------------------------------- Shortcodes ------------------------------- */

	/**
	 * The shortcodes of the plugins installed here.
	 *
	 * Same story as the post types: the engine has always expanded them (see
	 * {@see AIPB_Shortcodes}), but the model could not know that THIS site has [contact-form-7] or
	 * [wpforms] — so it either invented one, which stays written on the page as text, or rebuilt by
	 * hand a feature the site already had. Listing them was all it took.
	 *
	 * Names only: whoever has the plugin knows the attributes, and the real case is a user pasting
	 * a ready-made shortcode into the request.
	 */
	protected static function shortcodes_data() {
		if ( ! class_exists( 'AIPB_Shortcodes' ) ) {
			return array( 'tags' => array(), 'more' => 0 );
		}

		$tags = AIPB_Shortcodes::registered();

		return array(
			'tags' => array_values( array_slice( $tags, 0, self::SHORTCODE_CAP ) ),
			'more' => max( 0, count( $tags ) - self::SHORTCODE_CAP ),
		);
	}

	/* ---------------------------------- Fields --------------------------------- */

	/**
	 * The fields declared on a TAXONOMY, with the name of the placeholder that prints them.
	 *
	 * @return list<string>
	 */
	public static function term_fields( $taxonomy ) {
		if ( ! class_exists( 'AIPB_Fields' ) ) {
			return array();
		}

		$out = array();
		foreach ( AIPB_Fields::for_taxonomy( $taxonomy ) as $field ) {
			$out[] = 'term.acf_' . $field['key'] . ' — ' . $field['label'] . ' (' . mb_strtolower( AIPB_Fields::type_label( $field['type'] ) ) . ')';
		}

		return $out;
	}

	/**
	 * The extra fields available on a post type's loop items.
	 *
	 * They are documented for the prompt and kept aligned with the enrichment done in
	 * {@see AIPB_Template_Engine}: one source, the 'aipb_loop_item_fields' filter.
	 */
	public static function extra_fields( $post_type ) {
		$fields = array();

		// The fields declared on this site. They have to be given with their exact name: otherwise
		// the model guesses them — it writes post.acf_prezzo where the field is called `costo` — and
		// the placeholder comes out empty with nothing to flag it.
		if ( class_exists( 'AIPB_Fields' ) ) {
			foreach ( AIPB_Fields::for_type( $post_type ) as $field ) {
				$fields[] = 'field.acf_' . $field['key'] . ' — ' . $field['label'] . ' (' . mb_strtolower( AIPB_Fields::type_label( $field['type'] ) ) . ')';
			}
		}

		if ( 'product' === $post_type && function_exists( 'wc_get_product' ) ) {
			$fields = array(
				'field.price (already formatted, with the currency symbol)',
				'field.price_regular',
				'field.price_sale',
				'field.on_sale ("1" when on sale)',
				'field.sku',
				'field.stock_status',
				'field.in_stock',
				'field.add_to_cart_url',
				'field.rating',
				'field.review_count',
			);
		}

		return apply_filters( 'aipb_loop_item_fields', $fields, $post_type );
	}
}
