<?php
/**
 * PK Core - who reads the Site Profile, and what they do with it.
 *
 * The consumer matrix as DATA, so the Site Profile screen can tell the operator
 * which installed plugins each answer actually feeds, and what an answer would
 * unlock in a plugin they have not installed.
 *
 * WHY THIS IS DATA AND NOT PROSE ON THE PAGE. The panel is generated from this
 * map, so it is self-policing: a question that feeds nothing displays nothing,
 * which surfaces a dead question the moment it appears instead of letting it sit
 * there for a year. That is the failure this whole module exists to undo -
 * eleven capture surfaces accumulated because nothing ever showed that a field
 * had stopped earning its place.
 *
 * MEASURED VERSUS INTENDED. `consumes` is what a plugin reads TODAY - directly,
 * or through PKCORE_Mirror into a legacy option it already reads. `planned` is
 * what it is meant to read once built. ONLY `consumes` drives the panel.
 *
 * The split exists because the first draft of this file did not have it: every
 * v2 key was listed as consumed on the day it was created, when nothing read any
 * of them. The panel would have told the operator that `exclusions` feeds TagWise
 * and the Classifier, which was simply false - and false in the most damaging
 * direction, because it would have reported an answer as doing work while the
 * gap it was supposed to close stayed open. The rule below was already written
 * in this file at the time. Writing a rule is not keeping it.
 *
 * KEEPING IT HONEST. Entries here are derived from measured reads
 * (PK-CORE-SPEC-02 section 1), not from what a plugin might plausibly want.
 * Adding a key to a consumer's list without a real read is how the panel starts
 * lying, and a panel that overstates is worse than no panel - it tells the
 * operator their answer is doing work when it is not.
 *
 * @package PublisherKits\Core
 * @since   0.10.0
 */

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

if ( ! class_exists( 'PKCORE_Consumers' ) ) {

	/**
	 * Class PKCORE_Consumers
	 *
	 * @since 0.10.0
	 */
	class PKCORE_Consumers {

		/**
		 * Detected active handles, per request.
		 *
		 * @var array<string,bool>|null
		 */
		private static $active = null;

		/**
		 * The matrix.
		 *
		 * `file`     the FREE basename. The premium variant is derived, never
		 *            hand-listed - see is_active().
		 * `consumes` profile keys this plugin actually reads.
		 * `blurb`    what it does with them, for a plugin that is not installed.
		 *
		 * @return array
		 */
		public static function all() {
			return array(
				'insight-engine'          => array(
					'label'    => __( 'Insight Engine', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-insight-engine/publisherkits-insight-engine.php',
					'url'      => 'https://publisherkits.com/insight-engine',
					'blurb'    => __( 'Audits the publication against what you say it is, and reports where the two differ.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'description', 'site_type', 'audience_readers', 'primary_goal', 'secondary_goals', 'keywords', 'capacity' ),
					'planned'  => array( 'topics_declared', 'exclusions', 'peers', 'formats', 'commercial_model', 'audience_buyers', 'targets', 'commercial_targets', 'ideal', 'vision_notes', 'editorial_rules', 'channels', 'highest_value_audience' ),				),
				'socialpromoter'          => array(
					'label'    => __( 'SocialPromoter', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-socialpromoter/publisherkits-socialpromoter.php',
					'url'      => 'https://publisherkits.com/socialpromoter',
					'blurb'    => __( 'Writes social copy framed for your readers, in your house style.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'description', 'sector', 'audience_readers', 'primary_goal', 'secondary_goals', 'keywords', 'house_style', 'context', 'highest_value_audience' ),
					'planned'  => array( 'editorial_rules', 'topics_declared', 'exclusions', 'channels', 'audience_buyers', 'ideal' ),				),
				'excerptbuilder'          => array(
					'label'    => __( 'ExcerptBuilder', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-excerptbuilder/publisherkits-excerptbuilder.php',
					'url'      => 'https://publisherkits.com/excerptbuilder',
					'blurb'    => __( 'Writes excerpts aimed at the readers you describe, for the goal you set.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'description', 'site_type', 'audience_readers', 'primary_goal', 'highest_value_audience' ),
					'planned'  => array( 'editorial_rules', 'exclusions', 'topics_declared', 'sector' ),				),
				'emailbuilder'            => array(
					'label'    => __( 'EmailBuilder', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-email-builder/publisherkits-email-builder.php',
					'url'      => 'https://publisherkits.com/emailbuilder',
					'blurb'    => __( 'Writes newsletter copy in your language and house style.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'audience_readers', 'house_style' ),
					'planned'  => array( 'editorial_rules', 'description', 'sector', 'topics_declared', 'channels', 'primary_goal', 'highest_value_audience' ),				),
				'tagwise'                 => array(
					'label'    => __( 'TagWise', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-tagwise/tagwise.php',
					'url'      => 'https://publisherkits.com/tagwise',
					'blurb'    => __( 'Grounds tag suggestions in your sector and target vocabulary, and avoids what you do not cover.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'sector', 'keywords', 'house_style' ),
					'planned'  => array( 'topics_declared', 'exclusions', 'editorial_rules', 'description' ),				),
				'classifier'              => array(
					'label'    => __( 'Content Classifier', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-content-classifier/publisherkits-content-classifier.php',
					'url'      => 'https://publisherkits.com/classifier',
					'blurb'    => __( 'Anchors classification in your sector and publication context, and measures coverage against your declared topics.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'sector', 'context' ),
					'planned'  => array( 'topics_declared', 'exclusions', 'formats', 'description', 'audience_readers' ),				),
				'es-intelligence'         => array(
					'label'    => __( 'ES Intelligence', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-es-intelligence/publisherkits-es-intelligence.php',
					'url'      => 'https://publisherkits.com/es-intelligence',
					'blurb'    => __( 'Frames engagement reporting for your publication and what your advertisers buy.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'context' ),
					'planned'  => array( 'audience_buyers', 'commercial_model', 'commercial_targets', 'description', 'sector', 'audience_readers', 'primary_goal', 'editorial_rules', 'highest_value_audience' ),				),
				'competitor-intelligence' => array(
					'label'    => __( 'Competitor Intelligence', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-competitor-intelligence/publisherkits-competitor-intelligence.php',
					'url'      => 'https://publisherkits.com/competitor-intelligence',
					'blurb'    => __( 'Compares the peers you name against the ones it measures.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'sector', 'peers' ),
					'planned'  => array( 'audience_buyers', 'topics_declared', 'description', 'commercial_model' ),				),
				'newsdesk'                => array(
					'label'    => __( 'Newsdesk', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-newsdesk/publisherkits-newsdesk.php',
					'url'      => 'https://publisherkits.com/newsdesk',
					'blurb'    => __( 'Judges incoming stories against the topics and readers you describe.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'description', 'audience_readers' ),
					'planned'  => array( 'topics_declared', 'exclusions', 'editorial_rules', 'sector', 'audience_other', 'primary_goal', 'highest_value_audience' ),				),
				'biobuilder'              => array(
					'label'    => __( 'BioBuilder', 'publisherkits-socialpromoter' ),
					'file'     => 'publisherkits-biobuilder/publisherkits-biobuilder.php',
					'url'      => 'https://publisherkits.com/biobuilder',
					'blurb'    => __( 'Writes author biographies that place each writer in your publication.', 'publisherkits-socialpromoter' ),
					'consumes' => array( 'description' ),
					'planned'  => array( 'sector', 'audience_readers', 'editorial_rules', 'site_type' ),				),
			);
		}

		/**
		 * Every basename a handle may legitimately be installed under.
		 *
		 * Freemius installs a premium build into a DIFFERENT DIRECTORY - the
		 * folder gains a `-premium` suffix, the filename does not change. Derived
		 * rather than hand-listed, because hand-listing is exactly what failed in
		 * Insight Engine's own registry: it keyed on the free basename only, and
		 * on four of five estate sites reported the Classifier "not installed"
		 * while its premium build was active and answering. Every check gated on
		 * that handle silently could not run.
		 *
		 * @param string $file Registered basename.
		 * @return string[]
		 */
		private static function candidate_files( $file ) {
			$out   = array( $file );
			$slash = strpos( $file, '/' );
			if ( false === $slash ) {
				return $out;
			}
			$dir  = substr( $file, 0, $slash );
			$rest = substr( $file, $slash );
			if ( '-premium' !== substr( $dir, -8 ) ) {
				$out[] = $dir . '-premium' . $rest;
			}
			return $out;
		}

		/**
		 * Whether a consumer is active on this site.
		 *
		 * @param string $handle Consumer handle.
		 * @return bool
		 */
		public static function is_active( $handle ) {
			if ( null === self::$active ) {
				if ( ! function_exists( 'is_plugin_active' ) ) {
					require_once ABSPATH . 'wp-admin/includes/plugin.php';
				}
				self::$active = array();
				foreach ( self::all() as $key => $spec ) {
					$found = false;
					foreach ( self::candidate_files( $spec['file'] ) as $candidate ) {
						if ( is_plugin_active( $candidate ) ) {
							$found = true;
							break;
						}
					}
					self::$active[ $key ] = $found;
				}
			}
			return ! empty( self::$active[ (string) $handle ] );
		}

		/**
		 * Installed consumers that read a given profile key.
		 *
		 * Drives the "this feeds" line under each question.
		 *
		 * @param string $key Profile key.
		 * @return array<string,string> handle => label.
		 */
		public static function feeds( $key ) {
			$out = array();
			foreach ( self::all() as $handle => $spec ) {
				if ( in_array( (string) $key, $spec['consumes'], true ) && self::is_active( $handle ) ) {
					$out[ $handle ] = $spec['label'];
				}
			}
			return $out;
		}

		/**
		 * Consumers that WOULD read a given key but are not installed.
		 *
		 * Drives honest promotional copy: it is the same matrix, so it can only
		 * describe a capability the plugin genuinely has.
		 *
		 * @param string $key Profile key.
		 * @return array handle => array{label,blurb,url}.
		 */
		public static function could_feed( $key ) {
			$out = array();
			foreach ( self::all() as $handle => $spec ) {
				// `planned` counts here as well as `consumes`. It did not, and the
				// gap showed up the moment a v2 key was added: six plugins intend
				// to read highest_value_audience, none reads it yet, so on a site
				// where those plugins are not installed the question rendered
				// "Not currently used by any PublisherKits plugin" - the exact
				// discouraging state the third state was introduced to remove.
				// A plugin that plans to read a key genuinely WOULD use it, which
				// is what this line is for.
				$wanted = array_merge( $spec['consumes'], isset( $spec['planned'] ) ? $spec['planned'] : array() );
				if ( in_array( (string) $key, $wanted, true ) && ! self::is_active( $handle ) ) {
					$out[ $handle ] = array(
						'label' => $spec['label'],
						'blurb' => $spec['blurb'],
						'url'   => $spec['url'],
					);
				}
			}
			return $out;
		}

		/**
		 * Installed consumers that INTEND to read a key but do not yet.
		 *
		 * A third state, and it earns its place. Rendering the page against a
		 * real site showed TWELVE of twenty-two questions displaying "not used by
		 * any plugin" - including `exclusions` and `topics_declared`, the two the
		 * whole gap-reporting case rests on. Every one of them is a v2 key with
		 * no reader YET.
		 *
		 * "Not used" is true but corrosive: it tells the operator that more than
		 * half the form is pointless, and they would be right to stop filling it
		 * in. "Will be used by X" is a different claim and an honest one, PROVIDED
		 * the plan is real - which is why this reads from `planned`, a list that
		 * only gets an entry when a specific plugin is actually going to read it.
		 *
		 * It must never be conflated with feeds(). A key that is merely planned
		 * has no path to any consumer today, and the gap it is meant to close
		 * stays open until one is built.
		 *
		 * @param string $key Profile key.
		 * @return array<string,string> handle => label.
		 */
		public static function planned_for( $key ) {
			$out = array();
			foreach ( self::all() as $handle => $spec ) {
				$planned = isset( $spec['planned'] ) ? (array) $spec['planned'] : array();
				if ( in_array( (string) $key, $planned, true ) && self::is_active( $handle ) ) {
					$out[ $handle ] = $spec['label'];
				}
			}
			return $out;
		}

		/**
		 * Keys a consumer reads that are still EMPTY in the profile.
		 *
		 * This is what a new install should ask about: never re-collect an
		 * answered question, only surface the gaps that plugin actually cares
		 * about.
		 *
		 * @param string $handle Consumer handle.
		 * @return string[] Profile keys.
		 */
		public static function unanswered_for( $handle ) {
			$all = self::all();
			if ( ! isset( $all[ (string) $handle ] ) ) {
				return array();
			}
			$profile = PKCORE_Profile::all();
			$gaps    = array();
			foreach ( $all[ (string) $handle ]['consumes'] as $key ) {
				if ( self::is_empty_value( isset( $profile[ $key ] ) ? $profile[ $key ] : '' ) ) {
					$gaps[] = $key;
				}
			}
			return $gaps;
		}

		/**
		 * Whether a profile value counts as unanswered.
		 *
		 * Nested shapes are empty only when EVERY leaf is empty - a reader
		 * description with no seniority ticked is answered, not blank.
		 *
		 * @param mixed $value Value.
		 * @return bool
		 */
		private static function is_empty_value( $value ) {
			if ( is_array( $value ) ) {
				foreach ( $value as $leaf ) {
					if ( ! self::is_empty_value( $leaf ) ) {
						return false;
					}
				}
				return true;
			}
			return ( '' === trim( (string) $value ) || '0' === (string) $value );
		}
	}
}
