<?php
/**
 * Console Sync — pull workflow packs directly from the IntakeFlow Console.
 *
 * @package XPressUI_Bridge_Pro
 */

defined( 'ABSPATH' ) || exit;

// ---------------------------------------------------------------------------
// Settings helpers
// ---------------------------------------------------------------------------

function xpressui_get_console_connection(): array {
	// `ownerUid` is the workspace owner identifier used in hosted/builder links
	// (/hosted/{ownerUid}/{slug}, console /#/project/{ownerUid}/{slug}); it is resolved
	// from the Console `GET /api/v1/me` when the connection is saved.
	$defaults = [ 'apiUrl' => 'https://app.intakeflow.dev', 'apiToken' => '', 'ownerUid' => '' ];
	$stored   = get_option( 'xpressui_console_connection', [] );
	$conn     = is_array( $stored ) ? array_merge( $defaults, $stored ) : $defaults;
	if ( defined( 'XPRESSUI_CONSOLE_API_URL' ) ) {
		$conn['apiUrl'] = XPRESSUI_CONSOLE_API_URL;
	} elseif ( empty( $conn['apiUrl'] ) ) {
		$conn['apiUrl'] = 'https://app.intakeflow.dev';
	}
	return $conn;
}

function xpressui_get_wordpress_connect_url( string $return_page_url = '' ): string {
	return admin_url( 'edit.php?post_type=xpressui_submission&page=xpressui-settings' );
}

/**
 * Builds the "Edit on IntakeFlow" Console URL for a workflow.
 *
 * The console builder route is `/workflow/{ownerUid}/{slug}` (the `:userId` segment is
 * the workspace owner_uid, stored on the connection). Falls back to the Console root
 * when the owner_uid is missing (connection not re-saved since the upgrade) or no slug.
 *
 * @param string $slug Workflow slug.
 * @return string
 */
function xpressui_console_workflow_url( string $slug ): string {
	$conn  = xpressui_get_console_connection();
	$base  = trailingslashit( (string) $conn['apiUrl'] );
	$owner = (string) ( $conn['ownerUid'] ?? '' );
	if ( '' === $owner || '' === $slug ) {
		return $base;
	}
	return $base . 'workflow/' . rawurlencode( $owner ) . '/' . rawurlencode( $slug );
}

/**
 * Builds the "Edit on IntakeFlow" Console URL for a hosted link.
 *
 * The console route is `/hosted-links/{linkId}/customize` (scoped by the logged-in
 * session, so no owner_uid is needed).
 *
 * @param string $link_id Hosted link id.
 * @return string
 */
function xpressui_console_hosted_link_url( string $link_id ): string {
	$conn = xpressui_get_console_connection();
	$base = trailingslashit( (string) $conn['apiUrl'] );
	if ( '' === $link_id ) {
		return $base;
	}
	return $base . 'hosted-links/' . rawurlencode( $link_id ) . '/customize';
}

/**
 * Fetches a short-lived embed ticket from the Console `POST /api/v1/auth/embed-ticket`
 * (authenticated by the stored API token). The ticket is passed to the console iframe in
 * the URL fragment so the SPA can exchange it for a session (SSO) without exposing the
 * long-lived token to the browser.
 *
 * @param string $api_url   Console base URL.
 * @param string $api_token API token.
 * @return string The ticket, or '' if it could not be obtained.
 */
function xpressui_fetch_console_embed_ticket( string $api_url, string $api_token ): string {
	if ( '' === $api_url || '' === $api_token ) {
		return '';
	}
	$response = wp_remote_post(
		trailingslashit( $api_url ) . 'api/v1/auth/embed-ticket',
		[
			'headers' => [
				'X-Api-Token' => $api_token,
				'Accept'      => 'application/json',
			],
			'timeout' => 15,
		]
	);
	if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
		return '';
	}
	$body = json_decode( wp_remote_retrieve_body( $response ), true );
	return is_array( $body ) ? sanitize_text_field( (string) ( $body['ticket'] ?? '' ) ) : '';
}

function xpressui_render_console_connection_form(): void {
	$conn = xpressui_get_console_connection();
	?>
	<form id="xpressui-console-connection-form" method="post">
		<?php wp_nonce_field( 'xpressui_console_connection_action', 'xpressui_console_connection_nonce' ); ?>
		<input type="hidden" name="xpressui_save_console_connection" value="1">
		<table class="form-table" role="presentation">
			<tr>
				<th><label for="xpressui_console_api_url"><?php esc_html_e( 'Console API URL', 'xpressui-bridge' ); ?></label></th>
				<td>
					<input type="url" id="xpressui_console_api_url" name="xpressui_console_api_url"
						value="<?php echo esc_url( $conn['apiUrl'] ); ?>"
						class="regular-text"<?php echo defined( 'XPRESSUI_CONSOLE_API_URL' ) ? ' readonly' : ''; ?>>
					<p class="description"><?php esc_html_e( 'Base URL of your IntakeFlow Console instance.', 'xpressui-bridge' ); ?></p>
				</td>
			</tr>
			<tr>
				<th><label for="xpressui_console_api_token"><?php esc_html_e( 'API Token', 'xpressui-bridge' ); ?></label></th>
				<td>
					<input type="password" id="xpressui_console_api_token" name="xpressui_console_api_token"
						value="<?php echo esc_attr( $conn['apiToken'] ); ?>"
						class="regular-text" autocomplete="off">
					<p class="description">
						<?php esc_html_e( 'Generate a token in the Console under Profile > API Tokens.', 'xpressui-bridge' ); ?>
					</p>
				</td>
			</tr>
		</table>
		<?php submit_button( __( 'Save Connection', 'xpressui-bridge' ), 'secondary', 'submit', false ); ?>
		<span class="xpressui-ajax-status" style="margin-left:1rem;vertical-align:middle"></span>
	</form>
	<?php
}

// ---------------------------------------------------------------------------
// AJAX: save console connection
// ---------------------------------------------------------------------------

add_action( 'wp_ajax_xpressui_save_console_connection', 'xpressui_ajax_save_console_connection' );

function xpressui_ajax_save_console_connection(): void {
	check_ajax_referer( 'xpressui_console_connection_action', 'xpressui_console_connection_nonce' );
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'xpressui-bridge' ) ], 403 );
	}
		$api_url   = isset( $_POST['xpressui_console_api_url'] ) ? esc_url_raw( trim( sanitize_text_field( wp_unslash( (string) $_POST['xpressui_console_api_url'] ) ) ) ) : '';
	$api_token = sanitize_text_field( wp_unslash( (string) ( $_POST['xpressui_console_api_token'] ?? '' ) ) );
	// Resolve the workspace owner_uid for this token (the identifier used in hosted /
	// builder links) and store it alongside the connection so links can be rebuilt
	// locally without a per-project round-trip.
	$owner_uid = xpressui_fetch_console_owner_uid( $api_url, $api_token );

	update_option( 'xpressui_console_connection', [
		'apiUrl'   => $api_url,
		'apiToken' => $api_token,
		'ownerUid' => $owner_uid,
	] );
	delete_transient( 'xpressui_console_api_health' );
	wp_send_json_success( [
		'message'  => __( 'Console connection saved.', 'xpressui-bridge' ),
		'ownerUid' => $owner_uid,
	] );
}

/**
 * Fetches the workspace owner_uid for an API token from the Console `GET /api/v1/me`.
 *
 * @param string $api_url   Console base URL.
 * @param string $api_token API token.
 * @return string The ownerUid, or '' if it could not be resolved (bad token / network).
 */
function xpressui_fetch_console_owner_uid( string $api_url, string $api_token ): string {
	if ( '' === $api_url || '' === $api_token ) {
		return '';
	}
	$response = wp_remote_get(
		trailingslashit( $api_url ) . 'api/v1/me',
		[
			'headers' => [
				'X-Api-Token' => $api_token,
				'Accept'      => 'application/json',
			],
			'timeout' => 15,
		]
	);
	if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
		return '';
	}
	$body = json_decode( wp_remote_retrieve_body( $response ), true );
	return is_array( $body ) ? sanitize_text_field( (string) ( $body['ownerUid'] ?? '' ) ) : '';
}

// ---------------------------------------------------------------------------
// AJAX: list projects from the Console
// ---------------------------------------------------------------------------

add_action( 'wp_ajax_xpressui_console_list_projects', 'xpressui_ajax_console_list_projects' );

function xpressui_ajax_console_list_projects(): void {
	check_ajax_referer( 'xpressui_console_sync_nonce', 'nonce' );

	if ( ! current_user_can( 'manage_options' ) ) {
		wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'xpressui-bridge' ) ], 403 );
	}

	if ( ! xpressui_is_saas_connected() ) {
		wp_send_json_error( [ 'message' => __( 'Console Sync requires an active Console Connection.', 'xpressui-bridge' ) ], 403 );
	}

	$conn = xpressui_get_console_connection();
	if ( empty( $conn['apiUrl'] ) || empty( $conn['apiToken'] ) ) {
		wp_send_json_error( [ 'message' => __( 'Console connection not configured. Save your API URL and token first.', 'xpressui-bridge' ) ] );
	}

	$response = wp_remote_get(
		trailingslashit( $conn['apiUrl'] ) . 'api/v1/projects',
		[
			'headers' => [
				'X-Api-Token' => $conn['apiToken'],
				'Accept'      => 'application/json',
			],
			'timeout' => 15,
		]
	);

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

	$code = wp_remote_retrieve_response_code( $response );
	if ( 200 !== $code ) {
		/* translators: %d: HTTP status code returned by the Console API */
		wp_send_json_error( [ 'message' => sprintf( __( 'Console API returned status %d. Check your API URL and token.', 'xpressui-bridge' ), $code ) ] );
	}

	$body = json_decode( wp_remote_retrieve_body( $response ), true );
	$projects = is_array( $body['items'] ?? null ) ? $body['items'] : [];

	// Detect and delete locally installed workflows that are no longer present in Console API
	$installed_slugs = xpressui_get_installed_workflow_slugs();
	$api_slugs       = [];
	foreach ( $projects as $p ) {
		if ( ! empty( $p['slug'] ) ) {
			$api_slugs[] = sanitize_title( (string) $p['slug'] );
		}
	}
	foreach ( $installed_slugs as $slug ) {
		if ( xpressui_is_bundled_workflow( $slug ) ) {
			continue;
		}
		// Never prune locally-imported / offline workflows (Contact Form 7 / Gravity Forms
		// imports, or the importer's "Local" fallback). They were never created on the
		// Console, so their absence from the API list does NOT mean they were deleted there.
		// Pruning them on first connect/sync silently lost the user's imported forms.
		if ( xpressui_workflow_is_local_only( $slug ) ) {
			continue;
		}
		if ( ! in_array( $slug, $api_slugs, true ) ) {
			xpressui_delete_workflow( $slug, [
				'preserve_project_settings' => false,
				'mark_user_deleted'         => false,
			] );
		}
	}

	wp_send_json_success( [ 'projects' => $projects ] );
}

// ---------------------------------------------------------------------------
// AJAX: sync (download + install) one project
// ---------------------------------------------------------------------------

add_action( 'wp_ajax_xpressui_console_sync_project', 'xpressui_ajax_console_sync_project' );

function xpressui_sync_project( string $project_id ) {
	if ( ! xpressui_is_saas_connected() ) {
		return new WP_Error( 'xpressui_sync_inactive_license', __( 'Console Sync requires an active Console Connection.', 'xpressui-bridge' ) );
	}

	$conn = xpressui_get_console_connection();
	if ( empty( $conn['apiUrl'] ) || empty( $conn['apiToken'] ) ) {
		return new WP_Error( 'xpressui_sync_not_configured', __( 'Console connection not configured.', 'xpressui-bridge' ) );
	}

	$response = wp_remote_get(
		trailingslashit( $conn['apiUrl'] ) . 'api/v1/projects/' . rawurlencode( $project_id ) . '/download',
		[
			'headers' => [
				'X-Api-Token' => $conn['apiToken'],
			],
			'timeout' => 30,
		]
	);

	if ( is_wp_error( $response ) ) {
		return $response;
	}

	$code = wp_remote_retrieve_response_code( $response );
	if ( 200 !== $code ) {
		return new WP_Error(
			'xpressui_sync_download_failed',
			/* translators: %d: HTTP status code returned by the Console API */
			sprintf( __( 'Download failed (status %d).', 'xpressui-bridge' ), $code )
		);
	}

	$zip_content = wp_remote_retrieve_body( $response );
	$tmp_zip     = wp_tempnam( 'xpressui-sync' ) . '.zip';

	require_once ABSPATH . 'wp-admin/includes/file.php';
	global $wp_filesystem;
	if ( ! WP_Filesystem() ) {
		return new WP_Error( 'xpressui_filesystem_init_failed', __( 'Could not initialize WP Filesystem.', 'xpressui-bridge' ) );
	}

	if ( false === $wp_filesystem->put_contents( $tmp_zip, $zip_content, FS_CHMOD_FILE ) ) {
		return new WP_Error( 'xpressui_sync_tmp_write_failed', __( 'Could not write temporary ZIP file.', 'xpressui-bridge' ) );
	}

	$content_disp  = wp_remote_retrieve_header( $response, 'content-disposition' );
	preg_match( '/filename="?([^";\s]+)"?/', (string) $content_disp, $matches );
	$original_name = isset( $matches[1] ) ? sanitize_file_name( $matches[1] ) : 'sync.zip';

	$inspection = xpressui_validate_workflow_zip( $tmp_zip, $original_name );
	if ( is_wp_error( $inspection ) ) {
		$wp_filesystem->delete( $tmp_zip );
		return $inspection;
	}

	$target_dir = xpressui_get_workflows_base_dir();
	if ( '' === $target_dir ) {
		wp_delete_file( $tmp_zip );
		return new WP_Error( 'xpressui_sync_no_uploads_dir', __( 'The uploads directory is not available.', 'xpressui-bridge' ) );
	}

	if ( ! file_exists( $target_dir ) ) {
		wp_mkdir_p( $target_dir );
		global $wp_filesystem;
		$wp_filesystem->put_contents( $target_dir . 'index.php', '<?php' . PHP_EOL . '// Silence is golden.' . PHP_EOL, FS_CHMOD_FILE );
	}

	$slug     = (string) $inspection['slug'];
	$slug_dir = trailingslashit( $target_dir ) . $slug . '/';

	global $wp_filesystem;
	if ( file_exists( $slug_dir ) ) {
		$wp_filesystem->delete( $slug_dir, true );
	}

	$unzip_result = unzip_file( $tmp_zip, $target_dir );
	wp_delete_file( $tmp_zip );

	if ( is_wp_error( $unzip_result ) ) {
		return $unzip_result;
	}

	// Record the workflow in the manifest registry (real Console projectId,
	// runtime tier, capabilities, generatedAt, …). Without this a synced workflow
	// had no registry entry, so its projectId resolved to empty/slug and it was
	// misclassified as "local only" — wrongly offering "Create on Console" and
	// suppressing the "Out of Sync" badge. The on-disk manifest.json carries the
	// real projectId stamped by the Console export.
	$synced_manifest = xpressui_load_workflow_manifest( $slug );
	if ( ! empty( $synced_manifest ) ) {
		xpressui_store_workflow_manifest_meta( $slug, $synced_manifest );
	}

	// Fetch and save hosted link configurations
	$configs_response = wp_remote_get(
		trailingslashit( $conn['apiUrl'] ) . 'api/v1/projects/' . rawurlencode( $slug ) . '/hosted-links/configs',
		[
			'headers' => [
				'X-Api-Token' => $conn['apiToken'],
				'Accept'      => 'application/json',
			],
			'timeout' => 20,
		]
	);

	if ( ! is_wp_error( $configs_response ) && 200 === wp_remote_retrieve_response_code( $configs_response ) ) {
		$configs = json_decode( wp_remote_retrieve_body( $configs_response ), true );
		if ( is_array( $configs ) ) {
			$hosted_links_dir = $slug_dir . 'hosted-links/';
			if ( ! file_exists( $hosted_links_dir ) ) {
				wp_mkdir_p( $hosted_links_dir );
			}
			$active_link_ids = [];
			foreach ( $configs as $config ) {
				// Only retrieve and synchronize hosted links of type default (pas les relances)
				$payload_data = is_array( $config['payload'] ?? null ) ? $config['payload'] : [];
				$follow_up    = $payload_data['followUp'] ?? $payload_data['follow_up'] ?? [];
				if ( is_array( $follow_up ) && ( ! empty( $follow_up['parentSubmissionId'] ) || ! empty( $follow_up['parent_submission_id'] ) ) ) {
					continue;
				}

				$link_id = sanitize_file_name( (string) ( $config['id'] ?? '' ) );
				if ( '' !== $link_id ) {
					$active_link_ids[] = $link_id;

					// Clean up any old, orphaned hosted link configurations for this link_id in other projects.
					$base_dir = xpressui_get_workflows_base_dir();
					if ( $base_dir !== '' && is_dir( $base_dir ) ) {
						$subdirs = glob( trailingslashit( $base_dir ) . '*', GLOB_ONLYDIR );
						if ( is_array( $subdirs ) ) {
							foreach ( $subdirs as $subdir ) {
								$potential_slug = basename( $subdir );
								if ( $potential_slug !== $slug ) {
									$old_link_dir = trailingslashit( $subdir ) . 'hosted-links/' . $link_id . '/';
									if ( file_exists( $old_link_dir ) ) {
										$wp_filesystem->delete( $old_link_dir, true );
									}
								}
							}
						}
					}

					$link_dir = $hosted_links_dir . $link_id . '/';
					if ( ! file_exists( $link_dir ) ) {
						wp_mkdir_p( $link_dir );
					}
					$wp_filesystem->put_contents( $link_dir . 'link.config.json', wp_json_encode( $config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), FS_CHMOD_FILE );

					// Headless catalog (Model B): when the link carries a product front
					// catalog, pull its JSON snapshot so the plugin can render the product
					// grid server-side. The catalog folder is dropped when no longer present.
					$presentation  = is_array( $config['presentation'] ?? null ) ? $config['presentation'] : [];
					$front_catalog = is_array( $presentation['frontCatalog'] ?? null ) ? $presentation['frontCatalog'] : [];
					$catalogs_dir  = $link_dir . 'catalogs/';
					if ( ! empty( $front_catalog['catalogId'] ) ) {
						$catalog_response = wp_remote_get(
							trailingslashit( $conn['apiUrl'] ) . 'api/v1/projects/' . rawurlencode( $slug ) . '/hosted-links/' . rawurlencode( (string) ( $config['id'] ?? '' ) ) . '/catalog.json',
							[
								'headers' => [
									'X-Api-Token' => $conn['apiToken'],
									'Accept'      => 'application/json',
								],
								'timeout' => 20,
							]
						);
						if ( ! is_wp_error( $catalog_response ) && 200 === wp_remote_retrieve_response_code( $catalog_response ) ) {
							$catalog_json = json_decode( wp_remote_retrieve_body( $catalog_response ), true );
							if ( is_array( $catalog_json ) ) {
								if ( ! file_exists( $catalogs_dir ) ) {
									wp_mkdir_p( $catalogs_dir );
								}
								$wp_filesystem->put_contents( $catalogs_dir . 'catalog.json', wp_json_encode( $catalog_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), FS_CHMOD_FILE );
							}
						}
					} elseif ( is_dir( $catalogs_dir ) ) {
						$wp_filesystem->delete( $catalogs_dir, true );
					}
				}
			}

			// Clean up any local hosted links that are no longer active/present on the Console
			if ( is_dir( $hosted_links_dir ) ) {
				$local_dirs = glob( trailingslashit( $hosted_links_dir ) . '*', GLOB_ONLYDIR );
				if ( is_array( $local_dirs ) ) {
					foreach ( $local_dirs as $local_dir_path ) {
						$local_link_id = basename( $local_dir_path );
						if ( ! in_array( $local_link_id, $active_link_ids, true ) ) {
							$wp_filesystem->delete( $local_dir_path, true );
						}
					}
				}
			}
		}
	}

	return [
		'success' => true,
		'slug'    => $slug,
	];
}

function xpressui_ajax_console_sync_project(): void {
	check_ajax_referer( 'xpressui_console_sync_nonce', 'nonce' );

	if ( ! current_user_can( 'manage_options' ) ) {
		wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'xpressui-bridge' ) ], 403 );
	}

	$project_id = sanitize_text_field( wp_unslash( (string) ( $_POST['project_id'] ?? '' ) ) );
	if ( '' === $project_id ) {
		wp_send_json_error( [ 'message' => __( 'Missing project_id.', 'xpressui-bridge' ) ] );
	}

	$res = xpressui_sync_project( $project_id );
	if ( is_wp_error( $res ) ) {
		wp_send_json_error( [ 'message' => $res->get_error_message() ] );
	}

	wp_send_json_success( [
		'slug'    => $res['slug'],
		/* translators: %s: installed workflow slug */
		'message' => sprintf( __( 'Synced! Embed with: [xpressui id="%s"]', 'xpressui-bridge' ), $res['slug'] ),
	] );
}

/**
 * AJAX: (re)create a LOCAL / out-of-sync workflow on the SaaS Console.
 *
 * Loads the workflow's local package (title + fields + steps from manifest.json),
 * creates the project on the Console via the importer's create helper, then pulls
 * the newly created project back to WordPress so its stored projectId / sync state
 * flips from local-only to synced. Failures never mutate local state.
 */
function xpressui_ajax_console_create_local_workflow(): void {
	check_ajax_referer( 'xpressui_console_sync_nonce', 'nonce' );

	if ( ! current_user_can( 'manage_options' ) ) {
		wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'xpressui-bridge' ) ], 403 );
	}

	if ( ! xpressui_is_saas_connected() ) {
		wp_send_json_error( [ 'message' => __( 'Connect your IntakeFlow Console (configure an API Token) first.', 'xpressui-bridge' ) ] );
	}

	$slug = sanitize_title( wp_unslash( (string) ( $_POST['slug'] ?? '' ) ) );
	if ( '' === $slug ) {
		wp_send_json_error( [ 'message' => __( 'Missing workflow slug.', 'xpressui-bridge' ) ] );
	}

	if ( ! xpressui_workflow_is_local_only( $slug ) ) {
		wp_send_json_error( [ 'message' => __( 'This workflow is already on your Console.', 'xpressui-bridge' ) ] );
	}

	$manifest = xpressui_load_workflow_manifest( $slug );
	if ( empty( $manifest ) ) {
		$manifest = xpressui_load_bundled_workflow_manifest( $slug );
	}

	$fields = is_array( $manifest['fields'] ?? null ) ? $manifest['fields'] : [];
	$steps  = is_array( $manifest['steps'] ?? null ) ? $manifest['steps'] : [];
	if ( empty( $fields ) ) {
		wp_send_json_error( [ 'message' => __( 'Could not read this workflow’s fields from its local package.', 'xpressui-bridge' ) ] );
	}

	$title = sanitize_text_field( (string) ( $manifest['projectName'] ?? '' ) );
	if ( '' === $title ) {
		$title = $slug;
	}

	if ( empty( $steps ) ) {
		$steps = [
			[
				'id'     => 'step_1',
				'title'  => __( 'General Details', 'xpressui-bridge' ),
				'fields' => array_column( $fields, 'id' ),
			],
		];
	}

	$create_result = xpressui_console_create_project( $slug, $title, $fields, $steps );
	if ( is_wp_error( $create_result ) ) {
		$reason = xpressui_console_failure_reason( $create_result );
		wp_send_json_error( [
			/* translators: %s: short human-readable reason the Console could not be reached. */
			'message' => sprintf( __( 'Couldn’t create the workflow on your Console (%s). Your local workflow was left unchanged.', 'xpressui-bridge' ), $reason ),
		] );
	}

	// Console create succeeded — pull the new project back so the stored projectId /
	// sync state flips from local-only to synced. A sync failure here is NOT a
	// "Console unreachable" condition (the SaaS project exists), so we report it as
	// a warning and leave the local package as-is.
	$api_project_id = $create_result['projectId'];
	$final_slug     = $create_result['slug'];

	$sync_res = xpressui_sync_project( $api_project_id );
	if ( is_wp_error( $sync_res ) ) {
		wp_send_json_error( [
			/* translators: %s: error message from the sync attempt. */
			'message' => sprintf( __( 'Workflow created on your Console, but the local sync failed: %s', 'xpressui-bridge' ), $sync_res->get_error_message() ),
		] );
	}

	wp_send_json_success( [
		'slug'    => $sync_res['slug'] ?? $final_slug,
		'message' => __( 'Workflow created on your IntakeFlow Console.', 'xpressui-bridge' ),
	] );
}
add_action( 'wp_ajax_xpressui_console_create_local_workflow', 'xpressui_ajax_console_create_local_workflow' );

// ---------------------------------------------------------------------------
// UI section — injected into the free plugin's Workflows page
// ---------------------------------------------------------------------------

// add_action( 'xpressui_workflows_page_sections', 'xpressui_pro_render_console_sync_section' );

function xpressui_pro_render_console_sync_section(): void {
	$nonce = wp_create_nonce( 'xpressui_console_sync_nonce' );
	$is_license_active = xpressui_is_saas_connected();
	?>
	<div class="card xpressui-admin-card">
		<h2><?php esc_html_e( 'Console Sync', 'xpressui-bridge' ); ?></h2>
		<p class="description">
			<?php esc_html_e( 'Pull workflow packs directly from your IntakeFlow Console — no ZIP download required.', 'xpressui-bridge' ); ?>
		</p>

		<h3 style="margin-top:1rem"><?php esc_html_e( 'Connection', 'xpressui-bridge' ); ?></h3>
		<?php xpressui_render_console_connection_form(); ?>

		<?php if ( $is_license_active ) : ?>
			<h3 style="margin-top:1.5rem"><?php esc_html_e( 'Your Workflows', 'xpressui-bridge' ); ?></h3>
			<p>
				<button type="button" id="xpressui-load-projects" class="button button-secondary">
					<?php esc_html_e( 'Load from Console', 'xpressui-bridge' ); ?>
				</button>
				<button type="button" id="xpressui-sync-all-projects" class="button button-primary" style="display:none; margin-left: 10px;">
					<?php esc_html_e( 'Sync All Workflows', 'xpressui-bridge' ); ?>
				</button>
			</p>
			<div id="xpressui-projects-list"></div>

			<?php
			$console_sync_script = sprintf(
				<<<'JS'
(function () {
	var nonce = %1$s;
	var ajaxUrl = %2$s;
	var labels = %3$s;

	function escapeHtml(value) {
		return String(value || '').replace(/[&<>"']/g, function (char) {
			return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' })[char];
		});
	}

	function setStatus(el, msg, isError) {
		el.innerHTML = '<p style="color:' + (isError ? '#c00' : '#3a3') + '">' + escapeHtml(msg) + '</p>';
	}

	var loadedProjects = [];

	document.getElementById('xpressui-load-projects').addEventListener('click', function () {
		var list = document.getElementById('xpressui-projects-list');
		list.innerHTML = '<p>' + escapeHtml(labels.loading) + '</p>';
		var syncAllBtn = document.getElementById('xpressui-sync-all-projects');
		if (syncAllBtn) {
			syncAllBtn.style.display = 'none';
		}

		var data = new URLSearchParams();
		data.set('action', 'xpressui_console_list_projects');
		data.set('nonce', nonce);

		fetch(ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' })
			.then(function (r) { return r.json(); })
			.then(function (res) {
				if (!res.success) {
					setStatus(list, res.data.message, true);
					return;
				}
				var projects = res.data.projects;
				loadedProjects = projects;
				if (!projects.length) {
					list.innerHTML = '<p>' + escapeHtml(labels.noProjects) + '</p>';
					return;
				}
				if (syncAllBtn) {
					syncAllBtn.style.display = 'inline-block';
					syncAllBtn.disabled = false;
				}
				var html = '<table class="widefat striped"><thead><tr>'
					+ '<th>' + escapeHtml(labels.name) + '</th>'
					+ '<th>' + escapeHtml(labels.slug) + '</th>'
					+ '<th>' + escapeHtml(labels.updated) + '</th>'
					+ '<th></th></tr></thead><tbody>';
				projects.forEach(function (p) {
					var id = escapeHtml(p.id);
					var date = p.updatedAt ? new Date(p.updatedAt).toLocaleDateString() : '-';
					html += '<tr id="xpressui-row-' + id + '">'
						+ '<td>' + escapeHtml(p.name) + '</td>'
						+ '<td><code>' + escapeHtml(p.slug) + '</code></td>'
						+ '<td>' + escapeHtml(date) + '</td>'
						+ '<td><button type="button" class="button button-primary xpressui-sync-btn" data-id="' + id + '">'
						+ escapeHtml(labels.sync) + '</button></td>'
						+ '</tr>';
				});
				html += '</tbody></table>';
				list.innerHTML = html;

				list.querySelectorAll('.xpressui-sync-btn').forEach(function (btn) {
					btn.addEventListener('click', function () {
						var projectId = this.dataset.id;
						var row = document.getElementById('xpressui-row-' + projectId);
						this.disabled = true;
						this.textContent = labels.syncing;
						if (syncAllBtn) {
							syncAllBtn.disabled = true;
						}

						var syncData = new URLSearchParams();
						syncData.set('action', 'xpressui_console_sync_project');
						syncData.set('nonce', nonce);
						syncData.set('project_id', projectId);

						fetch(ajaxUrl, { method: 'POST', body: syncData, credentials: 'same-origin' })
							.then(function (r) { return r.json(); })
							.then(function (res) {
								if (syncAllBtn) {
									syncAllBtn.disabled = false;
								}
								if (!res.success) {
									row.querySelector('td:last-child').innerHTML =
										'<span style="color:#c00">' + escapeHtml(res.data.message) + '</span>';
								} else {
									row.querySelector('td:last-child').innerHTML =
										'<span style="color:#3a3">' + escapeHtml(labels.syncedPrefix + ' ' + res.data.message + ' - ' + labels.reloading) + '</span>';
									sessionStorage.setItem('xpressui_synced_slug', res.data.slug || '');
									setTimeout(function () { window.location.reload(); }, 1200);
								}
							})
							.catch(function () {
								if (syncAllBtn) {
									syncAllBtn.disabled = false;
								}
								row.querySelector('td:last-child').innerHTML =
									'<span style="color:#c00">' + escapeHtml(labels.networkError) + '</span>';
							});
					});
				});
			})
			.catch(function () {
				setStatus(list, labels.networkConnectionError, true);
			});
	});

	var syncAllBtn = document.getElementById('xpressui-sync-all-projects');
	if (syncAllBtn) {
		syncAllBtn.addEventListener('click', function () {
			if (!loadedProjects || !loadedProjects.length) return;
			var list = document.getElementById('xpressui-projects-list');

			document.getElementById('xpressui-load-projects').disabled = true;
			syncAllBtn.disabled = true;

			var progressDiv = document.getElementById('xpressui-global-sync-progress');
			if (!progressDiv) {
				progressDiv = document.createElement('div');
				progressDiv.id = 'xpressui-global-sync-progress';
				progressDiv.style.margin = '15px 0';
				progressDiv.style.padding = '12px 16px';
				progressDiv.style.background = '#f0f9ff';
				progressDiv.style.border = '1px solid #bae6fd';
				progressDiv.style.color = '#0369a1';
				progressDiv.style.borderRadius = '6px';
				progressDiv.style.fontWeight = '500';
				list.insertBefore(progressDiv, list.firstChild);
			}

			var index = 0;
			var total = loadedProjects.length;

			function syncNext() {
				var percent = Math.round((index / total) * 100);
				if (index >= total) {
					progressDiv.style.background = '#f0fdf4';
					progressDiv.style.border = '1px solid #bbf7d0';
					progressDiv.style.color = '#15803d';
					progressDiv.innerHTML = '<strong>' + escapeHtml(labels.allSynced) + '</strong> ' + escapeHtml(labels.reloading)
						+ '<div style="width: 100%; background: #dcfce7; border-radius: 9999px; height: 8px; margin-top: 10px; overflow: hidden;">'
						+ '  <div style="width: 100%; background: #16a34a; height: 100%; transition: width 0.3s ease-in-out;"></div>'
						+ '</div>';
					setTimeout(function () {
						window.location.reload();
					}, 1500);
					return;
				}

				var p = loadedProjects[index];
				progressDiv.innerHTML = '<div>' + escapeHtml(labels.syncingAll) + ' <strong>' + (index + 1) + '/' + total + '</strong> (' + escapeHtml(p.name) + ')...</div>'
					+ '<div style="width: 100%; background: #e0f2fe; border-radius: 9999px; height: 8px; margin-top: 10px; overflow: hidden;">'
					+ '  <div style="width: ' + percent + '%; background: #0284c7; height: 100%; transition: width 0.3s ease-in-out;"></div>'
					+ '</div>';

				var row = document.getElementById('xpressui-row-' + p.id);
				var rowBtn = row ? row.querySelector('.xpressui-sync-btn') : null;
				if (rowBtn) {
					rowBtn.disabled = true;
					rowBtn.textContent = labels.syncing;
				}

				var syncData = new URLSearchParams();
				syncData.set('action', 'xpressui_console_sync_project');
				syncData.set('nonce', nonce);
				syncData.set('project_id', p.id);

				fetch(ajaxUrl, { method: 'POST', body: syncData, credentials: 'same-origin' })
					.then(function (r) { return r.json(); })
					.then(function (res) {
						if (row) {
							if (!res.success) {
								row.querySelector('td:last-child').innerHTML =
									'<span style="color:#c00">' + escapeHtml(res.data.message) + '</span>';
							} else {
								row.querySelector('td:last-child').innerHTML =
									'<span style="color:#3a3">' + escapeHtml(labels.syncedPrefix + ' ' + res.data.message) + '</span>';
							}
						}
						index++;
						syncNext();
					})
					.catch(function () {
						if (row) {
							row.querySelector('td:last-child').innerHTML =
								'<span style="color:#c00">' + escapeHtml(labels.networkError) + '</span>';
						}
						index++;
						syncNext();
					});
			}

			syncNext();
		});
	}
}());
JS,
				wp_json_encode( $nonce ),
				wp_json_encode( admin_url( 'admin-ajax.php' ) ),
				wp_json_encode(
					[
						'loading'                => __( 'Loading...', 'xpressui-bridge' ),
						'noProjects'             => __( 'No projects found in your Console.', 'xpressui-bridge' ),
						'name'                   => __( 'Name', 'xpressui-bridge' ),
						'slug'                   => __( 'Slug', 'xpressui-bridge' ),
						'updated'                => __( 'Updated', 'xpressui-bridge' ),
						'sync'                   => __( 'Sync', 'xpressui-bridge' ),
						'syncing'                => __( 'Syncing...', 'xpressui-bridge' ),
						'syncedPrefix'           => __( 'Synced:', 'xpressui-bridge' ),
						'reloading'              => __( 'Reloading...', 'xpressui-bridge' ),
						'networkError'           => __( 'Network error.', 'xpressui-bridge' ),
						'networkConnectionError' => __( 'Network error. Check your connection.', 'xpressui-bridge' ),
						'syncingAll'             => __( 'Syncing workflows:', 'xpressui-bridge' ),
						'allSynced'              => __( 'All workflows synchronized!', 'xpressui-bridge' ),
					]
				)
			);
			wp_print_inline_script_tag( $console_sync_script );
			?>
		<?php else : ?>
			<p style="margin-top:1.5rem;color:#c00">
				<strong><?php esc_html_e( 'Console connection required.', 'xpressui-bridge' ); ?></strong><br>
				<?php esc_html_e( 'Please enter your API token above and save the connection to start syncing workflows.', 'xpressui-bridge' ); ?>
			</p>
		<?php endif; ?>

		<?php
		$connection_form_script = sprintf(
			<<<'JS'
(function () {
	var form = document.getElementById('xpressui-console-connection-form');
	if (!form) { return; }
	form.addEventListener('submit', function (e) {
		e.preventDefault();
		var statusEl  = form.querySelector('.xpressui-ajax-status');
		var submitBtn = form.querySelector('[type="submit"]');
		if (submitBtn) { submitBtn.disabled = true; }
		if (statusEl) { statusEl.textContent = %1$s; statusEl.style.color = ''; }

		var data = new FormData(form);
		data.set('action', 'xpressui_save_console_connection');

		fetch(%2$s, { method: 'POST', body: data, credentials: 'same-origin' })
			.then(function (r) { return r.json(); })
			.then(function (res) {
				if (submitBtn) { submitBtn.disabled = false; }
				if (statusEl) {
					statusEl.textContent = res.success ? %3$s : (res.data.message || 'Error.');
					statusEl.style.color = res.success ? '#3a3' : '#c00';
				}
				if (res.success) {
					setTimeout(function () { window.location.reload(); }, 1000);
				}
			})
			.catch(function () {
				if (submitBtn) { submitBtn.disabled = false; }
				if (statusEl) { statusEl.textContent = %4$s; statusEl.style.color = '#c00'; }
			});
	});
}());
JS,
			wp_json_encode( __( 'Saving…', 'xpressui-bridge' ) ),
			wp_json_encode( admin_url( 'admin-ajax.php' ) ),
			wp_json_encode( __( 'Saved. Reloading…', 'xpressui-bridge' ) ),
			wp_json_encode( __( 'Network error.', 'xpressui-bridge' ) )
		);
		wp_print_inline_script_tag( $connection_form_script );
		?>
	</div>
	<?php
}

function xpressui_check_console_api_health(): array {
	$conn = xpressui_get_console_connection();

	// Try to get cached status to avoid blocking page loads with synchronous HTTP requests.
	$cached = get_transient( 'xpressui_console_api_health' );
	if ( is_array( $cached ) && isset( $cached['apiUrl'], $cached['apiToken'], $cached['result'] ) ) {
		if ( $cached['apiUrl'] === $conn['apiUrl'] && $cached['apiToken'] === $conn['apiToken'] ) {
			return $cached['result'];
		}
	}

	if ( empty( $conn['apiUrl'] ) ) {
		$result = [
			'status'  => 'not_configured',
			'message' => __( 'Console API URL is not configured.', 'xpressui-bridge' ),
		];
		set_transient( 'xpressui_console_api_health', [
			'apiUrl'   => $conn['apiUrl'],
			'apiToken' => $conn['apiToken'],
			'result'   => $result,
		], 300 );
		return $result;
	}

	// 1. Check general API reachability
	$health_url = trailingslashit( $conn['apiUrl'] ) . 'api/health';
	$response   = wp_remote_get( $health_url, [ 'timeout' => 5 ] );

	if ( is_wp_error( $response ) ) {
		$result = [
			'status'  => 'unreachable',
			/* translators: %s: error message returned by the HTTP request */
			'message' => sprintf( __( 'API unreachable: %s', 'xpressui-bridge' ), $response->get_error_message() ),
		];
		set_transient( 'xpressui_console_api_health', [
			'apiUrl'   => $conn['apiUrl'],
			'apiToken' => $conn['apiToken'],
			'result'   => $result,
		], 300 );
		return $result;
	}

	$code = wp_remote_retrieve_response_code( $response );
	if ( 200 !== $code ) {
		$result = [
			'status'  => 'degraded',
			/* translators: %d: HTTP status code returned by the API */
			'message' => sprintf( __( 'API returned HTTP status %d.', 'xpressui-bridge' ), $code ),
		];
		set_transient( 'xpressui_console_api_health', [
			'apiUrl'   => $conn['apiUrl'],
			'apiToken' => $conn['apiToken'],
			'result'   => $result,
		], 300 );
		return $result;
	}

	$body = json_decode( wp_remote_retrieve_body( $response ), true );
	if ( ! is_array( $body ) || ( $body['status'] ?? '' ) !== 'ok' ) {
		$result = [
			'status'  => 'degraded',
			'message' => __( 'API health check returned unexpected response.', 'xpressui-bridge' ),
		];
		set_transient( 'xpressui_console_api_health', [
			'apiUrl'   => $conn['apiUrl'],
			'apiToken' => $conn['apiToken'],
			'result'   => $result,
		], 300 );
		return $result;
	}

	// 2. If token is configured, check authentication
	if ( ! empty( $conn['apiToken'] ) ) {
		$auth_url = trailingslashit( $conn['apiUrl'] ) . 'api/v1/projects';
		$auth_response = wp_remote_get( $auth_url, [
			'headers' => [
				'X-Api-Token' => $conn['apiToken'],
				'Accept'      => 'application/json',
			],
			'timeout' => 5,
		] );

		if ( is_wp_error( $auth_response ) ) {
			$result = [
				'status'  => 'auth_failed',
				/* translators: %s: error message returned by the authentication request */
				'message' => sprintf( __( 'Authentication check failed: %s', 'xpressui-bridge' ), $auth_response->get_error_message() ),
			];
			set_transient( 'xpressui_console_api_health', [
				'apiUrl'   => $conn['apiUrl'],
				'apiToken' => $conn['apiToken'],
				'result'   => $result,
			], 300 );
			return $result;
		}

		$auth_code = wp_remote_retrieve_response_code( $auth_response );
		if ( 401 === $auth_code || 403 === $auth_code ) {
			$result = [
				'status'  => 'invalid_token',
				'message' => __( 'Invalid or expired API Token.', 'xpressui-bridge' ),
			];
			set_transient( 'xpressui_console_api_health', [
				'apiUrl'   => $conn['apiUrl'],
				'apiToken' => $conn['apiToken'],
				'result'   => $result,
			], 300 );
			return $result;
		} elseif ( 200 !== $auth_code ) {
			$result = [
				'status'  => 'degraded',
				/* translators: %d: HTTP status code returned by the authenticated request */
				'message' => sprintf( __( 'Authenticated check returned HTTP status %d.', 'xpressui-bridge' ), $auth_code ),
			];
			set_transient( 'xpressui_console_api_health', [
				'apiUrl'   => $conn['apiUrl'],
				'apiToken' => $conn['apiToken'],
				'result'   => $result,
			], 300 );
			return $result;
		}

		$result = [
			'status'  => 'connected',
			'message' => __( 'Connected & authenticated.', 'xpressui-bridge' ),
		];
		set_transient( 'xpressui_console_api_health', [
			'apiUrl'   => $conn['apiUrl'],
			'apiToken' => $conn['apiToken'],
			'result'   => $result,
		], 300 );
		return $result;
	}

	$result = [
		'status'  => 'reachable',
		'message' => __( 'API reachable (Token missing).', 'xpressui-bridge' ),
	];
	set_transient( 'xpressui_console_api_health', [
		'apiUrl'   => $conn['apiUrl'],
		'apiToken' => $conn['apiToken'],
		'result'   => $result,
	], 300 );
	return $result;
}

/**
 * Handles the redirect callback from the Console during 1-click connection.
 * Sanitizes and saves the api_token, owner_uid, and api_url.
 */
function xpressui_maybe_handle_console_connect_callback() {
	if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) {
		return;
	}

	if ( empty( $_SERVER['REQUEST_URI'] ) ) {
		return;
	}

	$parsed_url = wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
	if ( empty( $parsed_url['query'] ) ) {
		return;
	}

	parse_str( $parsed_url['query'], $query_args );

	if ( ! isset( $query_args['api_token'] ) || ! isset( $query_args['owner_uid'] ) ) {
		return;
	}

	$state       = isset( $query_args['state'] ) ? sanitize_text_field( $query_args['state'] ) : '';
	$saved_state = get_transient( 'xpressui_connect_state' );

	if ( empty( $saved_state ) || $state !== $saved_state ) {
		return;
	}

	delete_transient( 'xpressui_connect_state' );

	$api_token = sanitize_text_field( $query_args['api_token'] );
	$owner_uid = sanitize_text_field( $query_args['owner_uid'] );
	$api_url   = isset( $query_args['api_url'] ) ? esc_url_raw( $query_args['api_url'] ) : 'https://app.intakeflow.dev';

	if ( '' === $api_token || '' === $owner_uid ) {
		return;
	}

	update_option( 'xpressui_console_connection', [
		'apiUrl'   => $api_url,
		'apiToken' => $api_token,
		'ownerUid' => $owner_uid,
	] );
	delete_transient( 'xpressui_console_api_health' );

	if ( function_exists( 'xpressui_set_admin_notice' ) ) {
		xpressui_set_admin_notice( __( 'Successfully connected to IntakeFlow Console!', 'xpressui-bridge' ), 'success' );
	}

	// Redirect to the same page without query args to keep URL clean
	$clean_url = remove_query_arg( [ 'api_token', 'owner_uid', 'api_url', 'state' ] );
	wp_safe_redirect( $clean_url );
	exit;
}
add_action( 'admin_init', 'xpressui_maybe_handle_console_connect_callback' );

/**
 * Check if the site is actively connected to the IntakeFlow Console (SaaS).
 * Validates that a token is configured and the API health check is connected.
 *
 * @return bool
 */
function xpressui_is_saas_connected(): bool {
	$conn = xpressui_get_console_connection();
	if ( empty( $conn['apiToken'] ) ) {
		return false;
	}
	$api_health = xpressui_check_console_api_health();
	return $api_health['status'] === 'connected';
}

