<?php
/**
 * PK Core - the Site Profile question set, as data.
 *
 * ONE definition of every question, which the wizard and the full form both
 * render from. They are two views of the same list, not two forms: the wizard
 * shows the `wizard` tier, the full page shows everything. Defining them twice
 * is how the estate ended up with eleven capture surfaces asking overlapping
 * questions in incompatible shapes.
 *
 * Question set and tiering: PK-CORE-SPEC-02 section 3.
 *
 * PLUGIN-CONTRIBUTED QUESTIONS. A plugin may append its OWN questions through
 * the `pkcore_profile_questions` filter, so activating it can ask what it
 * uniquely needs without re-asking anything already answered. Two rules make
 * that safe, both enforced in contributed():
 *
 *   1. A contributed question MUST declare its own `storage` callback pair.
 *      Core never reads or writes a contributed question's data. Without that
 *      boundary the shared profile becomes the place plugins park their
 *      configuration, which is the original disease.
 *   2. A contributed question may NOT use a core key. Shadowing a shared answer
 *      with a plugin-local one would silently split the value in two.
 *
 * THE PROMOTION RULE (PK-CORE-SPEC-02 section 5a): a contributed question is
 * plugin-specific only until a SECOND plugin asks for the same thing. At that
 * point it is shared data wearing a local name and belongs in the core set.
 *
 * @package PublisherKits\Core
 * @since   0.10.0
 */

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

if ( ! class_exists( 'PKCORE_Questions' ) ) {

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

		/**
		 * The six sections, in the order they are asked.
		 *
		 * @return array<string,string> id => title.
		 */
		public static function sections() {
			return array(
				'about'      => __( 'What your site is about', 'publisherkits-socialpromoter' ),
				'audience'   => __( 'Who it is for', 'publisherkits-socialpromoter' ),
				'goals'      => __( 'What you are trying to achieve', 'publisherkits-socialpromoter' ),
				'vision'     => __( 'Where you want to get to', 'publisherkits-socialpromoter' ),
				'commercial' => __( 'How it is funded', 'publisherkits-socialpromoter' ),
				'voice'      => __( 'Voice and rules', 'publisherkits-socialpromoter' ),
			);
		}

		/**
		 * Every core question.
		 *
		 * `tier`  'wizard' (asked at first run) or 'full' (the page only).
		 * `type`  text | textarea | select | checkboxes | list | number | nested.
		 * `vocab` closed vocabulary name, for select/checkboxes.
		 * `ideal` whether this question can carry an aspiration (section 4).
		 *
		 * @return array<string,array>
		 */
		public static function core() {
			$declared = self::declared();
			return array_merge( $declared, self::vision( $declared ) );
		}

		/**
		 * The DECLARED axis - what the publisher says is true now.
		 *
		 * @return array<string,array>
		 */
		private static function declared() {
			return array(

				/* ---- 1. About ---------------------------------------------- */
				'description'            => array(
					'section' => 'about',
					'tier'    => 'wizard',
					'type'    => 'textarea',
					'label'   => __( 'Describe the publication in a paragraph', 'publisherkits-socialpromoter' ),
					'help'    => __( 'Plain English, as you would explain it to a new advertiser. This is the single most widely used answer here.', 'publisherkits-socialpromoter' ),
					'ideal'   => true,
				),
				'sector'                 => array(
					'section' => 'about',
					'tier'    => 'wizard',
					'type'    => 'text',
					'label'   => __( 'What sector do you publish in?', 'publisherkits-socialpromoter' ),
					// Examples are DELIBERATELY GENERIC and span a trade sector, a
					// profession and a special interest, so a publisher of any kind
					// recognises the granularity being asked for - a specific niche,
					// not "business" or "news". They previously named this vendor's
					// own three verticals, which told every other publisher the
					// product was built for somebody else's sector. Keep them neutral.
					'help'    => __( 'In your own words - "commercial property", "veterinary practice", "amateur astronomy".', 'publisherkits-socialpromoter' ),
				),
				'site_type'              => array(
					'section' => 'about',
					'tier'    => 'wizard',
					'type'    => 'select',
					'vocab'   => 'site_types',
					'label'   => __( 'What kind of site is it?', 'publisherkits-socialpromoter' ),
				),
				'topics_declared'        => array(
					'section' => 'about',
					'tier'    => 'wizard',
					'type'    => 'list',
					'label'   => __( 'What topics do you cover?', 'publisherkits-socialpromoter' ),
					'help'    => __( 'One per line. What you believe you cover - not what a tool has measured. The difference between the two is the point.', 'publisherkits-socialpromoter' ),
					'ideal'   => true,
				),
				'exclusions'             => array(
					'section' => 'about',
					'tier'    => 'wizard',
					'type'    => 'list',
					'label'   => __( 'What do you NOT cover?', 'publisherkits-socialpromoter' ),
					'help'    => __( 'One per line. Cheap to answer and unusually useful: it stops tools suggesting things you have deliberately ruled out, and it is the sharpest signal that content has drifted.', 'publisherkits-socialpromoter' ),
				),
				'formats'                => array(
					'section' => 'about',
					'tier'    => 'full',
					'type'    => 'checkboxes',
					'vocab'   => 'formats',
					'label'   => __( 'What formats do you publish?', 'publisherkits-socialpromoter' ),
					'ideal'   => true,
				),

				/* ---- 2. Audience ------------------------------------------- */
				'audience_readers'       => array(
					'section' => 'audience',
					'tier'    => 'wizard',
					'type'    => 'nested',
					'label'   => __( 'Who reads the site?', 'publisherkits-socialpromoter' ),
					'fields'  => array(
						'description' => array(
							'type'  => 'textarea',
							'tier'  => 'wizard',
							'label' => __( 'Describe your readers', 'publisherkits-socialpromoter' ),
						),
						'seniority'   => array(
							'type'  => 'checkboxes',
							'tier'  => 'full',
							'vocab' => 'seniority',
							'label' => __( 'Reader seniority', 'publisherkits-socialpromoter' ),
						),
						'sectors'     => array(
							'type'  => 'list',
							'tier'  => 'full',
							'label' => __( 'Reader sectors or disciplines', 'publisherkits-socialpromoter' ),
						),
					),
					'ideal'   => true,
				),
				'audience_buyers'        => array(
					'section' => 'audience',
					'tier'    => 'full',
					'type'    => 'nested',
					'label'   => __( 'Who buys from you?', 'publisherkits-socialpromoter' ),
					'help'    => __( 'Advertisers, sponsors, recruiters. Nothing in the suite modelled this before - every other question describes readers.', 'publisherkits-socialpromoter' ),
					'fields'  => array(
						'description'   => array( 'type' => 'textarea', 'tier' => 'full', 'label' => __( 'Describe your commercial customers', 'publisherkits-socialpromoter' ) ),
						'what_they_buy' => array( 'type' => 'textarea', 'tier' => 'full', 'label' => __( 'What do they buy?', 'publisherkits-socialpromoter' ) ),
						'good_outcome'  => array( 'type' => 'textarea', 'tier' => 'full', 'label' => __( 'What does a good result look like to them?', 'publisherkits-socialpromoter' ) ),
					),
					'ideal'   => true,
				),
				'audience_other'         => array(
					'section' => 'audience',
					'tier'    => 'full',
					'type'    => 'list',
					'label'   => __( 'Any other audiences?', 'publisherkits-socialpromoter' ),
					'help'    => __( 'One per line - contributors, members, regulators, partners.', 'publisherkits-socialpromoter' ),
				),
				'audience'               => array(
					'section' => 'audience',
					'tier'    => 'full',
					'type'    => 'select',
					'label'   => __( 'B2B, B2C or mixed?', 'publisherkits-socialpromoter' ),
					// NOTHING READS THIS. Kept because removing a v1 locked key is
					// constrained by SPEC-00 section 11, not because it earns its
					// place. The "feeds" panel will correctly show no consumers,
					// which is the signal to retire it at the next contract break.
					'help'    => __( 'Retained for compatibility. No PublisherKits plugin currently reads this.', 'publisherkits-socialpromoter' ),
				),

				/* ---- 3. Goals ---------------------------------------------- */
				'primary_goal'           => array(
					'section' => 'goals',
					'tier'    => 'wizard',
					'type'    => 'select',
					'vocab'   => 'goals',
					'label'   => __( 'What is this publication mainly trying to achieve?', 'publisherkits-socialpromoter' ),
					'help'    => __( 'One answer. It changes what an audit weights, so pick the one you would defend.', 'publisherkits-socialpromoter' ),
					'ideal'   => true,
				),
				'secondary_goals'        => array(
					'section' => 'goals',
					'tier'    => 'full',
					'type'    => 'checkboxes',
					'vocab'   => 'goals',
					'label'   => __( 'Up to two secondary goals', 'publisherkits-socialpromoter' ),
					'ideal'   => true,
				),
				'capacity'               => array(
					'section' => 'goals',
					'tier'    => 'full',
					'type'    => 'nested',
					'label'   => __( 'What can you actually produce?', 'publisherkits-socialpromoter' ),
					'help'    => __( 'Capacity is what makes a recommendation actionable. "Publish three times more" is noise to a one-person desk.', 'publisherkits-socialpromoter' ),
					'fields'  => array(
						'posts_per_week_target' => array( 'type' => 'number', 'tier' => 'full', 'label' => __( 'Target posts per week', 'publisherkits-socialpromoter' ) ),
						'sources'               => array( 'type' => 'checkboxes', 'tier' => 'full', 'vocab' => 'content_sources', 'label' => __( 'Who writes it?', 'publisherkits-socialpromoter' ) ),
					),
					'ideal'   => true,
				),
				'targets'                => array(
					'section' => 'goals',
					'tier'    => 'full',
					'type'    => 'textarea',
					'label'   => __( 'Audience or engagement targets', 'publisherkits-socialpromoter' ),
					'help'    => __( 'Optional. Nothing reads this yet - it is captured for the strategy tools.', 'publisherkits-socialpromoter' ),
				),

				/* ---- 5. Commercial ----------------------------------------- */
				'commercial_model'       => array(
					'section' => 'commercial',
					'tier'    => 'full',
					'type'    => 'checkboxes',
					'vocab'   => 'commercial_models',
					'label'   => __( 'How is the publication funded?', 'publisherkits-socialpromoter' ),
					'ideal'   => true,
				),
				'highest_value_audience' => array(
					'section' => 'commercial',
					'tier'    => 'full',
					'type'    => 'nested',
					'label'   => __( 'Who is your highest-value audience?', 'publisherkits-socialpromoter' ),
					'help'    => __( 'The part of your audience that generates the most revenue - whoever that is for you: the readers advertisers pay to reach, the people who subscribe, or the ones who buy tickets, courses or products.', 'publisherkits-socialpromoter' ),
					'ideal'   => true,
					'fields'  => array(
						'who' => array( 'type' => 'textarea', 'tier' => 'full', 'label' => __( 'Who are they?', 'publisherkits-socialpromoter' ) ),
						'why' => array( 'type' => 'textarea', 'tier' => 'full', 'label' => __( 'Why are they the most valuable?', 'publisherkits-socialpromoter' ) ),
					),
				),
				'commercial_targets'     => array(
					'section' => 'commercial',
					'tier'    => 'full',
					'type'    => 'textarea',
					'label'   => __( 'Revenue or advertiser targets', 'publisherkits-socialpromoter' ),
					'help'    => __( 'Optional. Nothing reads this yet - it is captured for the strategy tools.', 'publisherkits-socialpromoter' ),
				),
				'peers'                  => array(
					'section' => 'commercial',
					'tier'    => 'full',
					'type'    => 'list',
					'label'   => __( 'Who do you consider your competitors?', 'publisherkits-socialpromoter' ),
					'help'    => __( 'One per line. Compared against the peers Competitor Intelligence measures.', 'publisherkits-socialpromoter' ),
				),

				/* ---- 6. Voice ---------------------------------------------- */
				'house_style'            => array(
					'section' => 'voice',
					'tier'    => 'wizard',
					'type'    => 'nested',
					'label'   => __( 'How should we write for you?', 'publisherkits-socialpromoter' ),
					'fields'  => array(
						'language'   => array( 'type' => 'text', 'tier' => 'wizard', 'label' => __( 'Writing language', 'publisherkits-socialpromoter' ) ),
						'tone_notes' => array( 'type' => 'textarea', 'tier' => 'full', 'label' => __( 'Tone notes', 'publisherkits-socialpromoter' ) ),
					),
				),
				'editorial_rules'        => array(
					'section'         => 'voice',
					'tier'            => 'wizard',
					'type'            => 'list',
					'label'           => __( 'Rules every tool must follow', 'publisherkits-socialpromoter' ),
					'help'            => __( 'One per line - spellings, phrases to avoid, things never to say. These apply to EVERYTHING. Rules for one output format belong in the plugin that produces it.', 'publisherkits-socialpromoter' ),
					// At first run this is asked as ONE question with `context`,
					// under a single "How should we write for you?" heading.
					// Nobody arriving at a setup wizard distinguishes "brand
					// voice" from "house rules", and asking twice invites the
					// same answer typed twice. They are separate fields on the
					// full page, where the distinction is worth making.
					'wizard_captures' => array( 'context' ),
				),
				'context'                => array(
					'section' => 'voice',
					// `full` so it is not counted twice: the wizard reaches it
					// through editorial_rules' wizard_captures, above.
					'tier'    => 'full',
					'type'    => 'textarea',
					'label'   => __( 'Anything else about your voice', 'publisherkits-socialpromoter' ),
					'help'    => __( 'Free text, passed to AI tools as publication context.', 'publisherkits-socialpromoter' ),
				),
				'keywords'               => array(
					'section' => 'voice',
					'tier'    => 'full',
					'type'    => 'list',
					'label'   => __( 'Target keywords', 'publisherkits-socialpromoter' ),
					'help'    => __( 'One per line. Terms you are TARGETING, not terms you already rank for.', 'publisherkits-socialpromoter' ),
					'ideal'   => true,
				),
				'channels'               => array(
					'section' => 'voice',
					'tier'    => 'full',
					'type'    => 'checkboxes',
					'vocab'   => 'channels',
					'label'   => __( 'Where does the content go?', 'publisherkits-socialpromoter' ),
				),
			);
		}

		/**
		 * The IDEAL axis - section 4, "where you want to get to".
		 *
		 * BUILT FROM THE DECLARED SET, never hand-listed. Every question that can
		 * carry an aspiration contributes a mirrored field with the SAME type and
		 * vocabulary, so an ideal can never hold a shape its declared twin could
		 * not - and adding an ideal-capable question later needs no second edit
		 * here. A hand-written mirror is a list that drifts.
		 *
		 * SPARSE BY DESIGN. The operator answers only where the desired state
		 * DIFFERS from what they said above. A blank means "no change intended",
		 * which is a real answer and not an unanswered question - so the set of
		 * fields carrying a value IS the strategy brief, enumerable and short.
		 *
		 * This section existed in the spec from the start and was never built.
		 * The page silently skips empty sections, so five of six rendered and
		 * looked complete while the whole aspiration axis was missing.
		 *
		 * @param array $declared The declared question set.
		 * @return array<string,array>
		 */
		private static function vision( $declared ) {
			$fields = array();
			foreach ( $declared as $key => $spec ) {
				if ( empty( $spec['ideal'] ) ) {
					continue;
				}
				$fields[ $key ] = array(
					'type'  => $spec['type'],
					'tier'  => 'full',
					'label' => $spec['label'],
				);
				if ( ! empty( $spec['vocab'] ) ) {
					$fields[ $key ]['vocab'] = $spec['vocab'];
				}
				// A nested declared question contributes its own sub-fields, so an
				// ideal audience can differ in description, sectors or seniority
				// independently rather than as one lump.
				if ( ! empty( $spec['fields'] ) ) {
					$fields[ $key ]['fields'] = $spec['fields'];
				}
			}

			return array(
				'ideal'        => array(
					'section' => 'vision',
					'tier'    => 'full',
					'type'    => 'nested',
					'label'   => __( 'Where do you want to get to?', 'publisherkits-socialpromoter' ),
					'help'    => __( 'Answer only where your intention DIFFERS from what you said above. Leaving a field blank means "no change intended", which is a real answer - the fields you do fill in become your strategy brief.', 'publisherkits-socialpromoter' ),
					'fields'  => $fields,
				),
				'vision_notes' => array(
					'section' => 'vision',
					'tier'    => 'full',
					'type'    => 'textarea',
					'label'   => __( 'Anything else about where the publication is heading', 'publisherkits-socialpromoter' ),
					'help'    => __( 'Free text. Plans, pressures, a change of direction - context a strategy tool cannot infer from the numbers.', 'publisherkits-socialpromoter' ),
				),
			);
		}

		/**
		 * The wizard set - core questions tiered `wizard`.
		 *
		 * @return array<string,array>
		 */
		public static function wizard() {
			$out = array();
			foreach ( self::core() as $key => $spec ) {
				if ( 'wizard' === $spec['tier'] ) {
					$out[ $key ] = $spec;
				}
			}
			return $out;
		}

		/**
		 * Plugin-contributed questions, validated.
		 *
		 * A malformed or trespassing contribution is DROPPED rather than
		 * rendered, and the reason is surfaced under WP_DEBUG. Silently ignoring
		 * it would leave the contributing plugin believing it had asked.
		 *
		 * @return array<string,array>
		 */
		public static function contributed() {
			/**
			 * Filter: add plugin-specific Site Profile questions.
			 *
			 * @since 0.10.0
			 * @param array $questions handle-prefixed key => spec.
			 */
			$raw = apply_filters( 'pkcore_profile_questions', array() );
			if ( ! is_array( $raw ) ) {
				return array();
			}

			$core = self::core();
			$out  = array();

			foreach ( $raw as $key => $spec ) {
				$key    = (string) $key;
				$reason = '';

				if ( '' === $key || ! is_array( $spec ) ) {
					$reason = 'malformed entry';
				} elseif ( array_key_exists( $key, $core ) ) {
					// Rule 2: shadowing a shared key would split one answer in two.
					$reason = 'uses a core profile key; if two plugins need it, promote it to the core set instead';
				} elseif ( empty( $spec['plugin'] ) ) {
					$reason = 'does not name the contributing plugin';
				} elseif ( empty( $spec['storage'] ) || ! is_array( $spec['storage'] )
					|| empty( $spec['storage']['get'] ) || empty( $spec['storage']['set'] )
					|| ! is_callable( $spec['storage']['get'] ) || ! is_callable( $spec['storage']['set'] ) ) {
					// Rule 1: core never owns a contributed answer.
					$reason = 'does not declare its own storage get/set callbacks';
				}

				if ( '' !== $reason ) {
					if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
						// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
						error_log( sprintf( 'PKCORE_Questions: dropped contributed question "%s" - %s.', $key, $reason ) );
					}
					continue;
				}

				$spec['contributed'] = true;
				$spec['tier']        = isset( $spec['tier'] ) && 'wizard' === $spec['tier'] ? 'wizard' : 'full';
				$spec['section']     = 'plugin:' . sanitize_key( (string) $spec['plugin'] );
				$out[ $key ]         = $spec;
			}

			return $out;
		}

		/**
		 * Core plus contributed.
		 *
		 * @return array<string,array>
		 */
		public static function all() {
			return array_merge( self::core(), self::contributed() );
		}

		/**
		 * Questions in a section.
		 *
		 * @param string $section Section id.
		 * @return array<string,array>
		 */
		public static function in_section( $section ) {
			$out = array();
			foreach ( self::all() as $key => $spec ) {
				if ( (string) $section === $spec['section'] ) {
					$out[ $key ] = $spec;
				}
			}
			return $out;
		}
	}
}
