<?php
/**
 * Settings Page Handlers
 *
 * Handles export (file download), import (file upload), cloud backup push,
 * and cloud backup pull for the Settings admin page.
 *
 * @package Agency_Pulse_Admin_Menu
 * @since   1.0.0
 */

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

class Agency_Pulse_Admin_Menu_Settings_Page {

	/**
	 * @var Agency_Pulse_Admin_Menu_Settings_Manager
	 */
	private $settings_manager;

	/**
	 * @param Agency_Pulse_Admin_Menu_Settings_Manager $settings_manager
	 */
	public function __construct( Agency_Pulse_Admin_Menu_Settings_Manager $settings_manager ) {
		$this->settings_manager = $settings_manager;
	}

	// -------------------------------------------------------------------------
	// Export
	// -------------------------------------------------------------------------

	/**
	 * Handle the export admin-post action.
	 *
	 * Outputs the full configuration as a JSON file download and exits.
	 *
	 * @since 1.0.0
	 */
	public function handle_export(): void {
		check_admin_referer( 'aamo_export_nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_die( esc_html__( 'You do not have permission to export the configuration.', 'agency-pulse-admin-menu' ) );
		}

		$payload  = $this->build_export_payload();
		$json     = wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
		$filename = 'aamo-config-' . gmdate( 'Y-m-d' ) . '.json';

		nocache_headers();
		header( 'Content-Type: application/json; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Content-Length: ' . strlen( $json ) );

		echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		exit;
	}

	// -------------------------------------------------------------------------
	// Import
	// -------------------------------------------------------------------------

	/**
	 * Handle the import admin-post action.
	 *
	 * Reads the uploaded JSON file, validates, sanitizes, and replaces the
	 * stored configuration. Redirects back to the Settings page with a result
	 * transient the template will display.
	 *
	 * @since 1.0.0
	 */
	public function handle_import(): void {
		check_admin_referer( 'aamo_import_nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_die( esc_html__( 'You do not have permission to import configurations.', 'agency-pulse-admin-menu' ) );
		}

		$redirect = admin_url( 'admin.php?page=' . Agency_Pulse_Admin_Menu_Constants::PAGE_SETTINGS );

		// Check file was supplied.
		if ( empty( $_FILES['aamo_import_file'] ) || UPLOAD_ERR_OK !== $_FILES['aamo_import_file']['error'] ) {
			$this->set_import_result( 'error', __( 'No file uploaded or upload error occurred.', 'agency-pulse-admin-menu' ) );
			wp_safe_redirect( $redirect );
			exit;
		}

		// Size limit: 512 KB.
		if ( $_FILES['aamo_import_file']['size'] > 524288 ) {
			$this->set_import_result( 'error', __( 'File exceeds the 512 KB size limit.', 'agency-pulse-admin-menu' ) );
			wp_safe_redirect( $redirect );
			exit;
		}

		$raw = file_get_contents( $_FILES['aamo_import_file']['tmp_name'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		if ( false === $raw ) {
			$this->set_import_result( 'error', __( 'Could not read the uploaded file.', 'agency-pulse-admin-menu' ) );
			wp_safe_redirect( $redirect );
			exit;
		}

		$result = $this->import_from_json( $raw );

		$this->set_import_result( $result['type'], $result['message'] );
		wp_safe_redirect( $redirect );
		exit;
	}

	// -------------------------------------------------------------------------
	// License & Connection — connect / check / disconnect
	// -------------------------------------------------------------------------

	/**
	 * AJAX: Handle all three license connection sub-actions.
	 *
	 * Sub-action is identified by the `connect_action` POST parameter:
	 *   connect          — initial key entry; POSTs to /api/plugin/token
	 *   check_connection — re-POSTs after email confirmation
	 *   disconnect       — clears local state only (slot release via dashboard)
	 *
	 * All successful state transitions return wp_send_json_success() with
	 * { status, message } so the JS always reads resp.data.status.
	 * Hard PHP-level failures (permissions, nonce) return wp_send_json_error().
	 *
	 * @since 1.0.0
	 */
	public function ajax_ao_connect(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_AO_CONNECT, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$connect_action = isset( $_POST['connect_action'] )
			? sanitize_key( wp_unslash( $_POST['connect_action'] ) )
			: 'connect';

		// Disconnect — reset local state, preserving plugin_key for reconnect.
		if ( 'disconnect' === $connect_action ) {
			Agency_Pulse_Admin_Menu_License_Manager::disconnect();
			wp_send_json_success( array(
				'status'  => 'inactive',
				'message' => '',
			) );
		}

		// Resolve the plugin key.
		if ( 'check_connection' === $connect_action ) {
			$plugin_key = Agency_Pulse_Admin_Menu_License_Manager::get_plugin_key();
			if ( empty( $plugin_key ) ) {
				wp_send_json_success( array(
					'status'  => 'inactive',
					'message' => __( 'No plugin key stored. Please enter your key and reconnect.', 'agency-pulse-admin-menu' ),
				) );
			}
		} else {
			$plugin_key = sanitize_text_field( wp_unslash( $_POST['plugin_key'] ?? '' ) );
			if ( empty( $plugin_key ) ) {
				wp_send_json_error( array( 'message' => __( 'Plugin key is required.', 'agency-pulse-admin-menu' ) ) );
			}
			Agency_Pulse_Admin_Menu_License_Manager::set_state( array(
				'plugin_key' => $plugin_key,
				'status'     => 'inactive',
				'last_error' => '',
			) );
		}

		// POST to /api/plugin/token with the apwp-ao SKU.
		$base     = rtrim( defined( 'APWP_CONNECT_URL' ) ? APWP_CONNECT_URL : '', '/' );
		$response = wp_remote_post(
			$base . '/api/plugin/token',
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( array(
					'plugin_key'    => $plugin_key,
					'plugin_domain' => home_url(),
					'plugin_sku'    => 'apwp-ao',
				) ),
				'timeout' => 20,
			)
		);

		if ( is_wp_error( $response ) ) {
			Agency_Pulse_Admin_Menu_License_Manager::set_state( array(
				'status'     => 'inactive',
				'last_error' => $response->get_error_message(),
			) );
			wp_send_json_success( array(
				'status'  => 'inactive',
				'message' => $response->get_error_message(),
			) );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $body ) ) {
			$body = array();
		}

		// 202 — email confirmation required.
		if ( 202 === $code ) {
			Agency_Pulse_Admin_Menu_License_Manager::set_state( array( 'status' => 'pending_email' ) );
			wp_send_json_success( array(
				'status'  => 'pending_email',
				'message' => __( 'Check your email to confirm this domain.', 'agency-pulse-admin-menu' ),
			) );
		}

		// 200 — domain already authorized; JWT returned.
		if ( 200 === $code && ! empty( $body['access_token'] ) ) {
			$jwt    = $body['access_token'];
			$claims = Agency_Pulse_Admin_Menu_License_Manager::decode_jwt_payload( $jwt );
			// JWT carries slot id as 'sid' claim (not 'slot_id').
			$slot_id = isset( $claims['sid'] ) ? (int) $claims['sid'] : 0;
			Agency_Pulse_Admin_Menu_License_Manager::set_state( array(
				'jwt'         => $jwt,
				'slot_id'     => $slot_id,
				'domain_hash' => $claims['dh'] ?? '',
				'expires_at'  => isset( $claims['exp'] ) ? (int) $claims['exp'] : 0,
				'status'      => 'active',
				'last_error'  => '',
			) );
			wp_send_json_success( array(
				'status'  => 'active',
				'slot_id' => $slot_id,
				'message' => __( 'Site connected successfully.', 'agency-pulse-admin-menu' ),
			) );
		}

		// 403 — slot full, release cooldown, or license revoked.
		if ( 403 === $code ) {
			// Server returns the machine-readable code in the 'error' field.
			$error_code = $body['error'] ?? '';

			if ( 'domain_limit_reached' === $error_code ) {
				Agency_Pulse_Admin_Menu_License_Manager::set_state( array( 'status' => 'slot_full' ) );
				wp_send_json_success( array(
					'status'  => 'slot_full',
					'message' => __( 'All domain slots for this license are in use.', 'agency-pulse-admin-menu' ),
				) );
			}

			if ( 'slot_release_cooldown' === $error_code ) {
				Agency_Pulse_Admin_Menu_License_Manager::set_state( array(
					'status'     => 'inactive',
					'last_error' => $body['message'] ?? '',
				) );
				wp_send_json_success( array(
					'status'  => 'inactive',
					'message' => $body['message'] ?? __( 'This domain was recently released. Please wait 30 days before reconnecting it.', 'agency-pulse-admin-menu' ),
				) );
			}

			Agency_Pulse_Admin_Menu_License_Manager::disconnect();
			Agency_Pulse_Admin_Menu_License_Manager::set_state( array(
				'last_error' => $body['message'] ?? __( 'License access has been revoked. Contact support.', 'agency-pulse-admin-menu' ),
			) );
			wp_send_json_success( array(
				'status'  => 'inactive',
				'message' => $body['message'] ?? __( 'License access has been revoked. Contact support.', 'agency-pulse-admin-menu' ),
			) );
		}

		// 401 — invalid plugin key.
		if ( 401 === $code ) {
			Agency_Pulse_Admin_Menu_License_Manager::disconnect();
			wp_send_json_success( array(
				'status'  => 'inactive',
				'message' => $body['message'] ?? __( 'Invalid plugin key.', 'agency-pulse-admin-menu' ),
			) );
		}

		// Any other status code — surface the server message and stay inactive.
		$fallback_msg = isset( $body['message'] ) ? $body['message'] : sprintf(
			/* translators: %d: HTTP status code */
			__( 'Server returned status %d.', 'agency-pulse-admin-menu' ),
			$code
		);
		Agency_Pulse_Admin_Menu_License_Manager::set_state( array(
			'status'     => 'inactive',
			'last_error' => $fallback_msg,
		) );
		wp_send_json_success( array(
			'status'  => 'inactive',
			'message' => $fallback_msg,
		) );
	}

	// -------------------------------------------------------------------------
	// Cloud backup — push
	// -------------------------------------------------------------------------

	/**
	 * AJAX: Push the current configuration to the cloud backup server.
	 *
	 * @since 1.0.0
	 */
	public function ajax_backup_push(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_BACKUP_PUSH, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$payload = $this->build_backup_payload();

		$response = Agency_Pulse_Admin_Menu_Api_Client::request(
			'POST',
			'plugin/ao-backup',
			array(
				'body'    => wp_json_encode( array(
					'backup_key' => Agency_Pulse_Admin_Menu_Constants::BACKUP_KEY,
					'payload'    => wp_json_encode( $payload ),
				) ),
				'headers' => array( 'Content-Type' => 'application/json' ),
				'timeout' => 20,
			)
		);

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => $response->get_error_message() ) );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );

		if ( 200 === $code || 201 === $code ) {
			wp_send_json_success( array( 'message' => __( 'Backup saved to cloud.', 'agency-pulse-admin-menu' ) ) );
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		$msg  = isset( $body['message'] ) ? $body['message'] : sprintf(
			/* translators: %d: HTTP status code */
			__( 'Server returned status %d.', 'agency-pulse-admin-menu' ),
			$code
		);
		wp_send_json_error( array( 'message' => $msg ) );
	}

	// -------------------------------------------------------------------------
	// Cloud backup — pull
	// -------------------------------------------------------------------------

	/**
	 * AJAX: Pull the cloud backup and replace the current configuration.
	 *
	 * @since 1.0.0
	 */
	public function ajax_backup_pull(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_BACKUP_PULL, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$response = Agency_Pulse_Admin_Menu_Api_Client::request(
			'GET',
			'plugin/ao-backup/' . rawurlencode( Agency_Pulse_Admin_Menu_Constants::BACKUP_KEY ),
			array( 'timeout' => 20 )
		);

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => $response->get_error_message() ) );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );

		if ( 404 === $code ) {
			wp_send_json_error( array( 'message' => __( 'No cloud backup found.', 'agency-pulse-admin-menu' ) ) );
		}

		if ( 200 !== $code ) {
			wp_send_json_error( array( 'message' => sprintf(
				/* translators: %d: HTTP status code */
				__( 'Server returned status %d.', 'agency-pulse-admin-menu' ),
				$code
			) ) );
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		// The server returns payload as a decoded object; re-encode to JSON so
		// restore_from_backup() can process it through the normal pipeline.
		$payload_raw = wp_json_encode( $body['payload'] ?? $body );

		$result = $this->restore_from_backup( $payload_raw );

		if ( 'error' === $result['type'] ) {
			wp_send_json_error( array( 'message' => $result['message'] ) );
		}

		wp_send_json_success( array( 'message' => $result['message'] ) );
	}

	// -------------------------------------------------------------------------
	// Template Library
	// -------------------------------------------------------------------------

	/**
	 * AJAX: Save the current full configuration as a named template on the server.
	 *
	 * Looks up whether this site has previously saved a template under the same
	 * name (via OPTION_TEMPLATE_SLUGS). If so, sends the stored slug so the
	 * server updates the existing template rather than creating a duplicate.
	 *
	 * On success the returned slug is stored in OPTION_TEMPLATE_SLUGS keyed by
	 * sanitize_key($name) so future saves with the same name will update.
	 *
	 * @since 1.0.0
	 */
	public function ajax_template_save(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_SAVE, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) );
		if ( '' === $name ) {
			wp_send_json_error( array( 'message' => __( 'Template name is required.', 'agency-pulse-admin-menu' ) ) );
		}
		if ( mb_strlen( $name ) > 191 ) {
			wp_send_json_error( array( 'message' => __( 'Template name must be 191 characters or fewer.', 'agency-pulse-admin-menu' ) ) );
		}

		$description = sanitize_text_field( wp_unslash( $_POST['description'] ?? '' ) );
		if ( mb_strlen( $description ) > 500 ) {
			$description = mb_substr( $description, 0, 500 );
		}

		$payload  = $this->build_export_payload( true );
		$slug_map = get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_TEMPLATE_SLUGS, array() );
		$slug_key = sanitize_key( $name );
		$slug     = isset( $slug_map[ $slug_key ] ) ? $slug_map[ $slug_key ] : '';

		$body = array(
			'name'        => $name,
			'description' => $description,
			'payload'     => wp_json_encode( $payload ),
		);
		if ( '' !== $slug ) {
			$body['slug'] = $slug;
		}

		$response = Agency_Pulse_Admin_Menu_Api_Client::request(
			'POST',
			'plugin/ao-templates',
			array(
				'body'    => wp_json_encode( $body ),
				'headers' => array( 'Content-Type' => 'application/json' ),
				'timeout' => 20,
			)
		);

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => $response->get_error_message() ) );
		}

		$code         = (int) wp_remote_retrieve_response_code( $response );
		$decoded      = json_decode( wp_remote_retrieve_body( $response ), true );
		$server_error = is_array( $decoded ) ? ( $decoded['error'] ?? '' ) : '';

		if ( 422 === $code && 'name_taken' === $server_error ) {
			wp_send_json_error( array(
				'message'    => __( 'A template with this name already exists. Please choose a different name.', 'agency-pulse-admin-menu' ),
				'name_taken' => true,
			) );
		}

		if ( 200 === $code || 201 === $code ) {
			$returned_slug = is_array( $decoded ) ? ( $decoded['slug'] ?? '' ) : '';

			if ( '' !== $returned_slug ) {
				$slug_map[ $slug_key ] = $returned_slug;
				update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_TEMPLATE_SLUGS, $slug_map );
			}

			wp_send_json_success( array(
				'message'    => 201 === $code
					? __( 'Template saved.', 'agency-pulse-admin-menu' )
					: __( 'Template updated.', 'agency-pulse-admin-menu' ),
				'slug'       => $returned_slug,
				'name'       => $name,
				'updated_at' => is_array( $decoded ) ? ( $decoded['updated_at'] ?? '' ) : '',
			) );
		}

		$msg = is_array( $decoded ) && isset( $decoded['message'] )
			? $decoded['message']
			: sprintf(
				/* translators: %d: HTTP status code */
				__( 'Server returned status %d.', 'agency-pulse-admin-menu' ),
				$code
			);
		wp_send_json_error( array( 'message' => $msg ) );
	}

	/**
	 * AJAX: Fetch the list of templates from the server (no payloads).
	 *
	 * @since 1.0.0
	 */
	public function ajax_template_list(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_LIST, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$response = Agency_Pulse_Admin_Menu_Api_Client::request(
			'GET',
			'plugin/ao-templates',
			array( 'timeout' => 20 )
		);

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => $response->get_error_message() ) );
		}

		$code    = (int) wp_remote_retrieve_response_code( $response );
		$decoded = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( 200 === $code && is_array( $decoded ) && isset( $decoded['templates'] ) ) {
			wp_send_json_success( array( 'templates' => $decoded['templates'] ) );
		}

		$msg = is_array( $decoded ) && isset( $decoded['message'] )
			? $decoded['message']
			: sprintf(
				/* translators: %d: HTTP status code */
				__( 'Server returned status %d.', 'agency-pulse-admin-menu' ),
				$code
			);
		wp_send_json_error( array( 'message' => $msg ) );
	}

	/**
	 * AJAX: Fetch a single template payload from the server and apply it.
	 *
	 * Uses the same import_from_json() pipeline as the file import action,
	 * so the same sanitization and storage logic applies.
	 *
	 * @since 1.0.0
	 */
	public function ajax_template_apply(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_APPLY, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$slug = sanitize_key( wp_unslash( $_POST['slug'] ?? '' ) );
		if ( '' === $slug ) {
			wp_send_json_error( array( 'message' => __( 'Template slug is required.', 'agency-pulse-admin-menu' ) ) );
		}

		$response = Agency_Pulse_Admin_Menu_Api_Client::request(
			'GET',
			'plugin/ao-templates/' . rawurlencode( $slug ),
			array( 'timeout' => 20 )
		);

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => $response->get_error_message() ) );
		}

		$code    = (int) wp_remote_retrieve_response_code( $response );
		$decoded = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( 404 === $code ) {
			wp_send_json_error( array( 'message' => __( 'Template not found.', 'agency-pulse-admin-menu' ) ) );
		}

		if ( 200 !== $code ) {
			$msg = is_array( $decoded ) && isset( $decoded['message'] )
				? $decoded['message']
				: sprintf(
					/* translators: %d: HTTP status code */
					__( 'Server returned status %d.', 'agency-pulse-admin-menu' ),
					$code
				);
			wp_send_json_error( array( 'message' => $msg ) );
		}

		// Server returns payload as a decoded object; re-encode so import_from_json()
		// can process it through the standard JSON parsing pipeline.
		$payload_raw = wp_json_encode( $decoded['payload'] ?? $decoded );
		$result      = $this->import_from_json( $payload_raw );

		if ( 'error' === $result['type'] ) {
			wp_send_json_error( array( 'message' => $result['message'] ) );
		}

		wp_send_json_success( array( 'message' => $result['message'] ) );
	}

	/**
	 * AJAX: Delete a template from the server and remove it from the local slug map.
	 *
	 * A 404 from the server is treated as success — the template is already gone,
	 * so the local slug map entry is cleaned up regardless.
	 *
	 * @since 1.0.0
	 */
	public function ajax_template_delete(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_DELETE, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$slug = sanitize_key( wp_unslash( $_POST['slug'] ?? '' ) );
		if ( '' === $slug ) {
			wp_send_json_error( array( 'message' => __( 'Template slug is required.', 'agency-pulse-admin-menu' ) ) );
		}

		$response = Agency_Pulse_Admin_Menu_Api_Client::request(
			'DELETE',
			'plugin/ao-templates/' . rawurlencode( $slug ),
			array( 'timeout' => 20 )
		);

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => $response->get_error_message() ) );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );

		if ( 200 === $code || 404 === $code ) {
			// Remove from local slug map regardless of whether the server found it.
			$slug_map = get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_TEMPLATE_SLUGS, array() );
			foreach ( $slug_map as $key => $stored_slug ) {
				if ( $stored_slug === $slug ) {
					unset( $slug_map[ $key ] );
					break;
				}
			}
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_TEMPLATE_SLUGS, $slug_map );

			wp_send_json_success( array( 'message' => __( 'Template deleted.', 'agency-pulse-admin-menu' ) ) );
		}

		$decoded = json_decode( wp_remote_retrieve_body( $response ), true );
		$msg     = is_array( $decoded ) && isset( $decoded['message'] )
			? $decoded['message']
			: sprintf(
				/* translators: %d: HTTP status code */
				__( 'Server returned status %d.', 'agency-pulse-admin-menu' ),
				$code
			);
		wp_send_json_error( array( 'message' => $msg ) );
	}

	// -------------------------------------------------------------------------
	// Admin Bar Customizer
	// -------------------------------------------------------------------------

	/**
	 * AJAX: Save admin bar customizer settings.
	 *
	 * @since 1.0.0
	 */
	public function ajax_save_branding_icon(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_BRANDING_ICON, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$valid_link_modes = array( 'wordpress', 'custom', 'none' );
		$link_mode        = sanitize_key( wp_unslash( $_POST['link_mode'] ?? 'wordpress' ) );
		if ( ! in_array( $link_mode, $valid_link_modes, true ) ) {
			$link_mode = 'wordpress';
		}

		$data = array(
			'site_name'    => sanitize_text_field( wp_unslash( $_POST['site_name'] ?? '' ) ),
			'enabled'      => ! empty( $_POST['enabled'] ) && '1' === $_POST['enabled'],
			'icon_url'     => esc_url_raw( wp_unslash( $_POST['icon_url'] ?? '' ) ),
			'icon_alt'     => sanitize_text_field( wp_unslash( $_POST['icon_alt'] ?? '' ) ),
			'link_mode'    => $link_mode,
			'link_url'     => esc_url_raw( wp_unslash( $_POST['link_url'] ?? '' ) ),
			'link_target'  => ! empty( $_POST['link_target'] ) && '1' === $_POST['link_target'],
			'hide_submenu' => ! empty( $_POST['hide_submenu'] ) && '1' === $_POST['hide_submenu'],
		);

		update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_BRANDING_ICON, $data );
		wp_send_json_success( array( 'message' => __( 'Settings saved.', 'agency-pulse-admin-menu' ) ) );
	}

	// -------------------------------------------------------------------------
	// Admin Footer Customizer
	// -------------------------------------------------------------------------

	/**
	 * AJAX: Save admin footer customizer settings.
	 *
	 * @since 1.0.0
	 */
	public function ajax_save_admin_footer(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_ADMIN_FOOTER, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$data = array(
			'enabled'      => ! empty( $_POST['enabled'] ) && '1' === $_POST['enabled'],
			'left_content' => wp_kses_post( wp_unslash( $_POST['left_content'] ?? '' ) ),
			'show_version' => ! empty( $_POST['show_version'] ) && '1' === $_POST['show_version'],
			'right_text'   => sanitize_text_field( wp_unslash( $_POST['right_text'] ?? '' ) ),
		);

		update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ADMIN_FOOTER, $data );
		wp_send_json_success( array( 'message' => __( 'Settings saved.', 'agency-pulse-admin-menu' ) ) );
	}

	// -------------------------------------------------------------------------
	// Color Customizer
	// -------------------------------------------------------------------------

	/**
	 * Color slot keys with their WP Default palette values.
	 *
	 * @since 1.0.0
	 * @return array<string, string>
	 */
	private function color_defaults(): array {
		return array(
			'sidebar_bg'        => '#1d2327',
			'submenu_bg'        => '#2c3338',
			'submenu_text'      => '#a7aaad',
			'submenu_hl_text'   => '#ffffff',
			'sidebar_text'      => '#a7aaad',
			'sidebar_icon'      => '#a7aaad',
			'sidebar_highlight' => '#2271b1',
			'sidebar_hl_text'   => '#ffffff',
			'adminbar_bg'       => '#1d2327',
			'adminbar_text'     => '#a7aaad',
			'accent'            => '#2271b1',
			'bubble_bg'         => '#d63638',
			'bubble_text'       => '#ffffff',
		);
	}

	/**
	 * AJAX: Save the custom color palette.
	 *
	 * @since 1.0.0
	 */
	public function ajax_save_color_scheme(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_COLOR_SCHEME, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$defaults = $this->color_defaults();
		$data     = array( 'enabled' => ! empty( $_POST['enabled'] ) && '1' === $_POST['enabled'] );

		foreach ( array_keys( $defaults ) as $slot ) {
			$raw           = sanitize_hex_color( wp_unslash( $_POST[ $slot ] ?? '' ) );
			$data[ $slot ] = '' !== $raw ? $raw : $defaults[ $slot ];
		}

		update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_COLOR_SCHEME, $data );
		wp_send_json_success( array( 'message' => __( 'Color scheme saved.', 'agency-pulse-admin-menu' ) ) );
	}

	/**
	 * AJAX: Reset color palette to WP Default values.
	 *
	 * @since 1.0.0
	 */
	public function ajax_reset_color_scheme(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_COLOR_SCHEME, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$data            = $this->color_defaults();
		$data['enabled'] = true;

		update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_COLOR_SCHEME, $data );
		wp_send_json_success( array(
			'message' => __( 'Color scheme reset to defaults.', 'agency-pulse-admin-menu' ),
			'colors'  => $data,
		) );
	}

	// -------------------------------------------------------------------------
	// Shared: build export payload
	// -------------------------------------------------------------------------

	/**
	 * Assemble the export/backup envelope from current stored data.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	private function build_export_payload( bool $include_customizations = false ): array {
		$payload = array(
			'version'          => '1.0',
			'plugin'           => 'agency-pulse-admin-menu',
			'exported_at'      => gmdate( 'c' ),
			'configs'          => $this->settings_manager->get_all_configs(),
			'role_assignments' => $this->settings_manager->get_role_assignments(),
		);

		if ( $include_customizations ) {
			$payload['branding_icon']     = get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_BRANDING_ICON, array() );
			$payload['admin_footer']      = get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ADMIN_FOOTER, array() );
			$payload['color_scheme']      = get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_COLOR_SCHEME, array() );
			$payload['dashboard_message'] = get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_DASHBOARD_MESSAGE, array() );
		}

		return $payload;
	}

	private function build_backup_payload(): array {
		return array(
			'version'           => '2.0',
			'plugin'            => 'agency-pulse-admin-menu',
			'backed_up_at'      => gmdate( 'c' ),
			'configs'           => $this->settings_manager->get_all_configs(),
			'role_assignments'  => $this->settings_manager->get_role_assignments(),
			'branding_icon'     => get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_BRANDING_ICON, array() ),
			'admin_footer'      => get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ADMIN_FOOTER, array() ),
			'color_scheme'      => get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_COLOR_SCHEME, array() ),
			'dashboard_message' => get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_DASHBOARD_MESSAGE, array() ),
			'access_mode'       => Agency_Pulse_Admin_Menu_Access_Control::get_mode(),
			'settings'          => $this->settings_manager->get_all_settings(),
			'role_manager'      => $this->build_role_manager_section(),
		);
	}

	/**
	 * Collect the current Role Manager state for the backup payload.
	 *
	 * Serialises every editable role's full capability map, the list of role
	 * slugs that hold ap_menu_manager as a role-level grant, and the list of
	 * users that hold ap_menu_manager as an explicit user-level override.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	private function build_role_manager_section(): array {
		global $wpdb;

		$cap     = Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS;
		$builtin = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );

		// All editable roles with their full capability maps.
		$roles       = array();
		$role_grants = array();

		foreach ( get_editable_roles() as $slug => $role_data ) {
			$caps = is_array( $role_data['capabilities'] ) ? $role_data['capabilities'] : array();

			$roles[ $slug ] = array(
				'display_name' => $role_data['name'],
				'is_builtin'   => in_array( $slug, $builtin, true ),
				'capabilities' => $caps,
			);

			if ( ! empty( $caps[ $cap ] ) ) {
				$role_grants[] = $slug;
			}
		}

		// Users with an explicit user-level cap grant (not inherited via their role).
		$user_query = new WP_User_Query( array(
			'meta_query' => array(
				array(
					'key'     => $wpdb->prefix . 'capabilities',
					'value'   => '"' . $cap . '"',
					'compare' => 'LIKE',
				),
			),
			'number' => 200,
		) );

		$user_grants = array();
		foreach ( $user_query->get_results() as $user ) {
			$user_grants[] = array(
				'user_login' => $user->user_login,
				'user_email' => $user->user_email,
			);
		}

		return array(
			'role_grants' => $role_grants,
			'user_grants' => $user_grants,
			'roles'       => $roles,
		);
	}

	// -------------------------------------------------------------------------
	// Shared: parse, validate, sanitize, and store imported data
	// -------------------------------------------------------------------------

	/**
	 * Decode a JSON string, sanitize its contents, and replace stored data.
	 *
	 * Used by both handle_import() and ajax_backup_pull().
	 *
	 * @since 1.0.0
	 * @param string $json Raw JSON string.
	 * @return array { type: 'success'|'error', message: string }
	 */
	public function import_from_json( string $json ): array {
		$data = json_decode( $json, true );

		if ( ! is_array( $data ) ) {
			return array(
				'type'    => 'error',
				'message' => __( 'Invalid JSON — could not parse the file.', 'agency-pulse-admin-menu' ),
			);
		}

		if ( ! isset( $data['configs'] ) || ! is_array( $data['configs'] ) ) {
			return array(
				'type'    => 'error',
				'message' => __( 'Invalid file — missing "configs" key.', 'agency-pulse-admin-menu' ),
			);
		}

		if ( ! isset( $data['role_assignments'] ) || ! is_array( $data['role_assignments'] ) ) {
			return array(
				'type'    => 'error',
				'message' => __( 'Invalid file — missing "role_assignments" key.', 'agency-pulse-admin-menu' ),
			);
		}

		$sanitized_configs = $this->sanitize_import_configs( $data['configs'] );

		if ( empty( $sanitized_configs ) ) {
			return array(
				'type'    => 'error',
				'message' => __( 'No valid configurations found in the file.', 'agency-pulse-admin-menu' ),
			);
		}

		$validated_assignments = $this->validate_role_assignments( $data['role_assignments'], $sanitized_configs );

		if ( ! $this->settings_manager->replace_all( $sanitized_configs, $validated_assignments ) ) {
			return array(
				'type'    => 'error',
				'message' => __( 'Could not save the imported configuration.', 'agency-pulse-admin-menu' ),
			);
		}

		// Restore admin customization settings when present in the payload.
		if ( isset( $data['branding_icon'] ) && is_array( $data['branding_icon'] ) ) {
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_BRANDING_ICON, $this->sanitize_branding_icon_data( $data['branding_icon'] ) );
		}
		if ( isset( $data['admin_footer'] ) && is_array( $data['admin_footer'] ) ) {
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ADMIN_FOOTER, $this->sanitize_admin_footer_data( $data['admin_footer'] ) );
		}
		if ( isset( $data['color_scheme'] ) && is_array( $data['color_scheme'] ) ) {
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_COLOR_SCHEME, $this->sanitize_color_scheme_data( $data['color_scheme'] ) );
		}
		if ( isset( $data['dashboard_message'] ) && is_array( $data['dashboard_message'] ) ) {
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_DASHBOARD_MESSAGE, $this->sanitize_dashboard_message_data( $data['dashboard_message'] ) );
		}

		return array(
			'type'    => 'success',
			'message' => sprintf(
				/* translators: %d: number of configurations imported */
				_n(
					'%d configuration imported successfully.',
					'%d configurations imported successfully.',
					count( $sanitized_configs ),
					'agency-pulse-admin-menu'
				),
				count( $sanitized_configs )
			),
		);
	}

	public function restore_from_backup( string $json ): array {
		$data = json_decode( $json, true );

		if ( ! is_array( $data ) ) {
			return array(
				'type'    => 'error',
				'message' => __( 'Invalid JSON — could not parse the backup.', 'agency-pulse-admin-menu' ),
			);
		}

		if ( ( $data['version'] ?? '' ) !== '2.0' || ( $data['plugin'] ?? '' ) !== 'agency-pulse-admin-menu' ) {
			return array(
				'type'    => 'error',
				'message' => __( 'This backup was created with an older version of the plugin. Push a fresh backup first, then restore.', 'agency-pulse-admin-menu' ),
			);
		}

		if ( ! isset( $data['configs'] ) || ! is_array( $data['configs'] ) ) {
			return array(
				'type'    => 'error',
				'message' => __( 'Invalid backup — missing "configs" key.', 'agency-pulse-admin-menu' ),
			);
		}

		if ( ! isset( $data['role_assignments'] ) || ! is_array( $data['role_assignments'] ) ) {
			return array(
				'type'    => 'error',
				'message' => __( 'Invalid backup — missing "role_assignments" key.', 'agency-pulse-admin-menu' ),
			);
		}

		$sanitized_configs     = $this->sanitize_import_configs( $data['configs'] );
		$validated_assignments = $this->validate_role_assignments( $data['role_assignments'], $sanitized_configs );

		if ( ! $this->settings_manager->replace_all( $sanitized_configs, $validated_assignments ) ) {
			return array(
				'type'    => 'error',
				'message' => __( 'Failed to save menu configurations.', 'agency-pulse-admin-menu' ),
			);
		}

		if ( isset( $data['branding_icon'] ) && is_array( $data['branding_icon'] ) ) {
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_BRANDING_ICON, $this->sanitize_branding_icon_data( $data['branding_icon'] ) );
		}

		if ( isset( $data['admin_footer'] ) && is_array( $data['admin_footer'] ) ) {
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ADMIN_FOOTER, $this->sanitize_admin_footer_data( $data['admin_footer'] ) );
		}

		if ( isset( $data['color_scheme'] ) && is_array( $data['color_scheme'] ) ) {
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_COLOR_SCHEME, $this->sanitize_color_scheme_data( $data['color_scheme'] ) );
		}

		if ( isset( $data['dashboard_message'] ) && is_array( $data['dashboard_message'] ) ) {
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_DASHBOARD_MESSAGE, $this->sanitize_dashboard_message_data( $data['dashboard_message'] ) );
		}

		if ( isset( $data['access_mode'] ) ) {
			$valid_modes = array(
				Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_DEFAULT,
				Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_ADMIN,
				Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY,
			);
			$mode = (string) $data['access_mode'];
			if ( in_array( $mode, $valid_modes, true ) ) {
				Agency_Pulse_Admin_Menu_Access_Control::set_mode( $mode );
			}
		}

		if ( isset( $data['settings'] ) && is_array( $data['settings'] ) ) {
			$safe_settings = array();
			if ( isset( $data['settings']['hide_builder_from_sidebar'] ) ) {
				$safe_settings['hide_builder_from_sidebar'] = (bool) $data['settings']['hide_builder_from_sidebar'];
			}
			if ( ! empty( $safe_settings ) ) {
				$this->settings_manager->save( $safe_settings );
			}
		}

		// Restore Role Manager state — optional section; absence is not an error.
		// access_mode has already been applied above, so capability maps written
		// here correctly overwrite any side-effect from entering cap_only mode.
		$rm_warnings = array();
		if ( isset( $data['role_manager'] ) && is_array( $data['role_manager'] ) ) {
			$rm_warnings = $this->restore_role_manager_section( $data['role_manager'] );
		}

		$message = __( 'Backup restored successfully.', 'agency-pulse-admin-menu' );
		if ( ! empty( $rm_warnings ) ) {
			$message .= ' ' . sprintf(
				/* translators: %s: comma-separated list of skipped user logins/emails */
				__( 'Some user access grants could not be matched and were skipped: %s', 'agency-pulse-admin-menu' ),
				implode( ', ', $rm_warnings )
			);
		}

		return array(
			'type'    => 'success',
			'message' => $message,
		);
	}

	/**
	 * Restore Role Manager state from a backup payload section.
	 *
	 * Applied after access_mode so that capability maps correctly overwrite any
	 * side-effect from entering cap_only mode (which removes ap_menu_manager
	 * from the administrator role).
	 *
	 * Steps:
	 *   1. Recreate missing cloned (non-builtin) roles.
	 *   2. Apply capability maps to all roles present in the payload.
	 *   3. Reapply role-level ap_menu_manager grants.
	 *   4. Apply user-level grants by login (fallback: email); skip+warn unmatched.
	 *
	 * @since 1.0.0
	 * @param array $rm Raw role_manager section from the backup payload.
	 * @return string[] Warning messages for any user grants that could not be matched.
	 */
	private function restore_role_manager_section( array $rm ): array {
		$warnings = array();
		$builtin  = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
		$cap      = Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS;

		$roles_payload  = isset( $rm['roles'] ) && is_array( $rm['roles'] ) ? $rm['roles'] : array();
		$role_grants    = isset( $rm['role_grants'] ) && is_array( $rm['role_grants'] ) ? $rm['role_grants'] : array();
		$user_grants    = isset( $rm['user_grants'] ) && is_array( $rm['user_grants'] ) ? $rm['user_grants'] : array();

		// Step 1 — recreate missing cloned (non-builtin) roles.
		foreach ( $roles_payload as $raw_slug => $role_data ) {
			$slug = sanitize_key( $raw_slug );
			if ( empty( $slug ) || in_array( $slug, $builtin, true ) ) {
				continue;
			}
			if ( null === get_role( $slug ) ) {
				$display_name = isset( $role_data['display_name'] )
					? sanitize_text_field( (string) $role_data['display_name'] )
					: $slug;
				add_role( $slug, $display_name, array() );
			}
		}

		// Step 2 — apply capability maps to all backed-up roles.
		foreach ( $roles_payload as $raw_slug => $role_data ) {
			$slug = sanitize_key( $raw_slug );
			if ( empty( $slug ) ) {
				continue;
			}
			$role_obj = get_role( $slug );
			if ( null === $role_obj ) {
				continue; // recreate step only handles non-builtins; skip if still missing
			}
			$caps = isset( $role_data['capabilities'] ) && is_array( $role_data['capabilities'] )
				? $role_data['capabilities']
				: array();
			foreach ( $caps as $raw_cap => $granted ) {
				$cap_name = sanitize_key( $raw_cap );
				if ( empty( $cap_name ) ) {
					continue;
				}
				if ( $granted ) {
					$role_obj->add_cap( $cap_name, true );
				} else {
					$role_obj->remove_cap( $cap_name );
				}
			}
		}

		// Step 3 — reapply role-level ap_menu_manager grants.
		// Step 2 already wrote the full capability map so this is normally a no-op,
		// but the role_grants list acts as an explicit safety net for the key cap.
		foreach ( $role_grants as $raw_slug ) {
			$slug     = sanitize_key( (string) $raw_slug );
			$role_obj = $slug ? get_role( $slug ) : null;
			if ( $role_obj ) {
				$role_obj->add_cap( $cap, true );
			}
		}

		// Step 4 — user-level ap_menu_manager grants.
		foreach ( $user_grants as $entry ) {
			if ( ! is_array( $entry ) ) {
				continue;
			}
			$login = isset( $entry['user_login'] ) ? sanitize_user( (string) $entry['user_login'] ) : '';
			$email = isset( $entry['user_email'] ) ? sanitize_email( (string) $entry['user_email'] ) : '';

			$user = null;
			if ( $login ) {
				$user = get_user_by( 'login', $login );
			}
			if ( ! $user && $email ) {
				$user = get_user_by( 'email', $email );
			}

			if ( $user ) {
				$user->add_cap( $cap, true );
			} else {
				$identifier = $login ?: $email ?: __( '(unknown)', 'agency-pulse-admin-menu' );
				$warnings[] = $identifier;
			}
		}

		return $warnings;
	}

	/**
	 * Sanitize the raw configs array from an import payload.
	 *
	 * Records that fail basic validation are silently dropped.
	 *
	 * @since 1.0.0
	 * @param array $raw_configs
	 * @return array Keyed by sanitized config ID.
	 */
	private function sanitize_import_configs( array $raw_configs ): array {
		$sanitized = array();

		foreach ( $raw_configs as $raw_id => $raw_config ) {
			if ( ! is_array( $raw_config ) ) {
				continue;
			}

			$id = sanitize_key( $raw_id );
			if ( empty( $id ) ) {
				continue;
			}

			$name = sanitize_text_field( (string) ( $raw_config['name'] ?? '' ) );
			if ( '' === $name || mb_strlen( $name ) > 100 ) {
				continue;
			}

			$sanitized[ $id ] = array(
				'id'      => $id,
				'name'    => $name,
				'created' => sanitize_text_field( (string) ( $raw_config['created'] ?? '' ) ),
				'items'   => $this->sanitize_import_items(
					isset( $raw_config['items'] ) && is_array( $raw_config['items'] )
						? $raw_config['items']
						: array()
				),
			);
		}

		return $sanitized;
	}

	/**
	 * Sanitize an items array from an imported config record.
	 *
	 * @since 1.0.0
	 * @param array $raw_items
	 * @return array
	 */
	private function sanitize_import_items( array $raw_items ): array {
		$sanitized = array();

		foreach ( $raw_items as $raw_item ) {
			if ( ! is_array( $raw_item ) ) {
				continue;
			}

			$slug             = sanitize_text_field( (string) ( $raw_item['slug'] ?? '' ) );
			$custom_container = ! empty( $raw_item['custom_container'] );

			if ( '' === $slug ) {
				continue;
			}

			if ( $custom_container && ! preg_match( '/^aamo_container_\d+$/', $slug ) ) {
				continue;
			}

			$label = null;
			if ( isset( $raw_item['label'] ) && null !== $raw_item['label'] ) {
				$label = sanitize_text_field( (string) $raw_item['label'] );
				$label = '' === $label ? null : $label;
			}

			$icon = null;
			if ( $custom_container && isset( $raw_item['icon'] ) && null !== $raw_item['icon'] ) {
				$icon = sanitize_text_field( (string) $raw_item['icon'] );
				$icon = '' === $icon ? null : $icon;
			}

			$item = array(
				'slug'             => $slug,
				'position'         => absint( $raw_item['position'] ?? 0 ),
				'label'            => $label,
				'hidden'           => ! empty( $raw_item['hidden'] ),
				'separator'        => ! empty( $raw_item['separator'] ),
				'custom_container' => $custom_container,
				'icon'             => $icon,
				'children'         => array(),
			);

			if ( ! empty( $raw_item['children'] ) && is_array( $raw_item['children'] ) ) {
				foreach ( $raw_item['children'] as $raw_child ) {
					if ( ! is_array( $raw_child ) ) {
						continue;
					}

					$child_slug = sanitize_text_field( (string) ( $raw_child['slug'] ?? '' ) );
					if ( '' === $child_slug ) {
						continue;
					}

					$child_label = null;
					if ( isset( $raw_child['label'] ) && null !== $raw_child['label'] ) {
						$child_label = sanitize_text_field( (string) $raw_child['label'] );
						$child_label = '' === $child_label ? null : $child_label;
					}

					$item['children'][] = array(
						'slug'     => $child_slug,
						'position' => absint( $raw_child['position'] ?? 0 ),
						'label'    => $child_label,
						'hidden'   => ! empty( $raw_child['hidden'] ),
					);
				}
			}

			$sanitized[] = $item;
		}

		return $sanitized;
	}

	/**
	 * Validate a role assignments map from an import payload.
	 *
	 * Role slugs must be registered WordPress roles. Config IDs must exist in
	 * the already-sanitized configs being imported (not the currently stored
	 * configs). Invalid entries are silently dropped.
	 *
	 * @since 1.0.0
	 * @param array $raw_assignments    Role-slug => config-ID from the payload.
	 * @param array $sanitized_configs  The sanitized configs that will be stored.
	 * @return array Validated role-slug => config-ID map.
	 */
	private function validate_role_assignments( array $raw_assignments, array $sanitized_configs ): array {
		$valid_roles   = array_keys( wp_roles()->get_names() );
		$valid_configs = array_keys( $sanitized_configs );
		$validated     = array();

		foreach ( $raw_assignments as $role => $config_id ) {
			$role      = sanitize_key( (string) $role );
			$config_id = sanitize_key( (string) $config_id );

			if ( ! in_array( $role, $valid_roles, true ) ) {
				continue;
			}
			if ( ! in_array( $config_id, $valid_configs, true ) ) {
				continue;
			}

			$validated[ $role ] = $config_id;
		}

		return $validated;
	}

	// -------------------------------------------------------------------------
	// Access Control AJAX handlers
	// -------------------------------------------------------------------------

	/**
	 * AJAX: Set the plugin access mode.
	 *
	 * @since 1.0.0
	 */
	public function ajax_set_access_mode(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_SET_ACCESS_MODE, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$mode        = sanitize_key( wp_unslash( $_POST['mode'] ?? '' ) );
		$valid_modes = array(
			Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_DEFAULT,
			Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_ADMIN,
			Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY,
		);

		if ( ! in_array( $mode, $valid_modes, true ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid access mode.', 'agency-pulse-admin-menu' ) ) );
		}

		if ( Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY === $mode
			 && ! Agency_Pulse_Admin_Menu_Access_Control::can_enable_cap_only() ) {
			wp_send_json_error( array( 'message' => __( 'Grant the permission to at least one user before enabling this mode.', 'agency-pulse-admin-menu' ) ) );
		}

		if ( ! Agency_Pulse_Admin_Menu_Access_Control::set_mode( $mode ) ) {
			wp_send_json_error( array( 'message' => __( 'Could not save the access mode.', 'agency-pulse-admin-menu' ) ) );
		}

		// Warn if the current admin would lose access after a page reload.
		$will_lose_access = false;
		if ( Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY === $mode ) {
			$holder_ids = array_map( function ( $u ) {
				return (int) $u->ID;
			}, Agency_Pulse_Admin_Menu_Access_Control::get_explicit_holders() );
			$will_lose_access = ! in_array( get_current_user_id(), $holder_ids, true );
		}

		wp_send_json_success( array(
			'message'        => __( 'Access mode updated.', 'agency-pulse-admin-menu' ),
			'will_lose_access' => $will_lose_access,
		) );
	}

	/**
	 * AJAX: Grant ap_menu_manager to a user.
	 *
	 * @since 1.0.0
	 */
	public function ajax_grant_manager_cap(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_GRANT_MANAGER_CAP, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$user_id = (int) ( $_POST['user_id'] ?? 0 );

		if ( ! $user_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid user.', 'agency-pulse-admin-menu' ) ) );
		}

		if ( ! Agency_Pulse_Admin_Menu_Access_Control::grant( $user_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Could not grant permission. User not found.', 'agency-pulse-admin-menu' ) ) );
		}

		wp_send_json_success( array(
			'message'          => __( 'Permission granted.', 'agency-pulse-admin-menu' ),
			'holders'          => $this->get_holders_for_response(),
			'canEnableCapOnly' => Agency_Pulse_Admin_Menu_Access_Control::can_enable_cap_only() ? 1 : 0,
		) );
	}

	/**
	 * AJAX: Revoke ap_menu_manager from a user.
	 *
	 * @since 1.0.0
	 */
	public function ajax_revoke_manager_cap(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_REVOKE_MANAGER_CAP, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$user_id = (int) ( $_POST['user_id'] ?? 0 );

		if ( ! $user_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid user.', 'agency-pulse-admin-menu' ) ) );
		}

		$result = Agency_Pulse_Admin_Menu_Access_Control::revoke( $user_id );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		if ( ! $result ) {
			wp_send_json_error( array( 'message' => __( 'Could not revoke permission. User not found.', 'agency-pulse-admin-menu' ) ) );
		}

		wp_send_json_success( array(
			'message'          => __( 'Permission revoked.', 'agency-pulse-admin-menu' ),
			'holders'          => $this->get_holders_for_response(),
			'canEnableCapOnly' => Agency_Pulse_Admin_Menu_Access_Control::can_enable_cap_only() ? 1 : 0,
		) );
	}

	/**
	 * Serialize explicit holders into the shape expected by the settings JS.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	private function get_holders_for_response(): array {
		return array_map( function ( WP_User $user ) {
			return array(
				'id'    => (int) $user->ID,
				'name'  => $user->display_name,
				'email' => $user->user_email,
			);
		}, Agency_Pulse_Admin_Menu_Access_Control::get_explicit_holders() );
	}

	// -------------------------------------------------------------------------
	// Dashboard Message AJAX handler
	// -------------------------------------------------------------------------

	/**
	 * AJAX: Save the dashboard message widget settings.
	 *
	 * @since 1.0.0
	 */
	public function ajax_save_dashboard_message(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_DASHBOARD_MSG, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$data = array(
			'enabled'    => ! empty( $_POST['enabled'] ) && '1' === $_POST['enabled'],
			'show_first' => ! empty( $_POST['show_first'] ) && '1' === $_POST['show_first'],
			'title'      => sanitize_text_field( wp_unslash( $_POST['title'] ?? '' ) ),
			'content'    => wp_kses_post( wp_unslash( $_POST['content'] ?? '' ) ),
		);

		if ( ! update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_DASHBOARD_MESSAGE, $data ) ) {
			// update_option returns false both on DB error and when the value is unchanged.
			// Treat unchanged as success so the UI doesn't mislead the user.
			if ( get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_DASHBOARD_MESSAGE ) !== $data ) {
				wp_send_json_error( array( 'message' => __( 'Could not save settings.', 'agency-pulse-admin-menu' ) ) );
			}
		}

		wp_send_json_success( array( 'message' => __( 'Settings saved.', 'agency-pulse-admin-menu' ) ) );
	}

	// -------------------------------------------------------------------------
	// Utility
	// -------------------------------------------------------------------------

	/**
	 * Persist an import result for display on the next page load.
	 *
	 * Stored as a 30-second transient keyed to the current user so concurrent
	 * admin sessions don't mix up notices.
	 *
	 * @since 1.0.0
	 * @param string $type    'success' or 'error'.
	 * @param string $message Human-readable notice text.
	 */
	private function set_import_result( string $type, string $message ): void {
		set_transient(
			'aamo_import_result_' . get_current_user_id(),
			array( 'type' => $type, 'message' => $message ),
			30
		);
	}

	// -------------------------------------------------------------------------
	// Reset Customization
	// -------------------------------------------------------------------------

	/**
	 * Reset admin customization options, with optional menu data clearing.
	 *
	 * Always writes factory defaults for the four customization options
	 * (branding icon, admin footer, color scheme, dashboard message).
	 *
	 * Optional flags (evaluated in precedence order):
	 *   $nuke_menus   — wipes all configs and role assignments entirely;
	 *                   supersedes $reset_menu and $reset_labels.
	 *   $reset_labels — restores every item's title and icon to its original
	 *                   WordPress value across all configs.
	 *   $reset_menu   — clears the items array from every config (shells kept).
	 *
	 * No auth checks — callers are responsible for verifying permissions.
	 *
	 * @since 1.0.0
	 * @param bool $reset_menu   Clear item arrangements from all configs.
	 * @param bool $reset_labels Restore item titles/icons to WP originals.
	 * @param bool $nuke_menus   Wipe all configs and role assignments entirely.
	 * @return string[] Option key suffixes that were reset.
	 */
	public function perform_reset_customization(
		bool $reset_menu,
		bool $reset_labels = false,
		bool $nuke_menus   = false
	): array {
		// ── Always: reset the four visual customization options ───────────────
		update_option(
			Agency_Pulse_Admin_Menu_Constants::OPTION_BRANDING_ICON,
			array(
				'enabled'      => false,
				'icon_url'     => '',
				'icon_alt'     => '',
				'link_mode'    => 'wordpress',
				'link_url'     => '',
				'link_target'  => false,
				'hide_submenu' => false,
				'site_name'    => '',
			)
		);

		update_option(
			Agency_Pulse_Admin_Menu_Constants::OPTION_ADMIN_FOOTER,
			array(
				'enabled'      => false,
				'left_content' => '',
				'show_version' => true,
				'right_text'   => '',
			)
		);

		$color_defaults            = $this->color_defaults();
		$color_defaults['enabled'] = false;
		update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_COLOR_SCHEME, $color_defaults );

		update_option(
			Agency_Pulse_Admin_Menu_Constants::OPTION_DASHBOARD_MESSAGE,
			array(
				'enabled'    => false,
				'show_first' => false,
				'title'      => '',
				'content'    => '',
			)
		);

		$reset = array( 'branding_icon', 'admin_footer', 'color_scheme', 'dashboard_message' );

		// ── Nuke: wipe all configs and role assignments; supersedes the rest ──
		if ( $nuke_menus ) {
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_CONFIGS, array() );
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ROLE_ASSIGNMENTS, array() );
			$reset[] = 'menu_configs';
			$reset[] = 'role_assignments';
			return $reset;
		}

		// ── Labels: restore original titles and icons across all configs ──────
		if ( $reset_labels ) {
			$configs = $this->settings_manager->get_all_configs();
			foreach ( $configs as &$config ) {
				if ( empty( $config['items'] ) ) {
					continue;
				}
				foreach ( $config['items'] as &$item ) {
					if ( ! empty( $item['original_title'] ) ) {
						$item['title'] = $item['original_title'];
					}
					if ( ! empty( $item['original_icon'] ) ) {
						$item['icon'] = $item['original_icon'];
					}
					if ( ! empty( $item['children'] ) ) {
						foreach ( $item['children'] as &$child ) {
							if ( ! empty( $child['original_title'] ) ) {
								$child['title'] = $child['original_title'];
							}
						}
						unset( $child );
					}
				}
				unset( $item );
			}
			unset( $config );
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_CONFIGS, $configs );
			$reset[] = 'menu_labels';
		}

		// ── Menu items: clear item arrangements (shells preserved) ────────────
		if ( $reset_menu ) {
			$configs = $this->settings_manager->get_all_configs();
			foreach ( $configs as &$config ) {
				$config['items'] = array();
			}
			unset( $config );
			update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_CONFIGS, $configs );
			$reset[] = 'menu_configs';
		}

		return $reset;
	}

	/**
	 * AJAX: Reset admin customization settings to defaults.
	 *
	 * @since 1.0.0
	 */
	public function ajax_reset_customization(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_CUSTOMIZATION, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$reset_menu   = ! empty( $_POST['reset_menu'] )   && '1' === $_POST['reset_menu'];
		$reset_labels = ! empty( $_POST['reset_labels'] ) && '1' === $_POST['reset_labels'];
		$nuke_menus   = ! empty( $_POST['nuke_menus'] )   && '1' === $_POST['nuke_menus'];

		$reset = $this->perform_reset_customization( $reset_menu, $reset_labels, $nuke_menus );

		wp_send_json_success( array( 'reset' => $reset ) );
	}

	// -------------------------------------------------------------------------
	// Import sanitizers — shared by import_from_json() for each option type
	// -------------------------------------------------------------------------

	/**
	 * Sanitize a raw branding icon option array from an import payload.
	 *
	 * @since 1.0.0
	 * @param array $raw
	 * @return array
	 */
	private function sanitize_branding_icon_data( array $raw ): array {
		$valid_link_modes = array( 'wordpress', 'custom', 'none' );
		$link_mode        = sanitize_key( (string) ( $raw['link_mode'] ?? 'wordpress' ) );
		if ( ! in_array( $link_mode, $valid_link_modes, true ) ) {
			$link_mode = 'wordpress';
		}
		return array(
			'site_name'    => sanitize_text_field( (string) ( $raw['site_name'] ?? '' ) ),
			'enabled'      => ! empty( $raw['enabled'] ),
			'icon_url'     => esc_url_raw( (string) ( $raw['icon_url'] ?? '' ) ),
			'icon_alt'     => sanitize_text_field( (string) ( $raw['icon_alt'] ?? '' ) ),
			'link_mode'    => $link_mode,
			'link_url'     => esc_url_raw( (string) ( $raw['link_url'] ?? '' ) ),
			'link_target'  => ! empty( $raw['link_target'] ),
			'hide_submenu' => ! empty( $raw['hide_submenu'] ),
		);
	}

	/**
	 * Sanitize a raw admin footer option array from an import payload.
	 *
	 * @since 1.0.0
	 * @param array $raw
	 * @return array
	 */
	private function sanitize_admin_footer_data( array $raw ): array {
		return array(
			'enabled'      => ! empty( $raw['enabled'] ),
			'left_content' => wp_kses_post( (string) ( $raw['left_content'] ?? '' ) ),
			'show_version' => ! empty( $raw['show_version'] ),
			'right_text'   => sanitize_text_field( (string) ( $raw['right_text'] ?? '' ) ),
		);
	}

	/**
	 * Sanitize a raw color scheme option array from an import payload.
	 *
	 * @since 1.0.0
	 * @param array $raw
	 * @return array
	 */
	private function sanitize_color_scheme_data( array $raw ): array {
		$defaults = $this->color_defaults();
		$data     = array( 'enabled' => ! empty( $raw['enabled'] ) );
		foreach ( array_keys( $defaults ) as $slot ) {
			$val           = sanitize_hex_color( (string) ( $raw[ $slot ] ?? '' ) );
			$data[ $slot ] = '' !== $val ? $val : $defaults[ $slot ];
		}
		return $data;
	}

	/**
	 * Sanitize a raw dashboard message option array from an import payload.
	 *
	 * @since 1.0.0
	 * @param array $raw
	 * @return array
	 */
	private function sanitize_dashboard_message_data( array $raw ): array {
		return array(
			'enabled'    => ! empty( $raw['enabled'] ),
			'show_first' => ! empty( $raw['show_first'] ),
			'title'      => sanitize_text_field( (string) ( $raw['title'] ?? '' ) ),
			'content'    => wp_kses_post( (string) ( $raw['content'] ?? '' ) ),
		);
	}

	// -------------------------------------------------------------------------
	// Admin Layout
	// -------------------------------------------------------------------------

	/**
	 * Default values for all layout settings (all four phases).
	 *
	 * @since 1.0.0
	 * @return array
	 */
	private function layout_defaults(): array {
		return array(
			'enabled'             => false,
			'sidebar_style_enabled' => true,
			'content_cards_enabled' => true,
			'interactive_enabled'   => true,
			'typography_enabled'    => true,
			'sidebar_style'       => 'default',
			'sidebar_item_radius' => 6,
			'content_bg'          => '#f0f0f1',
			'card_radius'         => 0,
			'card_shadow'         => false,
			'card_shadow_opacity' => 15,
			'card_shadow_offset'  => 2,
			'card_shadow_blur'    => 8,
			'button_radius'       => 3,
			'input_radius'        => 4,
			'font_source'         => 'system',
			'font_family'         => '',
			'font_url'            => '',
		);
	}

	/**
	 * AJAX: Save all layout settings.
	 *
	 * @since 1.0.0
	 */
	public function ajax_save_layout(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_LAYOUT, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$valid_styles = array( 'default', 'border_accent', 'pill' );
		$sidebar_style = sanitize_key( wp_unslash( $_POST['sidebar_style'] ?? 'default' ) );
		if ( ! in_array( $sidebar_style, $valid_styles, true ) ) {
			$sidebar_style = 'default';
		}

		$valid_font_sources = array( 'system', 'custom' );
		$font_source        = sanitize_key( wp_unslash( $_POST['font_source'] ?? 'system' ) );
		if ( ! in_array( $font_source, $valid_font_sources, true ) ) {
			$font_source = 'system';
		}

		$font_url = esc_url_raw( wp_unslash( $_POST['font_url'] ?? '' ) );
		if ( '' !== $font_url && 0 !== strpos( $font_url, 'https://' ) ) {
			$font_url = '';
		}

		$data = array(
			'enabled'               => ! empty( $_POST['enabled'] ) && '1' === $_POST['enabled'],
			'sidebar_style_enabled' => ! empty( $_POST['sidebar_style_enabled'] ) && '1' === $_POST['sidebar_style_enabled'],
			'content_cards_enabled' => ! empty( $_POST['content_cards_enabled'] ) && '1' === $_POST['content_cards_enabled'],
			'interactive_enabled'   => ! empty( $_POST['interactive_enabled'] ) && '1' === $_POST['interactive_enabled'],
			'typography_enabled'    => ! empty( $_POST['typography_enabled'] ) && '1' === $_POST['typography_enabled'],
			'sidebar_style'       => $sidebar_style,
			'sidebar_item_radius' => min( 24, max( 0, absint( $_POST['sidebar_item_radius'] ?? 6 ) ) ),
			'content_bg'          => sanitize_hex_color( wp_unslash( $_POST['content_bg'] ?? '' ) ) ?: '#f0f0f1',
			'card_radius'         => min( 16, max( 0, absint( $_POST['card_radius'] ?? 0 ) ) ),
			'card_shadow'         => ! empty( $_POST['card_shadow'] ) && '1' === $_POST['card_shadow'],
			'card_shadow_opacity' => min( 50, max( 0, absint( $_POST['card_shadow_opacity'] ?? 15 ) ) ),
			'card_shadow_offset'  => min( 20, max( 0, absint( $_POST['card_shadow_offset']  ?? 2  ) ) ),
			'card_shadow_blur'    => min( 20, max( 0, absint( $_POST['card_shadow_blur']     ?? 8  ) ) ),
			'button_radius'       => min( 12, max( 0, absint( $_POST['button_radius'] ?? 3 ) ) ),
			'input_radius'        => min( 12, max( 0, absint( $_POST['input_radius'] ?? 4 ) ) ),
			'font_source'         => $font_source,
			'font_family'         => sanitize_text_field( wp_unslash( $_POST['font_family'] ?? '' ) ),
			'font_url'            => $font_url,
		);

		update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_LAYOUT, $data );
		wp_send_json_success( array( 'message' => __( 'Layout settings saved.', 'agency-pulse-admin-menu' ) ) );
	}

	/**
	 * AJAX: Reset all layout settings to defaults.
	 *
	 * @since 1.0.0
	 */
	public function ajax_reset_layout(): void {
		check_ajax_referer( Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_LAYOUT, 'nonce' );

		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			wp_send_json_error( array( 'message' => __( 'License & Connection is not active.', 'agency-pulse-admin-menu' ) ), 403 );
		}

		$defaults = $this->layout_defaults();
		update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_LAYOUT, $defaults );
		wp_send_json_success( array(
			'message'  => __( 'Layout settings reset to defaults.', 'agency-pulse-admin-menu' ),
			'settings' => $defaults,
		) );
	}
}
