<?php
/**
 * Schema Architecture V2 install/settings defaults.
 *
 * Fresh installs record architecture/settings version, master schema output,
 * and identity defaults without seeding a full all-types `schemas` catalogue.
 *
 * @package SEOvaultSEO\Schema
 */

namespace SEOvaultSEO\Schema;

use SEOvaultSEO\Core\Options;

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

/**
 * Versioned schema architecture settings for safe new installs.
 */
class SchemaArchitectureSettings {

	/**
	 * Current schema architecture generation.
	 */
	const ARCHITECTURE_VERSION = 2;

	/**
	 * Current schema settings shape version (UI/storage contract).
	 */
	const SETTINGS_VERSION = 1;

	/**
	 * Option key (without seovaultseo_ prefix): architecture generation.
	 */
	const OPTION_ARCHITECTURE_VERSION = 'schema_architecture_version';

	/**
	 * Option key: settings shape version.
	 */
	const OPTION_SETTINGS_VERSION = 'schema_settings_version';

	/**
	 * Option key: publisher identity mode for automatic schema.
	 */
	const OPTION_PUBLISHER_IDENTITY = 'schema_publisher_identity';

	/**
	 * Option key: pending schema onboarding hint for later phases.
	 */
	const OPTION_ONBOARDING_PENDING = 'schema_onboarding_pending';

	/**
	 * Option key: optional automatic component toggles (V2).
	 */
	const OPTION_AUTOMATIC = 'schema_automatic_components';

	/**
	 * Option key: catalogue presentation preferences (V2, non-emission).
	 */
	const OPTION_CATALOGUE = 'schema_catalogue_presentation';

	/**
	 * Option key: monotonic settings revision for stale-write protection.
	 */
	const OPTION_SETTINGS_REVISION = 'schema_settings_revision';

	/**
	 * Allowed publisher identity modes.
	 *
	 * @var string[]
	 */
	const IDENTITY_MODES = array( 'organization', 'person', 'both' );

	/**
	 * Install safe V2 defaults for a fresh site (add-only; never overwrite).
	 *
	 * @return array<string,mixed> Exact option values written or already present for the V2 keys.
	 */
	public static function install_fresh_defaults() {
		self::add_option_once( self::OPTION_ARCHITECTURE_VERSION, self::ARCHITECTURE_VERSION );
		self::add_option_once( self::OPTION_SETTINGS_VERSION, self::SETTINGS_VERSION );
		self::add_option_once( self::OPTION_PUBLISHER_IDENTITY, 'organization' );
		self::add_option_once( self::OPTION_ONBOARDING_PENDING, 'yes' );

		if ( class_exists( __NAMESPACE__ . '\\SchemaOnboardingGuidance' ) ) {
			SchemaOnboardingGuidance::reset_notice_dismissal();
		}

		// Master schema output switch (may already be set by Activator defaults).
		if ( false === get_option( 'seovaultseo_schema_enabled', false ) ) {
			add_option( 'seovaultseo_schema_enabled', 'yes', '', false );
		}

		// Fresh V2 installs never need the legacy all-enabled → opt-in migration.
		if ( false === get_option( SchemaDefaultsMigration::MARKER_OPTION, false ) ) {
			add_option( SchemaDefaultsMigration::MARKER_OPTION, '1', '', false );
		}

		// Fresh V2 installs have nothing to migrate — stamp target so runtime flags default on.
		if ( class_exists( __NAMESPACE__ . '\\SchemaMigration' ) ) {
			self::add_option_once(
				SchemaMigration::OPTION_MIGRATION_VERSION,
				SchemaMigration::TARGET_MIGRATION_VERSION
			);
		}

		// Explicitly do not seed seovaultseo_schemas — catalogue stays code-defined.

		return self::get_install_state();
	}

	/**
	 * Snapshot of V2 install-related option state.
	 *
	 * @return array<string,mixed>
	 */
	public static function get_install_state() {
		return array(
			'architecture_version' => self::get_architecture_version(),
			'settings_version'     => self::get_settings_version(),
			'publisher_identity'   => self::get_publisher_identity(),
			'onboarding_pending'   => self::is_onboarding_pending(),
			'schema_enabled'       => (string) get_option( 'seovaultseo_schema_enabled', '' ),
			'schemas_option_set'   => false !== get_option( 'seovaultseo_schemas', false ),
			'defaults_migrated'    => (string) get_option( SchemaDefaultsMigration::MARKER_OPTION, '' ),
		);
	}

	/**
	 * Whether this site is on architecture generation 2+.
	 *
	 * @return bool
	 */
	public static function is_architecture_v2() {
		return self::get_architecture_version() >= self::ARCHITECTURE_VERSION;
	}

	/**
	 * Get the recorded architecture generation.
	 *
	 * @return int
	 */
	public static function get_architecture_version() {
		return (int) Options::get( self::OPTION_ARCHITECTURE_VERSION, 0 );
	}

	/**
	 * Get the recorded settings shape version.
	 *
	 * @return int
	 */
	public static function get_settings_version() {
		return (int) Options::get( self::OPTION_SETTINGS_VERSION, 0 );
	}

	/**
	 * Publisher identity for automatic foundational schema.
	 *
	 * @return string organization|person|both
	 */
	public static function get_publisher_identity() {
		$identity = Options::get( self::OPTION_PUBLISHER_IDENTITY, 'organization' );
		$identity = is_string( $identity ) ? strtolower( $identity ) : 'organization';
		if ( ! in_array( $identity, self::IDENTITY_MODES, true ) ) {
			return 'organization';
		}
		return $identity;
	}

	/**
	 * Persist publisher identity (allow-listed).
	 *
	 * @param string $identity Identity mode.
	 * @return bool
	 */
	public static function set_publisher_identity( $identity ) {
		$identity = is_string( $identity ) ? strtolower( $identity ) : '';
		if ( ! in_array( $identity, self::IDENTITY_MODES, true ) ) {
			return false;
		}
		return Options::set( self::OPTION_PUBLISHER_IDENTITY, $identity, false );
	}

	/**
	 * Whether schema onboarding still needs identity guidance later.
	 *
	 * @return bool
	 */
	public static function is_onboarding_pending() {
		return 'yes' === (string) Options::get( self::OPTION_ONBOARDING_PENDING, 'no' );
	}

	/**
	 * Optional automatic component toggles (safe-to-disable foundational pieces).
	 *
	 * @return array<string,bool>
	 */
	public static function get_automatic_components() {
		$defaults = SchemaSettingsSanitizer::default_automatic();
		$stored   = Options::get( self::OPTION_AUTOMATIC, array() );
		if ( ! is_array( $stored ) ) {
			return $defaults;
		}

		$merged = SchemaSettingsSanitizer::merge_with_current(
			array( 'automatic' => $stored ),
			array(
				'schema_enabled'     => 'yes',
				'publisher_identity' => 'organization',
				'automatic'          => $defaults,
				'catalogue'          => SchemaSettingsSanitizer::default_catalogue(),
			)
		);

		return $merged['automatic'];
	}

	/**
	 * Whether an optional automatic component is enabled.
	 *
	 * @param string $key Component key (breadcrumbs|search_action).
	 * @return bool
	 */
	public static function is_automatic_component_enabled( $key ) {
		$components = self::get_automatic_components();
		$key        = is_string( $key ) ? $key : '';
		if ( ! array_key_exists( $key, $components ) ) {
			return true;
		}
		return ! empty( $components[ $key ] );
	}

	/**
	 * Catalogue presentation preferences (never control emission).
	 *
	 * @return array{show_extension_types:bool,collapsed_sections:string[]}
	 */
	public static function get_catalogue_presentation() {
		$defaults = SchemaSettingsSanitizer::default_catalogue();
		$stored   = Options::get( self::OPTION_CATALOGUE, array() );
		if ( ! is_array( $stored ) ) {
			return $defaults;
		}

		$merged = SchemaSettingsSanitizer::merge_with_current(
			array( 'catalogue' => $stored ),
			array(
				'schema_enabled'     => 'yes',
				'publisher_identity' => 'organization',
				'automatic'          => SchemaSettingsSanitizer::default_automatic(),
				'catalogue'          => $defaults,
			)
		);

		return $merged['catalogue'];
	}

	/**
	 * Current settings revision for stale-write protection.
	 *
	 * @return int
	 */
	public static function get_settings_revision() {
		return max( 0, (int) Options::get( self::OPTION_SETTINGS_REVISION, 0 ) );
	}

	/**
	 * Snapshot of saveable V2 settings (UI + AJAX).
	 *
	 * @return array<string,mixed>
	 */
	public static function get_v2_settings_snapshot() {
		$enabled = Options::is_schema_enabled() ? 'yes' : 'no';

		return array(
			'schema_enabled'     => $enabled,
			'publisher_identity' => self::get_publisher_identity(),
			'automatic'          => self::get_automatic_components(),
			'catalogue'          => self::get_catalogue_presentation(),
			'settings_revision'  => self::get_settings_revision(),
		);
	}

	/**
	 * Persist a sanitized V2 settings partial with stale-revision protection.
	 *
	 * Never writes the legacy `schemas` catalogue.
	 *
	 * @param array $sanitized Sanitized settings from SchemaSettingsSanitizer.
	 * @param int   $expected_revision Client-supplied expected revision.
	 * @return array|\WP_Error Success payload or conflict/error.
	 */
	public static function save_v2_settings( array $sanitized, $expected_revision ) {
		$expected_revision = (int) $expected_revision;
		$current_revision  = self::get_settings_revision();

		if ( $expected_revision !== $current_revision ) {
			return new \WP_Error(
				'schema_settings_stale',
				__( 'Schema settings were updated elsewhere. Reload the page and try again.', 'seovault-ai' ),
				array(
					'status'            => 409,
					'settings_revision' => $current_revision,
					'settings'          => self::get_v2_settings_snapshot(),
				)
			);
		}

		$merged = SchemaSettingsSanitizer::merge_with_current(
			$sanitized,
			self::get_v2_settings_snapshot()
		);

		Options::set_schema_enabled( $merged['schema_enabled'] );
		self::set_publisher_identity( $merged['publisher_identity'] );
		Options::set( self::OPTION_AUTOMATIC, $merged['automatic'], false );
		Options::set( self::OPTION_CATALOGUE, $merged['catalogue'], false );

		if ( isset( $sanitized['publisher_identity'] ) && class_exists( __NAMESPACE__ . '\\SchemaOnboardingGuidance' ) ) {
			SchemaOnboardingGuidance::mark_identity_guidance_complete();
		}

		$new_revision = $current_revision + 1;
		Options::set( self::OPTION_SETTINGS_REVISION, $new_revision, false );

		if ( class_exists( '\SEOvaultSEO\Schema\Validation\SchemaValidationInvalidator' ) ) {
			\SEOvaultSEO\Schema\Validation\SchemaValidationInvalidator::bump_generation();
		}

		$snapshot                      = self::get_v2_settings_snapshot();
		$snapshot['settings_revision'] = $new_revision;

		return array(
			'settings'          => $snapshot,
			'settings_revision' => $new_revision,
			'message'           => __( 'Schema settings saved.', 'seovault-ai' ),
		);
	}

	/**
	 * Runtime emission defaults when `schemas` option is absent/empty.
	 *
	 * Returns only foundational types with safe enabled flags. Specialized
	 * catalogue types remain available via SchemaUtils::get_default_schemas()
	 * / type registry for pickers — they are not selected for emission.
	 *
	 * @return array
	 */
	public static function get_runtime_default_schemas() {
		$types = array();
		foreach ( SchemaUtils::get_default_schema_types() as $type => $class ) {
			if ( SchemaTypePolicyRegistry::is_foundational( $type ) ) {
				$types[ $type ] = $class;
			}
		}

		$schemas = SchemaUtils::build_schemas_from_types( $types );
		return SchemaUtils::build_final_schemas( $schemas );
	}

	/**
	 * Resolve stored global `schemas` option values for migration / catalogue helpers.
	 *
	 * Not used for frontend emission after the hard-cut (emission uses
	 * {@see FoundationalContextCollector}). Kept for merge/fallback when reading
	 * leftover option data during restore, deprecation scans, and tests.
	 *
	 * @param mixed $saved Saved `schemas` option value.
	 * @return array
	 */
	public static function resolve_stored_global_schemas( $saved ) {
		if ( ! is_array( $saved ) || empty( $saved ) ) {
			return self::get_runtime_default_schemas();
		}

		// Partial saved configs: merge catalogue defaults without enabling specialized by default.
		return SchemaUtils::merge_with_defaults( $saved, SchemaUtils::get_default_schemas() );
	}

	/**
	 * @deprecated Use {@see resolve_stored_global_schemas()}.
	 *
	 * @param mixed $saved Saved `schemas` option value.
	 * @return array
	 */
	public static function resolve_global_schemas_for_emission( $saved ) {
		return self::resolve_stored_global_schemas( $saved );
	}

	/**
	 * Add an option only when it does not already exist.
	 *
	 * @param string $key   Key without prefix.
	 * @param mixed  $value Value.
	 * @return void
	 */
	private static function add_option_once( $key, $value ) {
		$full = 'seovaultseo_' . $key;
		if ( false === get_option( $full, false ) ) {
			add_option( $full, $value, '', false );
		}
	}
}
