<?php
/**
 * PK Core - the shared Site Profile vocabularies.
 *
 * ONE definition of every closed list the profile uses, so the wizard, the full
 * form and every consuming plugin agree on both the stored slug and the label
 * shown to the operator. Before this existed the same lists were declared
 * separately in each plugin's own settings view, which is how the estate ended
 * up storing "Human Resources", "b2b_publication" and "B2B trade publication"
 * as three answers to one question.
 *
 * ADOPTED WHOLESALE FROM INSIGHT ENGINE (owner decision, 2026-08-25 -
 * PK-CORE-SPEC-02 section 5b). These slugs are Insight Engine's existing values,
 * verbatim, because:
 *
 *   1. They are live on all five estate sites; the core's own vocabulary was
 *      populated on three. Adopting migrates the store that is actually full.
 *   2. It DELETES PKCORE_Legacy_Profile::goal_map(), which collapsed eleven goal
 *      slugs into three - advertising, advertiser_results, leads_enquiries,
 *      ecommerce and donations ALL became "monetise". Five distinct commercial
 *      goals became one token on every read.
 *   3. It lets PKCORE_Mirror project goals 1:1 in both directions. Under two
 *      vocabularies it could not project them at all.
 *   4. The vocabulary carries product meaning: goals are eleven rather than four
 *      BECAUSE each maps to what an audit weights. A coarser core list would
 *      discard that on every write.
 *
 * Slugs are STORAGE and are immutable once shipped - a slug change silently
 * orphans stored answers on every live site. Labels are display and may be
 * edited freely.
 *
 * @package PublisherKits\Core
 * @since   0.10.0
 */

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

if ( ! class_exists( 'PKCORE_Vocab' ) ) {

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

		/**
		 * What kind of publication this is.
		 *
		 * @return array<string,string> slug => label.
		 */
		public static function site_types() {
			return array(
				'b2b_publication'   => __( 'B2B trade publication / industry news', 'publisherkits-socialpromoter' ),
				'consumer_magazine' => __( 'Consumer magazine or lifestyle publication', 'publisherkits-socialpromoter' ),
				'blog'              => __( 'Personal blog or expert blog', 'publisherkits-socialpromoter' ),
				'news'              => __( 'News site or local news', 'publisherkits-socialpromoter' ),
				'corporate'         => __( 'Corporate or company website with content', 'publisherkits-socialpromoter' ),
				'ecommerce'         => __( 'E-commerce site with editorial content', 'publisherkits-socialpromoter' ),
				'charity_ngo'       => __( 'Charity, NGO, or non-profit', 'publisherkits-socialpromoter' ),
				'education'         => __( 'Education or learning resource', 'publisherkits-socialpromoter' ),
				'community'         => __( 'Community or membership site', 'publisherkits-socialpromoter' ),
				'portfolio'         => __( 'Portfolio or personal brand site', 'publisherkits-socialpromoter' ),
				'hobbyist'          => __( 'Hobbyist or enthusiast site', 'publisherkits-socialpromoter' ),
				'other'             => __( 'Other', 'publisherkits-socialpromoter' ),
			);
		}

		/**
		 * Publication goals.
		 *
		 * The hint is not decoration: it states which signals an audit weights
		 * for that goal, and is the reason this list is eleven values rather
		 * than a tidier four.
		 *
		 * @return array<string,array{label:string,hint:string}>
		 */
		public static function goals() {
			return array(
				'organic_traffic'    => array(
					'label' => __( 'Grow organic search traffic', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Content volume, keyword coverage and SEO health matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'email_subscribers'  => array(
					'label' => __( 'Build an email audience', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Content quality, excerpt coverage and publishing consistency matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'leads_enquiries'    => array(
					'label' => __( 'Generate leads or enquiries', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Author credibility, content depth and conversion signals matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'advertising'        => array(
					'label' => __( 'Maximise advertising or sponsorship revenue', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Traffic volume, content categorisation and tag taxonomy quality matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'advertiser_results' => array(
					'label' => __( 'Deliver results for advertisers and sponsors', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Audience quality, topical authority and content credibility matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'brand_awareness'    => array(
					'label' => __( 'Build brand or personal brand awareness', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Social promotion rate, author profiles and content consistency matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'community'          => array(
					'label' => __( 'Build a community or membership', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Engagement signals, publishing frequency and audience alignment matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'ecommerce'          => array(
					'label' => __( 'Drive product or service sales', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Internal linking, content-product alignment and search visibility matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'donations'          => array(
					'label' => __( 'Drive donations or fundraising', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Trust signals, author credibility and audience alignment matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'education'          => array(
					'label' => __( 'Educate or inform readers', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Content depth, tag taxonomy and structured navigation matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
				'events'             => array(
					'label' => __( 'Drive event attendance or bookings', 'publisherkits-socialpromoter' ),
					'hint'  => __( 'Social promotion, content timeliness and audience reach matter most for this goal.', 'publisherkits-socialpromoter' ),
				),
			);
		}

		/**
		 * Reader seniority bands.
		 *
		 * @return array<string,string> slug => label.
		 */
		public static function seniority() {
			return array(
				'general_public'  => __( 'General public / consumers', 'publisherkits-socialpromoter' ),
				'hobbyists'       => __( 'Hobbyists or enthusiasts', 'publisherkits-socialpromoter' ),
				'students'        => __( 'Students or learners', 'publisherkits-socialpromoter' ),
				'early_career'    => __( 'Early-career professionals', 'publisherkits-socialpromoter' ),
				'practitioners'   => __( 'Practitioners / individual contributors', 'publisherkits-socialpromoter' ),
				'managers'        => __( 'Managers and team leads', 'publisherkits-socialpromoter' ),
				'senior_leaders'  => __( 'Senior leaders and directors', 'publisherkits-socialpromoter' ),
				'c_level'         => __( 'C-level executives', 'publisherkits-socialpromoter' ),
				'business_owners' => __( 'Business owners and founders', 'publisherkits-socialpromoter' ),
				'academics'       => __( 'Academics or researchers', 'publisherkits-socialpromoter' ),
				'healthcare'      => __( 'Healthcare professionals', 'publisherkits-socialpromoter' ),
				'educators'       => __( 'Teachers or educators', 'publisherkits-socialpromoter' ),
				'retirees'        => __( 'Retirees or older adults', 'publisherkits-socialpromoter' ),
				'mixed'           => __( 'Mixed - no specific profile', 'publisherkits-socialpromoter' ),
			);
		}

		/**
		 * How the publication is funded.
		 *
		 * GROUNDED IN REAL REVENUE ACCOUNTS, not invented. Derived from the Xero
		 * income accounts of Black and White Trading Ltd (line of business:
		 * "Conferences, Training, Publishing"), read 2026-08-25 across the
		 * preceding two years of trading - GBP 933k of income.
		 *
		 * The first draft of this list was written from imagination and got the
		 * shape of the business wrong. Measured, by size:
		 *
		 *   Advertising Sales - Print          345k   MISSING from the draft
		 *   Solus Emails Campaigns             268k   conflated with newsletter ads
		 *   Advertising Sales - Online         121k
		 *   Webinar                             81k   buried inside "events"
		 *   Content Creation + Marketing        91k   called "advertorial"
		 *   Site Sponsorships                   12k
		 *   Advertising in emails                9k
		 *   Training Live / On Demand            4k   MISSING
		 *   Book & Guide Sales                   1k   MISSING
		 *
		 * PRINT AND SOLUS EMAIL ALONE ARE 66% OF INCOME and neither had its own
		 * entry. Solus is separated from newsletter advertising because the ledger
		 * separates them and the gap between them is thirtyfold - collapsing the
		 * two would have hidden the single largest digital product behind the
		 * smallest.
		 *
		 * Zero-balance accounts that nonetheless exist in the chart of accounts
		 * are included, because a chart of accounts records what a publisher SELLS
		 * even in a period when it sold none: Listings, Affiliate Campaigns,
		 * Advertising + Lead Generation, Reader Paid For, Event Sponsorships,
		 * Ticket Sales, Event Recordings, Event Management Fees, Data Sales,
		 * Consultancy Fees.
		 *
		 * `awards` is the exception - it is NOT a Xero income account. Retained on
		 * the owner's instruction; note there is a second Xero organisation in the
		 * group, so it may be booked there.
		 *
		 * @return array<string,string> slug => label.
		 */
		public static function commercial_models() {
			return array(
				'print_ads'           => __( 'Print or magazine advertising', 'publisherkits-socialpromoter' ),
				'display_advertising' => __( 'Display advertising on the site', 'publisherkits-socialpromoter' ),
				'programmatic'        => __( 'Programmatic advertising', 'publisherkits-socialpromoter' ),
				'solus_email'         => __( 'Solus or dedicated email campaigns', 'publisherkits-socialpromoter' ),
				'newsletter_ads'      => __( 'Advertising within newsletters', 'publisherkits-socialpromoter' ),
				'content_marketing'   => __( 'Content marketing, advertorial and sponsored content', 'publisherkits-socialpromoter' ),
				'sponsorship'         => __( 'Site or section sponsorship', 'publisherkits-socialpromoter' ),
				'webinars'            => __( 'Webinars', 'publisherkits-socialpromoter' ),
				'events'              => __( 'Events - tickets, sponsorship or management', 'publisherkits-socialpromoter' ),
				'awards'              => __( 'Awards programmes', 'publisherkits-socialpromoter' ),
				'training'            => __( 'Training, live or on demand', 'publisherkits-socialpromoter' ),
				'listings'            => __( 'Job listings, directory or classified listings', 'publisherkits-socialpromoter' ),
				'lead_generation'     => __( 'Lead generation for advertisers', 'publisherkits-socialpromoter' ),
				'publications'        => __( 'Books, guides or reports sold', 'publisherkits-socialpromoter' ),
				'subscriptions'       => __( 'Reader subscriptions or membership', 'publisherkits-socialpromoter' ),
				'affiliate'           => __( 'Affiliate revenue', 'publisherkits-socialpromoter' ),
				'data_sales'          => __( 'Data or list sales', 'publisherkits-socialpromoter' ),
				'consultancy'         => __( 'Consultancy or services', 'publisherkits-socialpromoter' ),
				'grants_donations'    => __( 'Grants or donations', 'publisherkits-socialpromoter' ),
				'none'                => __( 'Not commercially funded', 'publisherkits-socialpromoter' ),
			);
		}

		/**
		 * Content formats the publication produces.
		 *
		 * NOT A FIXED LIST, AND CORE MUST NOT INVENT ONE.
		 *
		 * A declared format is only worth capturing if it can be compared with a
		 * measured one, and the measured vocabulary belongs to the Content
		 * Classifier - where it is defined PER PACK, not globally. Measured on a
		 * live site 2026-08-25: one sector pack declares eleven content types
		 * (news, analysis, feature, opinion, interview, how-to, research,
		 * whitepaper, podcast, sponsored, campaign), and a portfolio running four
		 * different packs would find any list hardcoded here wrong on most of
		 * them.
		 *
		 * A first draft of this method DID hardcode twelve values. It omitted
		 * `report`, `roundup`, `guide` and `event` - two of which are present in
		 * live classification data. A publisher could not have declared a format
		 * the Classifier actively measures, so the gap report would have shown
		 * those as undeclared coverage forever: a false finding manufactured by
		 * the vocabulary itself.
		 *
		 * Worse, the stored data does not match the pack either. The same site
		 * stores `announcement` and `event`, neither of which is in that pack's
		 * vocabulary, while `campaign` is declared and never used. So even the
		 * pack list is not a safe source without the Classifier reconciling it.
		 *
		 * Until `PKCL_Read` exposes the vocabulary through its public contract -
		 * requested, not yet built - this returns EMPTY, and the Site Profile
		 * simply does not offer the question. An unasked question is honest; a
		 * question whose answers cannot match what is measured is not.
		 *
		 * @return array<string,string> slug => label.
		 */
		public static function formats() {
			/**
			 * Filter: the content-format vocabulary offered on the Site Profile.
			 *
			 * EMPTY BY DEFAULT, DELIBERATELY. See the docblock above - core cannot
			 * know this list, and inventing one produces false findings.
			 *
			 * @since 0.10.0
			 * @param array<string,string> $formats slug => label.
			 */
			$formats = apply_filters( 'pkcore_format_vocabulary', array() );
			if ( ! is_array( $formats ) ) {
				return array();
			}

			$out = array();
			foreach ( $formats as $slug => $label ) {
				$slug = strtolower( trim( (string) $slug ) );
				if ( '' !== $slug ) {
					$out[ $slug ] = (string) $label;
				}
			}
			return $out;
		}

		/**
		 * Who writes the content.
		 *
		 * @return array<string,string> slug => label.
		 */
		public static function content_sources() {
			return array(
				'staff'        => __( 'Staff writers', 'publisherkits-socialpromoter' ),
				'freelance'    => __( 'Commissioned freelancers', 'publisherkits-socialpromoter' ),
				'contributors' => __( 'Unpaid expert contributors', 'publisherkits-socialpromoter' ),
				'pr'           => __( 'Press releases and PR submissions', 'publisherkits-socialpromoter' ),
				'suppliers'    => __( 'Suppliers or vendors direct (not via a PR agency)', 'publisherkits-socialpromoter' ),
				'syndicated'   => __( 'Syndicated or licensed content', 'publisherkits-socialpromoter' ),
				'ai_assisted'  => __( 'AI-assisted drafting', 'publisherkits-socialpromoter' ),
			);
		}

		/**
		 * Distribution channels.
		 *
		 * @return array<string,string> slug => label.
		 */
		public static function channels() {
			return array(
				'onsite'        => __( 'The website itself', 'publisherkits-socialpromoter' ),
				'newsletter'    => __( 'Editorial newsletter', 'publisherkits-socialpromoter' ),
				'product-email' => __( 'Product or solus email', 'publisherkits-socialpromoter' ),
				'social'        => __( 'Social platforms', 'publisherkits-socialpromoter' ),
				'av'            => __( 'Audio and video - podcast, webinar, vlog', 'publisherkits-socialpromoter' ),
				'print'         => __( 'Print edition', 'publisherkits-socialpromoter' ),
			);
		}

		/**
		 * Whether a value is in a given list. Used by sanitisation so an unknown
		 * slug is dropped rather than stored - a stored slug with no label
		 * renders as a blank option and looks like data loss.
		 *
		 * @param string $value Slug to test.
		 * @param string $list  Vocabulary name: site_types|goals|seniority|
		 *                      commercial_models|formats|content_sources|channels.
		 * @return bool
		 */
		public static function is_valid( $value, $list ) {
			$options = self::options( $list );
			return array_key_exists( (string) $value, $options );
		}

		/**
		 * A vocabulary as a flat slug => label map, whichever shape it is stored in.
		 *
		 * @param string $list Vocabulary name.
		 * @return array<string,string>
		 */
		public static function options( $list ) {
			switch ( (string) $list ) {
				case 'site_types':
					return self::site_types();
				case 'goals':
					$out = array();
					foreach ( self::goals() as $slug => $spec ) {
						$out[ $slug ] = $spec['label'];
					}
					return $out;
				case 'seniority':
					return self::seniority();
				case 'commercial_models':
					return self::commercial_models();
				case 'formats':
					return self::formats();
				case 'content_sources':
					return self::content_sources();
				case 'channels':
					return self::channels();
				default:
					return array();
			}
		}

		/**
		 * Keep only values present in a vocabulary, de-duplicated and re-indexed.
		 *
		 * @param mixed  $values Raw value or list.
		 * @param string $list   Vocabulary name.
		 * @return string[]
		 */
		public static function filter_list( $values, $list ) {
			$values = is_array( $values ) ? $values : array( $values );
			$out    = array();
			foreach ( $values as $value ) {
				$value = sanitize_key( (string) $value );
				if ( '' !== $value && self::is_valid( $value, $list ) ) {
					$out[] = $value;
				}
			}
			return array_values( array_unique( $out ) );
		}
	}
}
