<?php
/**
 * Adds Sifency settings to WordPress WXR exports and restores them during import.
 *
 * @package Sifency_Addons
 */

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

final class Sifency_Addons_WXR_Settings {

	const XML_NODE           = 'sifency_addons_settings';
	const OPTION_WIDGETS     = 'sifency_addons_settings';
	const OPTION_CPT_MODULES = 'sifency_cpt_modules_status';
	const OPTION_THEME       = 'sifency-options';

	/**
	 * Singleton instance.
	 *
	 * @var self|null
	 */
	private static $instance = null;

	/**
	 * Whether the current request is producing a WordPress WXR file.
	 *
	 * @var bool
	 */
	private $is_wxr_export = false;

	/**
	 * Prevents duplicate restore work in a single import request.
	 *
	 * @var bool
	 */
	private $payload_restored = false;

	/**
	 * Prevents repeated late module loading in a single import request.
	 *
	 * @var bool
	 */
	private $modules_loaded_for_import = false;

	/**
	 * Get the singleton instance.
	 *
	 * @return self
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Register hooks.
	 */
	private function __construct() {
		add_action( 'export_wp', array( $this, 'mark_wxr_export' ), 1 );
		add_action( 'rss2_head', array( $this, 'render_settings_node' ), 1 );
		add_action( 'import_start', array( $this, 'restore_settings_on_import_start' ), 1 );
		add_filter( 'wp_import_terms', array( $this, 'ensure_modules_for_import_terms' ), 1 );
		add_filter( 'wp_import_posts', array( $this, 'ensure_modules_for_import_posts' ), 1 );
	}

	/**
	 * Mark the request as a WXR export before rss2_head is fired by export_wp().
	 *
	 * @param array $args Export arguments.
	 * @return void
	 */
	public function mark_wxr_export( $args ) {
		$this->is_wxr_export = true;
	}

	/**
	 * Print the Sifency settings payload inside the WXR channel.
	 *
	 * @return void
	 */
	public function render_settings_node() {
		if ( ! $this->is_wxr_export || ! defined( 'WXR_VERSION' ) ) {
			return;
		}

		$json = wp_json_encode( $this->get_export_payload(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
		if ( empty( $json ) ) {
			return;
		}

		$payload = base64_encode( $json );

		printf(
			"\n\t<%1\$s version=\"1\" encoding=\"base64\">%2\$s</%1\$s>\n",
			esc_html( self::XML_NODE ),
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CDATA wrapper for XML export; $payload is base64-encoded JSON.
			$this->wrap_cdata( $payload )
		);
	}

	/**
	 * Restore Sifency settings as soon as the WordPress importer starts.
	 *
	 * @return void
	 */
	public function restore_settings_on_import_start() {
		$file = $this->get_current_import_file();
		if ( $file ) {
			$this->restore_settings_from_file( $file );
		}
	}

	/**
	 * Restore settings from an explicit WXR file.
	 *
	 * @param string $file WXR file path.
	 * @return bool
	 */
	public function restore_settings_from_file( $file ) {
		if ( $this->payload_restored ) {
			return true;
		}

		$payload = $this->read_payload_from_file( $file );
		if ( empty( $payload ) ) {
			return false;
		}

		$this->restore_payload( $payload );
		$this->payload_restored = true;

		return true;
	}

	/**
	 * Enable CPT modules needed by imported taxonomy terms.
	 *
	 * @param array $terms Parsed WXR terms.
	 * @return array
	 */
	public function ensure_modules_for_import_terms( $terms ) {
		if ( ! is_array( $terms ) ) {
			return $terms;
		}

		$taxonomies = array();
		foreach ( $terms as $term ) {
			if ( isset( $term['term_taxonomy'] ) ) {
				$taxonomies[] = sanitize_key( $term['term_taxonomy'] );
			}
		}

		$this->enable_modules_for_taxonomies( $taxonomies );

		return $terms;
	}

	/**
	 * Enable CPT modules needed by imported posts before post_type_exists() checks.
	 *
	 * @param array $posts Parsed WXR posts.
	 * @return array
	 */
	public function ensure_modules_for_import_posts( $posts ) {
		if ( ! is_array( $posts ) ) {
			return $posts;
		}

		$post_types = array();
		foreach ( $posts as $post ) {
			if ( isset( $post['post_type'] ) ) {
				$post_types[] = sanitize_key( $post['post_type'] );
			}
		}

		$this->enable_modules_for_post_types( $post_types );

		return $posts;
	}

	/**
	 * Build the export payload.
	 *
	 * @return array
	 */
	private function get_export_payload() {
		$options = $this->get_export_options();

		return array(
			'version'            => 1,
			'source'             => 'sifency-addons',
			'exported_at'        => gmdate( 'c' ),
			'site_url'           => home_url(),
			'widget_settings'    => $options[ self::OPTION_WIDGETS ],
			'cpt_modules_status' => $options[ self::OPTION_CPT_MODULES ],
			'options'            => $options,
		);
	}

	/**
	 * Get Sifency options that should travel with demo/content exports.
	 *
	 * @return array
	 */
	public function get_export_options() {
		$options = array(
			self::OPTION_WIDGETS     => $this->get_normalized_addons_settings(),
			self::OPTION_CPT_MODULES => $this->get_normalized_cpt_module_status(),
		);

		$theme_options = get_option( self::OPTION_THEME, null );
		if ( null !== $theme_options && false !== $theme_options ) {
			$options[ self::OPTION_THEME ] = $theme_options;
		}

		return $options;
	}

	/**
	 * Get widget settings with explicit on/off states for registered widgets.
	 *
	 * @return array
	 */
	private function get_normalized_addons_settings() {
		$settings = get_option( self::OPTION_WIDGETS, array() );
		if ( ! is_array( $settings ) ) {
			$settings = array();
		}

		$saved_widgets = array();
		if ( isset( $settings['widgets'] ) && is_array( $settings['widgets'] ) ) {
			$saved_widgets = $settings['widgets'];
		}

		$settings['widgets'] = $this->get_normalized_widget_statuses( $saved_widgets );

		return $settings;
	}

	/**
	 * Get normalized widget statuses.
	 *
	 * @param array $saved_widgets Saved widget option values.
	 * @return array
	 */
	private function get_normalized_widget_statuses( array $saved_widgets ) {
		$statuses = array();
		$widgets  = $this->get_all_widget_ids();

		foreach ( $widgets as $widget_id ) {
			$widget_id              = sanitize_key( $widget_id );
			$saved_status           = isset( $saved_widgets[ $widget_id ] ) ? $saved_widgets[ $widget_id ] : 'on';
			$statuses[ $widget_id ] = ( 'off' === $saved_status ) ? 'off' : 'on';
		}

		foreach ( $saved_widgets as $widget_id => $saved_status ) {
			$widget_id = sanitize_key( $widget_id );
			if ( '' === $widget_id || isset( $statuses[ $widget_id ] ) ) {
				continue;
			}
			$statuses[ $widget_id ] = ( 'off' === $saved_status ) ? 'off' : 'on';
		}

		return $statuses;
	}

	/**
	 * Get all known widget IDs from the registry.
	 *
	 * @return array
	 */
	private function get_all_widget_ids() {
		if ( class_exists( '\Sifency_Addons\Includes\Elementor\Sifency_Addons_Widget_Registry' ) ) {
			$widgets = \Sifency_Addons\Includes\Elementor\Sifency_Addons_Widget_Registry::get_flat();
			if ( is_array( $widgets ) ) {
				return array_keys( $widgets );
			}
		}

		$config_file = SIFENCY_ADDONS_DIR . 'includes/elementor/sifency-widgets.php';
		if ( ! file_exists( $config_file ) ) {
			return array();
		}

		$config = include $config_file;
		if ( ! is_array( $config ) ) {
			return array();
		}

		$widget_ids = array();
		foreach ( $config as $widgets ) {
			if ( is_array( $widgets ) ) {
				$widget_ids = array_merge( $widget_ids, array_keys( $widgets ) );
			}
		}

		return $widget_ids;
	}

	/**
	 * Get normalized CPT module statuses.
	 *
	 * @return array
	 */
	private function get_normalized_cpt_module_status() {
		$statuses = array();

		if ( class_exists( 'Sifency_Addons_CPT_Module_Manager' ) ) {
			$modules = Sifency_Addons_CPT_Module_Manager::get_all_modules();
			foreach ( array_keys( $modules ) as $module_key ) {
				$statuses[ sanitize_key( $module_key ) ] = Sifency_Addons_CPT_Module_Manager::is_module_enabled( $module_key );
			}

			return $statuses;
		}

		$saved_statuses = get_option( self::OPTION_CPT_MODULES, array() );
		if ( ! is_array( $saved_statuses ) ) {
			return array();
		}

		foreach ( $saved_statuses as $module_key => $enabled ) {
			$statuses[ sanitize_key( $module_key ) ] = (bool) $enabled;
		}

		return $statuses;
	}

	/**
	 * Restore all supported values from a payload.
	 *
	 * @param array $payload Decoded payload.
	 * @return void
	 */
	private function restore_payload( array $payload ) {
		if ( isset( $payload['cpt_modules_status'] ) && is_array( $payload['cpt_modules_status'] ) ) {
			$this->restore_cpt_module_status( $payload['cpt_modules_status'] );
		} elseif ( isset( $payload['options'][ self::OPTION_CPT_MODULES ] ) && is_array( $payload['options'][ self::OPTION_CPT_MODULES ] ) ) {
			$this->restore_cpt_module_status( $payload['options'][ self::OPTION_CPT_MODULES ] );
		}

		if ( isset( $payload['widget_settings'] ) && is_array( $payload['widget_settings'] ) ) {
			$this->restore_widget_settings( $payload['widget_settings'] );
		} elseif ( isset( $payload['options'][ self::OPTION_WIDGETS ] ) && is_array( $payload['options'][ self::OPTION_WIDGETS ] ) ) {
			$this->restore_widget_settings( $payload['options'][ self::OPTION_WIDGETS ] );
		}

		if ( isset( $payload['options'][ self::OPTION_THEME ] ) && is_array( $payload['options'][ self::OPTION_THEME ] ) ) {
			update_option( self::OPTION_THEME, $payload['options'][ self::OPTION_THEME ] );
		}

		$this->load_enabled_modules_for_import();

		do_action( 'sifency_addons_wxr_settings_imported', $payload );
	}

	/**
	 * Restore the CPT module status option.
	 *
	 * @param array $statuses Module statuses.
	 * @return void
	 */
	private function restore_cpt_module_status( array $statuses ) {
		$allowed_modules = array();
		if ( class_exists( 'Sifency_Addons_CPT_Module_Manager' ) ) {
			$allowed_modules = array_keys( Sifency_Addons_CPT_Module_Manager::get_all_modules() );
		}

		$normalized = array();
		foreach ( $statuses as $module_key => $enabled ) {
			$module_key = sanitize_key( $module_key );
			if ( '' === $module_key ) {
				continue;
			}

			if ( ! empty( $allowed_modules ) && ! in_array( $module_key, $allowed_modules, true ) ) {
				continue;
			}

			$normalized[ $module_key ] = (bool) $enabled;
		}

		if ( ! empty( $normalized ) ) {
			update_option( self::OPTION_CPT_MODULES, $normalized );
		}
	}

	/**
	 * Restore Sifency widget settings.
	 *
	 * @param array $settings Settings option value.
	 * @return void
	 */
	private function restore_widget_settings( array $settings ) {
		$existing = get_option( self::OPTION_WIDGETS, array() );
		if ( ! is_array( $existing ) ) {
			$existing = array();
		}

		if ( isset( $settings['widgets'] ) && is_array( $settings['widgets'] ) ) {
			$existing['widgets'] = $this->sanitize_widget_statuses( $settings['widgets'] );
			unset( $settings['widgets'] );
		}

		foreach ( $settings as $setting_key => $setting_value ) {
			$setting_key = sanitize_key( $setting_key );
			if ( '' === $setting_key ) {
				continue;
			}
			$existing[ $setting_key ] = $setting_value;
		}

		update_option( self::OPTION_WIDGETS, $existing );
	}

	/**
	 * Sanitize widget on/off values.
	 *
	 * @param array $statuses Widget statuses.
	 * @return array
	 */
	private function sanitize_widget_statuses( array $statuses ) {
		$sanitized = array();

		foreach ( $statuses as $widget_id => $status ) {
			$widget_id = sanitize_key( $widget_id );
			if ( '' === $widget_id ) {
				continue;
			}
			$sanitized[ $widget_id ] = ( 'off' === $status ) ? 'off' : 'on';
		}

		return $sanitized;
	}

	/**
	 * Read the embedded payload from a WXR file.
	 *
	 * @param string $file WXR file path.
	 * @return array
	 */
	private function read_payload_from_file( $file ) {
		if ( empty( $file ) || ! is_readable( $file ) ) {
			return array();
		}

		if ( class_exists( 'DOMDocument' ) ) {
			$payload = $this->read_payload_with_dom( $file );
			if ( ! empty( $payload ) ) {
				return $payload;
			}
		}

		return $this->read_payload_with_regex( $file );
	}

	/**
	 * Read payload using DOMDocument.
	 *
	 * @param string $file WXR file path.
	 * @return array
	 */
	private function read_payload_with_dom( $file ) {
		$internal_errors = libxml_use_internal_errors( true );
		$old_value       = null;

		if ( function_exists( 'libxml_disable_entity_loader' ) && PHP_VERSION_ID < 80000 ) {
			$old_value = libxml_disable_entity_loader( true );
		}

		$dom     = new DOMDocument();
		$success = $dom->load( $file, LIBXML_NONET );

		if ( null !== $old_value ) {
			libxml_disable_entity_loader( $old_value );
		}

		libxml_clear_errors();
		libxml_use_internal_errors( $internal_errors );

		if ( ! $success || isset( $dom->doctype ) ) {
			return array();
		}

		$nodes = $dom->getElementsByTagName( self::XML_NODE );
		if ( ! $nodes->length ) {
			return array();
		}

		return $this->decode_payload( $nodes->item( 0 )->textContent );
	}

	/**
	 * Regex fallback for hosts without DOMDocument.
	 *
	 * @param string $file WXR file path.
	 * @return array
	 */
	private function read_payload_with_regex( $file ) {
		$contents = file_get_contents( $file );
		if ( false === $contents ) {
			return array();
		}

		$node = preg_quote( self::XML_NODE, '|' );
		if ( ! preg_match( '|<' . $node . '[^>]*>(.*?)</' . $node . '>|is', $contents, $match ) ) {
			return array();
		}

		$payload = trim( $match[1] );
		$payload = preg_replace( '|^<!\[CDATA\[(.*)\]\]>$|s', '$1', $payload );

		return $this->decode_payload( $payload );
	}

	/**
	 * Decode a base64 JSON payload.
	 *
	 * @param string $payload Encoded payload.
	 * @return array
	 */
	private function decode_payload( $payload ) {
		$payload = trim( (string) $payload );
		if ( '' === $payload ) {
			return array();
		}

		$decoded = base64_decode( $payload, true );
		$json    = false === $decoded ? $payload : $decoded;
		$data    = json_decode( $json, true );

		return is_array( $data ) ? $data : array();
	}

	/**
	 * Try to identify the WXR file being imported by WordPress Importer.
	 *
	 * @return string
	 */
	private function get_current_import_file() {
		$import_id = 0;

		if ( isset( $_POST['import_id'] ) ) {
			$import_id = absint( wp_unslash( $_POST['import_id'] ) );
		} elseif ( isset( $_REQUEST['import_id'] ) ) {
			$import_id = absint( wp_unslash( $_REQUEST['import_id'] ) );
		}

		if ( $import_id ) {
			$file = get_attached_file( $import_id );
			if ( is_string( $file ) && is_readable( $file ) ) {
				return $file;
			}
		}

		if ( ! function_exists( 'debug_backtrace' ) ) {
			return '';
		}

		$trace = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, 12 );
		foreach ( $trace as $frame ) {
			if ( empty( $frame['function'] ) || empty( $frame['args'][0] ) || ! is_string( $frame['args'][0] ) ) {
				continue;
			}

			if ( ! in_array( $frame['function'], array( 'import_start', 'import' ), true ) ) {
				continue;
			}

			if ( isset( $frame['class'] ) && is_a( $frame['class'], 'WP_Import', true ) && is_readable( $frame['args'][0] ) ) {
				return $frame['args'][0];
			}
		}

		return '';
	}

	/**
	 * Enable modules from a list of imported post types.
	 *
	 * @param array $post_types Post types.
	 * @return void
	 */
	private function enable_modules_for_post_types( array $post_types ) {
		$map     = $this->get_post_type_module_map();
		$modules = array();

		foreach ( array_unique( $post_types ) as $post_type ) {
			if ( isset( $map[ $post_type ] ) ) {
				$modules[] = $map[ $post_type ];
			}
		}

		$this->enable_modules( $modules );
	}

	/**
	 * Enable modules from a list of imported taxonomies.
	 *
	 * @param array $taxonomies Taxonomies.
	 * @return void
	 */
	private function enable_modules_for_taxonomies( array $taxonomies ) {
		$map     = $this->get_taxonomy_module_map();
		$modules = array();

		foreach ( array_unique( $taxonomies ) as $taxonomy ) {
			if ( isset( $map[ $taxonomy ] ) ) {
				$modules[] = $map[ $taxonomy ];
			}
		}

		$this->enable_modules( $modules );
	}

	/**
	 * Enable modules and load their CPT registration classes immediately.
	 *
	 * @param array $modules Module keys.
	 * @return void
	 */
	private function enable_modules( array $modules ) {
		$modules = array_unique( array_filter( array_map( 'sanitize_key', $modules ) ) );
		if ( empty( $modules ) ) {
			return;
		}

		$allowed_modules = array();
		if ( class_exists( 'Sifency_Addons_CPT_Module_Manager' ) ) {
			$allowed_modules = array_keys( Sifency_Addons_CPT_Module_Manager::get_all_modules() );
		}

		$statuses = get_option( self::OPTION_CPT_MODULES, array() );
		if ( ! is_array( $statuses ) ) {
			$statuses = array();
		}

		$changed = false;
		foreach ( $modules as $module_key ) {
			if ( ! empty( $allowed_modules ) && ! in_array( $module_key, $allowed_modules, true ) ) {
				continue;
			}

			if ( empty( $statuses[ $module_key ] ) ) {
				$statuses[ $module_key ] = true;
				$changed                 = true;
			}
		}

		if ( $changed ) {
			update_option( self::OPTION_CPT_MODULES, $statuses );
			$this->modules_loaded_for_import = false;
		}

		$this->load_enabled_modules_for_import();
	}

	/**
	 * Load enabled module files after option restoration.
	 *
	 * @return void
	 */
	private function load_enabled_modules_for_import() {
		if ( $this->modules_loaded_for_import || ! class_exists( 'Sifency_Addons_CPT_Module_Manager' ) ) {
			return;
		}

		Sifency_Addons_CPT_Module_Manager::load_enabled_modules();
		$this->modules_loaded_for_import = true;
	}

	/**
	 * Map registered CPT post types to their module keys.
	 *
	 * @return array
	 */
	private function get_post_type_module_map() {
		return array(
			'team'        => 'team',
			'service'     => 'service',
			'portfolio'   => 'portfolio',
			'event'       => 'event',
			'career'      => 'career',
			'doc'         => 'doc',
			'digital'     => 'digital',
			'award'       => 'award',
			'client'      => 'client',
			'stack'       => 'stack',
			'testimonial' => 'testimonial',
			'menu'        => 'restaurant-menu',
		);
	}

	/**
	 * Map registered CPT taxonomies to their module keys.
	 *
	 * @return array
	 */
	private function get_taxonomy_module_map() {
		return array(
			'team_category'        => 'team',
			'service_category'     => 'service',
			'portfolio_category'   => 'portfolio',
			'event_category'       => 'event',
			'career_category'      => 'career',
			'doc_category'         => 'doc',
			'digital_category'     => 'digital',
			'award_category'       => 'award',
			'client_category'      => 'client',
			'stack_category'       => 'stack',
			'testimonial_category' => 'testimonial',
			'menu_category'        => 'restaurant-menu',
		);
	}

	/**
	 * Wrap text in XML CDATA.
	 *
	 * @param string $value Text value.
	 * @return string
	 */
	private function wrap_cdata( $value ) {
		return '<![CDATA[' . str_replace( ']]>', ']]]]><![CDATA[>', (string) $value ) . ']]>';
	}
}
