<?php
/**
 * Builder-specific tools.
 *
 * Builder detection, curated documentation, and Builder-native helpers live
 * here. Shared WordPress/media/files/admin/Woo tools stay in their own files.
 */

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

class PCD_PC_Tools_Bricks {

	public static function register(): void {
		PCD_PC_Tools::register(
			'bricks_status',
			'Detect whether Bricks Builder is active and return the available Bricks capabilities this site exposes.',
			[ 'type' => 'object', 'properties' => new stdClass() ],
			[ __CLASS__, 'status' ],
			[ 'cap' => 'edit_theme_options', 'category' => 'bricks' ]
		);

		PCD_PC_Tools::register(
			'bricks_docs',
			'Return a curated catalog of Bricks Builder documentation, OR fetch the actual content of a specific docs page through the Conduit relay. Three ways to call: (1) no args returns the full catalog; (2) topic filters to one category; (3) read:"<url>" fetches an allowlisted Bricks docs URL as markdown. Call this before building, editing, or troubleshooting Bricks content so generated builder data is grounded in the official schema and template docs.',
			[
				'type' => 'object',
				'properties' => [
					'topic' => [
						'type' => 'string',
						'description' => 'Optional category. Available categories are returned by calling without args.',
					],
					'read' => [
						'type' => 'string',
						'description' => 'Optional URL from the catalog, or another URL on an allowlisted Bricks docs host. The relay fetches and converts it to markdown.',
					],
				],
			],
			[ __CLASS__, 'docs' ],
			[ 'cap' => 'edit_posts', 'category' => 'bricks' ]
		);

		PCD_PC_Tools::register(
			'bricks_element_catalog',
			'List every Bricks element registered on this site (name, label, category, and whether it is nestable = can contain child elements). Read from the live Bricks element registry, so it reflects this site\'s actual elements including any third-party/custom ones. Use this to discover what elements exist before calling bricks_schema. The `name` value (e.g. "heading", "container", "section", "button", "image") is exactly what goes in an element object\'s `name` field when you write Bricks content.',
			[
				'type' => 'object',
				'properties' => [
					'category' => [ 'type' => 'string', 'description' => 'Optional category filter, e.g. "basic", "layout", "media", "general", "wordpress", "woocommerce".' ],
				],
			],
			[ __CLASS__, 'element_catalog' ],
			[ 'cap' => 'edit_posts', 'category' => 'bricks' ]
		);

		PCD_PC_Tools::register(
			'bricks_schema',
			'Return the full control/settings schema for ONE Bricks element — every setting key with its type, default, options, tab, group, and conditional (required) rules — read live from this site\'s Bricks installation. **Call this before writing an element\'s `settings` object** so you use real setting keys instead of guessing. Names match bricks_element_catalog (e.g. "heading", "button", "container"). Note: in stored content, responsive/state variants append a colon suffix to the key (e.g. "_width:tablet_portrait") and many CSS settings use underscore-prefixed keys — see the data-model doc via bricks_docs.',
			[
				'type' => 'object',
				'required' => [ 'name' ],
				'properties' => [
					'name'    => [ 'type' => 'string', 'description' => 'Element name, e.g. "heading", "container", "button".' ],
					'compact' => [ 'type' => 'boolean', 'default' => true, 'description' => 'Trim each control to {type,label,default,options,tab,group,required,placeholder}. Pass false for the raw control definitions (verbose: includes CSS selectors, etc.).' ],
				],
			],
			[ __CLASS__, 'schema' ],
			[ 'cap' => 'edit_posts', 'category' => 'bricks' ]
		);

		PCD_PC_Tools::register(
			'bricks_page_read',
			'Read the Bricks element array for a post/page/template. Returns the flat list of element objects ({id, name, parent, children, settings}) exactly as stored in postmeta, plus the editor mode and a compact nesting tree so you can see the structure at a glance. Safe / non-destructive. Call this before bricks_page_write to see what is already there. `area` picks which region: "content" (default, the page body = _bricks_page_content_2), "header" (_bricks_page_header_2), or "footer" (_bricks_page_footer_2).',
			[
				'type' => 'object',
				'required' => [ 'id' ],
				'properties' => [
					'id'   => [ 'type' => 'integer', 'description' => 'Post/page/template ID to read.' ],
					'area' => [ 'type' => 'string', 'enum' => [ 'content', 'header', 'footer' ], 'default' => 'content', 'description' => 'Which Bricks region to read. Default "content".' ],
					'tree' => [ 'type' => 'boolean', 'default' => true, 'description' => 'Include a compact id/name nesting tree alongside the flat element array. Default true.' ],
				],
			],
			[ __CLASS__, 'page_read' ],
			[ 'cap' => 'edit_posts', 'category' => 'bricks' ]
		);

		PCD_PC_Tools::register(
			'bricks_page_write',
			'Write the Bricks element array for a post/page/template — THE Bricks build capability. Pass `elements`: a flat array of element objects, each {id (6-char string, auto-generated if omitted), name (e.g. "section","container","heading"), parent (0 for a root element, else the parent element\'s id), children (array of child ids), settings (object of setting keys from bricks_schema)}. The tool validates/normalizes ids + parent/children links, snapshots the prior content for undo, writes to the correct postmeta key, and flips the post\'s editor mode to "bricks" so Bricks renders it. ALWAYS read the data-model + element schemas first (bricks_docs, bricks_element_catalog, bricks_schema). Pass `elements: []` to clear a region. Destructive — overwrites the whole region.',
			[
				'type' => 'object',
				'required' => [ 'id', 'elements' ],
				'properties' => [
					'id'             => [ 'type' => 'integer', 'description' => 'Post/page/template ID to write.' ],
					'elements'       => [ 'type' => 'array', 'description' => 'Flat array of Bricks element objects. Empty array clears the region.', 'items' => [ 'type' => 'object' ] ],
					'area'           => [ 'type' => 'string', 'enum' => [ 'content', 'header', 'footer' ], 'default' => 'content', 'description' => 'Which Bricks region to write. Default "content".' ],
					'regenerate_css' => [ 'type' => 'boolean', 'default' => false, 'description' => 'Only relevant when the site uses external CSS files (cssLoading=file). Regenerates Bricks CSS files after the write. Inline-CSS sites (the default) generate CSS at render and need no regen.' ],
				],
			],
			[ __CLASS__, 'page_write' ],
			[ 'cap' => 'edit_posts', 'category' => 'bricks' ]
		);

		PCD_PC_Tools::register(
			'bricks_template_list',
			'List Bricks templates (the bricks_template post type: headers, footers, single/content, sections, popups, archives, search, error pages). Returns each template\'s id, title, status, template type, and which regions hold content — so you can find a template to read/edit. Bricks templates store their elements in the same postmeta as pages, so use bricks_page_read / bricks_page_write (with the template\'s id) to read or build their content.',
			[
				'type' => 'object',
				'properties' => [
					'type'   => [ 'type' => 'string', 'description' => 'Optional template-type filter: header, footer, content, section, popup, archive, search, error.' ],
					'status' => [ 'type' => 'string', 'default' => 'any', 'description' => 'Post status filter (publish, draft, any). Default "any".' ],
					'limit'  => [ 'type' => 'integer', 'default' => 100, 'description' => 'Max templates to return (1-500). Default 100.' ],
				],
			],
			[ __CLASS__, 'template_list' ],
			[ 'cap' => 'edit_posts', 'category' => 'bricks' ]
		);

		PCD_PC_Tools::register(
			'bricks_template_create',
			'Create an empty Bricks template (a bricks_template post) of a given type, then populate it with bricks_page_write using the returned id. `type` is one of: header, footer, content (single), section, popup, archive, search, error. Sets the _bricks_template_type meta and the editor mode so Bricks recognizes it. To control WHERE a template applies (its conditions), set template settings in the Bricks UI or pass `settings`.',
			[
				'type' => 'object',
				'required' => [ 'title', 'type' ],
				'properties' => [
					'title'    => [ 'type' => 'string', 'description' => 'Template title.' ],
					'type'     => [ 'type' => 'string', 'enum' => [ 'header', 'footer', 'content', 'section', 'popup', 'archive', 'search', 'error' ], 'description' => 'Template type.' ],
					'status'   => [ 'type' => 'string', 'default' => 'publish', 'description' => 'Post status. Default "publish".' ],
					'settings' => [ 'type' => 'object', 'description' => 'Optional template settings (conditions, etc.) stored in _bricks_template_settings.' ],
				],
			],
			[ __CLASS__, 'template_create' ],
			[ 'cap' => 'edit_theme_options', 'category' => 'bricks' ]
		);

		PCD_PC_Tools::register(
			'bricks_globals_read',
			'Read Bricks global/site-wide stores from wp_options — theme styles, global classes, components, global variables, color palettes, and breakpoints. These live in options (not postmeta) and are referenced by element settings (e.g. _cssGlobalClasses points at global-class ids; components are reused element trees). Call with no args for a summary/index of every store + counts; pass `which` to get the full data for one store. Read these before writing content that references global classes/components/variables so you use real ids.',
			[
				'type' => 'object',
				'properties' => [
					'which' => [ 'type' => 'string', 'enum' => [ 'theme_styles', 'global_classes', 'components', 'variables', 'color_palette', 'breakpoints' ], 'description' => 'Optional: return the full data for one store. Omit for an index of all stores with counts.' ],
				],
			],
			[ __CLASS__, 'globals_read' ],
			[ 'cap' => 'edit_theme_options', 'category' => 'bricks' ]
		);
	}

	public static function status( array $args ) {
		$theme = wp_get_theme();
		$template = wp_get_theme( get_template() );
		$stylesheet = wp_get_theme( get_stylesheet() );

		$signals = [
			'BRICKS_VERSION' => defined( 'BRICKS_VERSION' ) ? BRICKS_VERSION : null,
			'BRICKS_DB_PAGE_CONTENT' => defined( 'BRICKS_DB_PAGE_CONTENT' ) ? BRICKS_DB_PAGE_CONTENT : null,
			'class_Bricks_Theme' => class_exists( '\\Bricks\\Theme' ),
			'class_Bricks_DB' => class_exists( '\\Bricks\\Database' ),
			'class_Bricks_Elements' => class_exists( '\\Bricks\\Elements' ),
			'function_bricks_is_frontend' => function_exists( 'bricks_is_frontend' ),
			'post_type_bricks_template' => post_type_exists( 'bricks_template' ),
		];

		$is_bricks = false;
		foreach ( $signals as $value ) {
			if ( ! empty( $value ) ) {
				$is_bricks = true;
				break;
			}
		}

		$template_name = strtolower( (string) $template->get( 'Name' ) );
		if ( false !== strpos( $template_name, 'bricks' ) ) {
			$is_bricks = true;
		}

		return [
			'bricks_active' => $is_bricks,
			'version' => defined( 'BRICKS_VERSION' ) ? BRICKS_VERSION : null,
			'theme' => [
				'current' => $theme->get( 'Name' ),
				'template' => $template->get( 'Name' ),
				'stylesheet' => $stylesheet->get( 'Name' ),
			],
			'signals' => $signals,
			'tools_ready' => [
				'bricks_docs',
				'bricks_status',
				'bricks_element_catalog',
				'bricks_schema',
				'bricks_page_read',
				'bricks_page_write',
				'bricks_template_list',
				'bricks_template_create',
				'bricks_globals_read',
			],
			'next_tools_to_wire' => [
				'bricks_global_classes_write',
				'bricks_theme_styles_write',
				'bricks_components_write',
			],
		];
	}

	public static function docs( array $args ) {
		if ( ! empty( $args['read'] ) ) {
			return self::fetch_bricks_doc_via_relay( (string) $args['read'] );
		}

		$catalog = self::docs_catalog();
		$topic = strtolower( trim( (string) ( $args['topic'] ?? '' ) ) );
		if ( '' !== $topic ) {
			if ( ! isset( $catalog['docs'][ $topic ] ) ) {
				return new WP_Error( 'unknown_topic', "Unknown topic '{$topic}'. Available: " . implode( ', ', $catalog['categories'] ) );
			}
			return [
				'topic' => $topic,
				'how_to_use' => $catalog['how_to_use'],
				'docs' => $catalog['docs'][ $topic ],
			];
		}

		return $catalog;
	}

	private static function docs_catalog(): array {
		$catalog = [
			'must_read_first' => [
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/', 'about' => 'THE Bricks data model. Documents the postmeta keys (_bricks_page_content_2 / _bricks_page_header_2 / _bricks_page_footer_2), the flat element array (id/name/parent/children/settings), the underscore-prefixed CSS settings, and the breakpoint/pseudo-class colon-suffix rules. Read before writing any Bricks content.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/elements/common/element/', 'about' => 'The exact shape of ONE element object: id (6-char), name, parent, children, settings, plus meta keys (_cssGlobalClasses, _conditions, _interactions, _attributes) and the selectors array (Bricks 2.0+). The unit you repeat to build a page.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/article/dynamic-data/', 'about' => 'The dynamic-tag vocabulary (post/taxonomy/author/user/site/custom-field) plus filters. These tags embed directly inside element settings.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/article/create-your-own-elements/', 'about' => 'The PHP element contract: extend \\Bricks\\Element, set $name/$category, set_controls(), render(). Read to understand what valid settings keys exist per element.' ],
				[ 'url' => 'https://github.com/DigiSavvy-Inc/bricks-builder-docs', 'about' => 'Community AI-optimized markdown mirror of the Bricks docs (actions, filters, controls, elements, templates). Built for LLM/MCP consumption — ideal to fetch wholesale.' ],
			],
			'data_model' => [
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/', 'about' => 'Data model hub: postmeta keys, element array, globals in wp_options.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/elements/common/element/', 'about' => 'Single-element envelope and meta keys.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/elements/common/conditions/', 'about' => 'Element display-conditions data structure.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/elements/common/interactions/', 'about' => 'Element interactions (event/action) data structure.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/settings/page/', 'about' => 'Page Settings schema — page-level postmeta (SEO, header/footer toggles, custom code), distinct from element content.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/settings/template/', 'about' => 'Template Settings schema — template-level config for headers/footers/single/archive.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/general/content-area/', 'about' => 'Content Area schema — the header/content/footer areas an element array belongs to.' ],
			],
			'elements_controls' => [
				[ 'url' => 'https://academy.bricksbuilder.io/article/create-your-own-elements/', 'about' => 'Register a custom element in PHP: controls + render contract (verified, with working example).' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/elements/nestable-elements/', 'about' => 'Nestable Elements API — custom container-type elements that hold children.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/article/element-controls/', 'about' => 'Element Controls reference: the control definition structure (tab, group, type, css, default, required).' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/controls/', 'about' => 'Controls index — every control type (text, color, typography, repeater, query, dimensions, etc.) and its options.' ],
			],
			'classes_components_styles' => [
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/global/theme-styles/', 'about' => 'Theme Styles schema (stored in the bricks_theme_styles option) — the richest global schema; 50+ sections.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/global/global-classes/', 'about' => 'Global Classes schema (id/name/category/settings/selectors). Thin/table-only — pair with the builder guide.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/global/components/', 'about' => 'Components schema (id/elements/properties/variants). Thin — pair with the builder Components guide below.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/builder/features/components/', 'about' => 'Builder guide for Components — the conceptual companion the schema page lacks.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/global/global-variables/', 'about' => 'Global Variables (CSS-variable) storage shape. Very thin/no-prose.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/global/color-palettes/', 'about' => 'Color Palettes option schema.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/global/breakpoints/', 'about' => 'Breakpoint definitions — required to emit correct :tablet_portrait / :mobile_landscape setting-key suffixes.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/global/pseudo-classes/', 'about' => 'Valid pseudo-state suffixes for settings keys.' ],
			],
			'hooks_filters' => [
				[ 'url' => 'https://academy.bricksbuilder.io/developer/hooks/', 'about' => 'Hooks landing page (actions + filters index).' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/hooks/filters/', 'about' => 'Filter Hooks reference — 200+ filters (query, dynamic data, render, theme styles, conditions, Woo).' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/hooks/actions/', 'about' => 'Action Hooks reference — render header/footer, query before/after, dynamic data lifecycle, css generation.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/hooks/filters/filter-bricks-frontend-render_element/', 'about' => 'Filter bricks/frontend/render_element — modify any element\'s frontend HTML.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/hooks/filters/filter-bricks-element-settings/', 'about' => 'Filter bricks/element/settings — mutate element settings at render.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/hooks/filters/filter-bricks-builder-elements/', 'about' => 'Filter bricks/builder/elements — register/alter the elements list.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/hooks/filters/filter-bricks-elements-element_name-controls/', 'about' => 'Filter bricks/elements/{name}/controls — add controls to existing elements without forking.' ],
			],
			'dynamic_data_loops' => [
				[ 'url' => 'https://academy.bricksbuilder.io/article/dynamic-data/', 'about' => 'Dynamic data reference — the complete tag vocabulary and filters to embed in settings.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/builder/dynamic-content/query-loop/', 'about' => 'Query Loop guide — looping posts/terms/users (a core layout pattern).' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/dynamic-data/create-your-own-dynamic-data-tag/', 'about' => 'Register custom dynamic data tags in PHP.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/functions/function-bricks_render_dynamic_data/', 'about' => 'bricks_render_dynamic_data() — resolve dynamic tags in PHP.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/controls/query-control/', 'about' => 'Query Control — the data shape for emitting query-loop config inside settings.' ],
			],
			'programmatic_writes' => [
				[ 'url' => 'https://academy.bricksbuilder.io/developer/schema/', 'about' => 'IMPORTANT: there is NO official WP-REST endpoint that creates Bricks content (Bricks content is not in the REST response). Programmatic writes go through update_post_meta() on _bricks_page_content_2 (+ header/footer keys). This schema page is the contract for that array.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/developer/guides/bricks-cli/', 'about' => 'Bricks WP-CLI — `wp bricks regenerate_assets` (run after programmatic writes when CSS is in external-file mode).' ],
				[ 'url' => 'https://brickssync.com/brickssync-documentation/', 'about' => 'BricksSync — WP-CLI JSON export/import of templates + settings between sites. The practical migration path that fills the REST gap.' ],
			],
			'community' => [
				[ 'url' => 'https://sinanisler.com/codex/bricks-builder-docs-custom-elements-php-bricks-controls-php/', 'about' => 'Sinan Isler\'s ~5000-word custom-element + controls reference with full working PHP examples. Best end-to-end tutorial outside the Academy.' ],
				[ 'url' => 'https://brickslabs.com/', 'about' => 'BricksLabs — 600+ practical code-snippet tutorials (query loops, dynamic data, custom code, Woo, JS).' ],
				[ 'url' => 'https://docs.bricksforge.io/', 'about' => 'Bricksforge add-on docs — extended dynamic-data tags, API Query Builder, form actions (widely used in the ecosystem).' ],
				[ 'url' => 'https://bricksextras.com/docs/dynamic-tags/', 'about' => 'BricksExtras docs — common third-party elements/tags you may encounter in existing sites.' ],
				[ 'url' => 'https://forum.bricksbuilder.io/c/developers/', 'about' => 'Bricks Community Forum, Developers category — canonical Q&A for data-model edge cases not yet in the Academy.' ],
			],
			'github' => [
				[ 'url' => 'https://github.com/cristianuibar/bricks-mcp', 'about' => 'PRIOR ART (92★, active): a WP plugin exposing the full Bricks data model as 11 MCP tools to Claude/Cursor. Study its data-model mapping and tool design.' ],
				[ 'url' => 'https://github.com/DigiSavvy-Inc/bricks-builder-docs', 'about' => 'AI-optimized markdown mirror of the Bricks docs (the Bricks analog of 16wells/divi-docs). Ideal to fetch/cache wholesale.' ],
				[ 'url' => 'https://github.com/Widdin/wp-bricks-elements', 'about' => 'Free custom Bricks elements collection (84★). NOTE: stale (pre-2.x, 2023) — examples only, not current.' ],
			],
			'official_status' => [
				[ 'url' => 'https://academy.bricksbuilder.io/', 'about' => 'Official Bricks Academy root.' ],
				[ 'url' => 'https://academy.bricksbuilder.io/collection/developer/', 'about' => 'The complete Developer landing page — index of guides, elements, controls, functions, dynamic data, hooks, and schema.' ],
				[ 'url' => 'https://bricksbuilder.io/changelog/', 'about' => 'Official changelog. Check before assuming a feature, schema, or hook is current (e.g. the selectors array arrived in 2.0).' ],
				[ 'url' => 'https://forum.bricksbuilder.io/', 'about' => 'Official Bricks community forum.' ],
			],
		];

		return [
			'how_to_use' => 'Before writing or mutating Bricks content programmatically: (1) read must_read_first — ESPECIALLY the data model (/developer/schema/) and the single-element schema, since Bricks content is a flat JSON element array stored in postmeta (_bricks_page_content_2 / header / footer), NOT WordPress blocks and NOT reachable via the standard REST API; (2) use data_model + elements_controls for exact element names and settings keys; (3) keep client-editable copy/media in native Bricks elements (Heading/Text/RichText/Button/Image), using global classes/components/custom CSS for effects rather than trapping content in Code/HTML elements; (4) check the changelog for recent schema changes. Use read:"<url>" to fetch any allowlisted page inline.',
			'categories' => array_keys( $catalog ),
			'docs' => $catalog,
			'allowed_hosts_for_relay_read_mode' => self::allowed_doc_hosts(),
		];
	}

	private static function allowed_doc_hosts(): array {
		return [
			'academy.bricksbuilder.io',
			'bricksbuilder.io',
			'www.bricksbuilder.io',
			'forum.bricksbuilder.io',
			'ideas.bricksbuilder.io',
			'github.com',
			'www.github.com',
			'raw.githubusercontent.com',
		];
	}

	private static function fetch_bricks_doc_via_relay( string $url ) {
		$url = trim( $url );
		if ( '' === $url ) {
			return new WP_Error( 'missing_url', 'Pass a `read` URL from the bricks_docs catalog, or any URL on an allowed Bricks docs host.' );
		}

		$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
		if ( ! in_array( $host, self::allowed_doc_hosts(), true ) ) {
			return new WP_Error( 'host_not_allowed', 'Bricks docs read mode only allows: ' . implode( ', ', self::allowed_doc_hosts() ) );
		}

		if ( ! class_exists( 'PCD_PC_Pair' ) || ! PCD_PC_Pair::is_paired() ) {
			return new WP_Error( 'not_paired', 'Conduit must be paired with the relay to fetch documentation pages.' );
		}

		$backend = PCD_PC_Pair::backend_url();
		if ( ! $backend ) {
			return new WP_Error( 'no_backend', 'Relay backend URL is not configured.' );
		}

		$response = wp_remote_post( $backend . '/bricks-doc', [
			'headers' => [
				'Content-Type'       => 'application/json',
				'Accept'             => 'application/json',
				'X-Conduit-Customer' => PCD_PC_Pair::customer_id(),
				'X-Conduit-Site'     => site_url(),
				'X-Conduit-Token'    => PCD_PC_Auth::get_token(),
				'X-Conduit-Product'  => 'pressconduit',
			],
			'body'    => wp_json_encode( [ 'url' => $url ] ),
			'timeout' => 25,
		] );

		if ( is_wp_error( $response ) ) {
			return new WP_Error( 'relay_unreachable', 'Relay request failed: ' . $response->get_error_message() );
		}

		$status = (int) wp_remote_retrieve_response_code( $response );
		$body = wp_remote_retrieve_body( $response );
		$json = json_decode( $body, true );

		if ( 200 !== $status ) {
			$err = is_array( $json ) && isset( $json['error'] ) ? $json['error'] : ( 'HTTP ' . $status );
			$extra = is_array( $json ) && isset( $json['allowed'] ) ? ( ' Allowed hosts: ' . implode( ', ', $json['allowed'] ) ) : '';
			return new WP_Error( 'relay_error', 'Relay declined to fetch this doc: ' . $err . $extra );
		}

		if ( ! is_array( $json ) || ! isset( $json['content'] ) ) {
			return new WP_Error( 'relay_bad_response', 'Relay returned an unexpected payload.' );
		}

		return [
			'url'        => $json['url'] ?? $url,
			'title'      => $json['title'] ?? '',
			'cached'     => ! empty( $json['cached'] ),
			'fetched_at' => $json['fetched_at'] ?? null,
			'content'    => (string) $json['content'],
			'warning'    => $json['warning'] ?? null,
		];
	}

	// ------------------------------------------------------------------
	// Element schema (read live from this site's Bricks element registry —
	// mirrors how divi_module_schema reads ET_Builder_Element::get_modules()).
	// ------------------------------------------------------------------

	public static function element_catalog( array $args ) {
		$registry = self::bricks_registry();
		if ( is_wp_error( $registry ) ) {
			return $registry;
		}
		$filter = strtolower( trim( (string) ( $args['category'] ?? '' ) ) );
		$rows   = [];
		foreach ( array_keys( $registry ) as $name ) {
			$info = self::bricks_element_info( (string) $name, $registry );
			if ( ! $info ) continue;
			if ( '' !== $filter && strtolower( $info['category'] ) !== $filter ) continue;
			$rows[] = $info;
		}
		usort( $rows, function ( $a, $b ) { return strcmp( $a['name'], $b['name'] ); } );

		$categories = array_values( array_unique( array_filter( array_map( function ( $r ) { return $r['category']; }, $rows ) ) ) );
		sort( $categories );

		return [
			'count'      => count( $rows ),
			'categories' => $categories,
			'note'       => 'Put the `name` value in an element object\'s `name` field. Call bricks_schema with a name for its full settings. `nestable: true` means the element can contain child elements (Section, Container, Block, Div, etc.) — those are your layout wrappers.',
			'elements'   => $rows,
		];
	}

	public static function schema( array $args ) {
		$name = trim( (string) ( $args['name'] ?? '' ) );
		if ( '' === $name ) {
			return new WP_Error( 'missing_name', 'Pass an element `name` (see bricks_element_catalog).' );
		}
		$registry = self::bricks_registry();
		if ( is_wp_error( $registry ) ) {
			return $registry;
		}
		if ( ! isset( $registry[ $name ] ) ) {
			return new WP_Error( 'not_found', "No Bricks element named '{$name}' on this site. Call bricks_element_catalog to list them." );
		}
		$class = $registry[ $name ]['class'] ?? '';
		if ( ! $class || ! class_exists( $class ) ) {
			return new WP_Error( 'no_class', "Element '{$name}' has no loadable class." );
		}

		try {
			$el = new $class();
			if ( method_exists( $el, 'load' ) ) {
				$el->load(); // populates ->controls (same call Bricks' own load_element makes)
			}
		} catch ( \Throwable $e ) {
			return new WP_Error( 'load_failed', "Could not load element '{$name}': " . $e->getMessage() );
		}

		$controls = is_array( $el->controls ?? null ) ? $el->controls : [];
		$compact  = ! isset( $args['compact'] ) || ! empty( $args['compact'] );
		if ( $compact ) {
			$controls = array_map( [ __CLASS__, 'compact_bricks_control' ], $controls );
		}
		$groups = is_array( $el->control_groups ?? null ) ? $el->control_groups : [];

		return [
			'name'           => $name,
			'label'          => method_exists( $el, 'get_label' ) ? $el->get_label() : ( $el->label ?? '' ),
			'category'       => $el->category ?? '',
			'tag'            => $el->tag ?? null,
			'nestable'       => ! empty( $el->nestable ),
			'control_groups' => $groups ?: new stdClass(),
			'control_count'  => count( $controls ),
			'controls'       => $controls ?: new stdClass(),
			'tip'            => 'These keys go in an element\'s `settings` object. A control with a `required` array is conditional (shown only when another control has a given value). Responsive/state variants append a colon suffix to the key (e.g. "width:tablet_portrait"). For the exact storage/encoding rules call bricks_docs and read the data model page.',
		];
	}

	// ------------------------------------------------------------------
	// Page read / write — the actual Bricks build capability. Bricks
	// content is a flat element array in postmeta (NOT post_content, NOT
	// the REST API). Mirrors divi_layout_get / divi_set_post_layout.
	// ------------------------------------------------------------------

	/** Map an `area` arg to its Bricks postmeta key. */
	private static function area_meta_key( string $area ): string {
		switch ( $area ) {
			case 'header':
				return defined( 'BRICKS_DB_PAGE_HEADER' ) ? BRICKS_DB_PAGE_HEADER : '_bricks_page_header_2';
			case 'footer':
				return defined( 'BRICKS_DB_PAGE_FOOTER' ) ? BRICKS_DB_PAGE_FOOTER : '_bricks_page_footer_2';
			case 'content':
			default:
				return defined( 'BRICKS_DB_PAGE_CONTENT' ) ? BRICKS_DB_PAGE_CONTENT : '_bricks_page_content_2';
		}
	}

	public static function page_read( array $args ) {
		$id = (int) ( $args['id'] ?? 0 );
		if ( ! $id || ! ( $post = get_post( $id ) ) ) {
			return new WP_Error( 'not_found', "Post #{$id} not found." );
		}
		$area = strtolower( (string) ( $args['area'] ?? 'content' ) );
		if ( ! in_array( $area, [ 'content', 'header', 'footer' ], true ) ) {
			$area = 'content';
		}
		$meta_key = self::area_meta_key( $area );
		$elements = get_post_meta( $id, $meta_key, true );
		$elements = is_array( $elements ) ? array_values( $elements ) : [];

		$editor_mode = get_post_meta( $id, defined( 'BRICKS_DB_EDITOR_MODE' ) ? BRICKS_DB_EDITOR_MODE : '_bricks_editor_mode', true );

		$out = [
			'id'            => $id,
			'title'         => $post->post_title,
			'post_type'     => $post->post_type,
			'status'        => $post->post_status,
			'area'          => $area,
			'meta_key'      => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- response field label, not a query argument.
			'editor_mode'   => $editor_mode ?: '(unset)',
			'element_count' => count( $elements ),
			'root_count'    => count( array_filter( $elements, function ( $e ) { return empty( $e['parent'] ); } ) ),
			'elements'      => $elements,
		];

		if ( ! isset( $args['tree'] ) || ! empty( $args['tree'] ) ) {
			$out['tree'] = self::elements_tree_summary( $elements );
		}
		if ( ! $elements ) {
			$out['note'] = "This region is empty. Editor mode is '{$out['editor_mode']}' — Bricks only renders its element array when editor mode is 'bricks' (bricks_page_write sets that for you).";
		}
		return $out;
	}

	public static function page_write( array $args ) {
		$id = (int) ( $args['id'] ?? 0 );
		if ( ! $id || ! ( $post = get_post( $id ) ) ) {
			return new WP_Error( 'not_found', "Post #{$id} not found." );
		}
		if ( ! array_key_exists( 'elements', $args ) ) {
			return new WP_Error( 'missing_elements', 'Pass `elements` (a flat array of Bricks element objects). Pass [] to clear the region.' );
		}
		$elements = $args['elements'];
		if ( ! is_array( $elements ) ) {
			return new WP_Error( 'bad_elements', '`elements` must be an array of element objects.' );
		}
		// Reject an associative array passed where a list is expected.
		if ( $elements && array_keys( $elements ) !== range( 0, count( $elements ) - 1 ) ) {
			return new WP_Error( 'bad_elements', '`elements` must be a flat (non-keyed) array. Each element is its own object with id/name/parent/children/settings.' );
		}

		$area = strtolower( (string) ( $args['area'] ?? 'content' ) );
		if ( ! in_array( $area, [ 'content', 'header', 'footer' ], true ) ) {
			$area = 'content';
		}
		$meta_key = self::area_meta_key( $area );

		// Normalize + validate before we touch anything.
		$normalized = self::normalize_elements( $elements );
		if ( is_wp_error( $normalized ) ) {
			return $normalized;
		}
		list( $clean, $warnings ) = $normalized;

		// Snapshot prior region content for undo.
		$prior = get_post_meta( $id, $meta_key, true );
		$snapshot_id = PCD_PC_Snapshot::capture( 'meta', "{$id}:{$meta_key}", [
			'post_id' => $id,
			'key'     => $meta_key,
			'value'   => is_array( $prior ) ? $prior : [],
		], "Bricks {$area} for #{$id} before page_write" );

		$editor_key = defined( 'BRICKS_DB_EDITOR_MODE' ) ? BRICKS_DB_EDITOR_MODE : '_bricks_editor_mode';

		if ( empty( $clean ) ) {
			delete_post_meta( $id, $meta_key );
			// If the body is now empty and no header/footer remain, fall back to the WP editor.
			$still_bricks = $area !== 'content' || get_post_meta( $id, self::area_meta_key( 'header' ), true ) || get_post_meta( $id, self::area_meta_key( 'footer' ), true );
			update_post_meta( $id, $editor_key, $still_bricks ? 'bricks' : 'wordpress' );
		} else {
			// wp_slash counteracts the wp_unslash inside the metadata API, so
			// backslashes in setting values (CSS, etc.) survive — same trick
			// Bricks' own save_post uses via Ajax::decode().
			update_post_meta( $id, $meta_key, wp_slash( $clean ) );
			update_post_meta( $id, $editor_key, 'bricks' );
		}

		// Bump the modified date + flush caches WITHOUT round-tripping
		// post_content through wp_update_post (that re-slashes existing
		// content and is a known corruption footgun). CSS regen is handled
		// explicitly below rather than relying on the save_post side effect.
		global $wpdb;
		$now = current_time( 'mysql' );
		$wpdb->update( $wpdb->posts, [ 'post_modified' => $now, 'post_modified_gmt' => current_time( 'mysql', true ) ], [ 'ID' => $id ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
		clean_post_cache( $id );

		$css = self::maybe_regenerate_css( ! empty( $args['regenerate_css'] ) );

		return [
			'id'            => $id,
			'area'          => $area,
			'meta_key'      => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- response field label, not a query argument.
			'applied'       => true,
			'element_count' => count( $clean ),
			'root_count'    => count( array_filter( $clean, function ( $e ) { return empty( $e['parent'] ); } ) ),
			'editor_mode'   => empty( $clean ) ? get_post_meta( $id, $editor_key, true ) : 'bricks',
			'snapshot_id'   => $snapshot_id,
			'css'           => $css,
			'warnings'      => $warnings,
			'edit_url'      => admin_url( "post.php?post={$id}&action=edit" ),
			'tip'           => $warnings ? 'Written, but review warnings — orphaned parent/child links can render as a flat stack.' : 'Written. View the page on the frontend to confirm the layout.',
		];
	}

	/**
	 * Validate + normalize a flat element array into Bricks' stored shape.
	 * Returns [ $clean_elements, $warnings ] or WP_Error on a fatal problem.
	 */
	private static function normalize_elements( array $elements ) {
		if ( empty( $elements ) ) {
			return [ [], [] ];
		}

		$warnings = [];
		$seen_ids = [];
		$clean    = [];

		// First pass: assign/dedupe ids and validate names.
		foreach ( $elements as $i => $el ) {
			if ( ! is_array( $el ) ) {
				return new WP_Error( 'bad_element', "Element at index {$i} is not an object." );
			}
			$name = isset( $el['name'] ) ? trim( (string) $el['name'] ) : '';
			if ( '' === $name ) {
				return new WP_Error( 'missing_name', "Element at index {$i} has no `name`. Every element needs a name (see bricks_element_catalog)." );
			}

			$eid = isset( $el['id'] ) ? trim( (string) $el['id'] ) : '';
			if ( '' === $eid || isset( $seen_ids[ $eid ] ) || ! preg_match( '/^[A-Za-z0-9]{4,}$/', $eid ) ) {
				if ( '' !== $eid && isset( $seen_ids[ $eid ] ) ) {
					$warnings[] = "Duplicate id '{$eid}' at index {$i} — regenerated.";
				}
				$eid = self::new_element_id( $seen_ids );
			}
			$seen_ids[ $eid ] = true;

			$el['id']   = $eid;
			$el['name'] = $name;
			$clean[]    = $el;
		}

		// Second pass: normalize parent/children/settings and build child maps.
		$id_set       = array_fill_keys( array_column( $clean, 'id' ), true );
		$declared_kids = [];
		foreach ( $clean as &$el ) {
			// parent: 0 for root, else a string id.
			if ( empty( $el['parent'] ) || 0 === $el['parent'] || '0' === $el['parent'] ) {
				$el['parent'] = 0;
			} else {
				$el['parent'] = (string) $el['parent'];
				if ( ! isset( $id_set[ $el['parent'] ] ) ) {
					$warnings[] = "Element '{$el['id']}' ({$el['name']}) names parent '{$el['parent']}' which is not in this array — treated as root.";
					$el['parent'] = 0;
				}
			}

			// children: array of string ids.
			if ( isset( $el['children'] ) && is_array( $el['children'] ) ) {
				$kids = array_values( array_filter( array_map( 'strval', $el['children'] ), function ( $cid ) use ( $id_set ) {
					return isset( $id_set[ $cid ] );
				} ) );
				$el['children'] = $kids;
				foreach ( $kids as $cid ) {
					$declared_kids[ $cid ] = $el['id'];
				}
			} else {
				$el['children'] = [];
			}

			// settings: associative array (object). Default empty object.
			if ( ! isset( $el['settings'] ) || ! is_array( $el['settings'] ) ) {
				$el['settings'] = [];
			}
		}
		unset( $el );

		// Cross-check parent vs children declarations (non-fatal hints).
		foreach ( $clean as $el ) {
			if ( $el['parent'] && isset( $declared_kids[ $el['id'] ] ) && $declared_kids[ $el['id'] ] !== $el['parent'] ) {
				$warnings[] = "Element '{$el['id']}' has parent '{$el['parent']}' but is listed under '{$declared_kids[ $el['id'] ]}' children — Bricks uses the children array; verify nesting.";
			}
		}

		return [ $clean, $warnings ];
	}

	/** Generate a Bricks-style 6-char id, avoiding ids already in $seen. */
	private static function new_element_id( array $seen ): string {
		for ( $attempt = 0; $attempt < 20; $attempt++ ) {
			if ( class_exists( '\\Bricks\\Helpers' ) && method_exists( '\\Bricks\\Helpers', 'generate_random_id' ) ) {
				$id = (string) \Bricks\Helpers::generate_random_id( false );
			} else {
				$id = substr( str_shuffle( str_repeat( '0123456789abcdefghijklmnopqrstuvwxyz', 2 ) ), 0, 6 );
			}
			if ( '' !== $id && ! isset( $seen[ $id ] ) ) {
				return $id;
			}
		}
		return substr( md5( uniqid( (string) wp_rand(), true ) ), 0, 6 );
	}

	/** Compact id/name nesting tree for human/AI scanning. */
	private static function elements_tree_summary( array $elements ): array {
		$by_parent = [];
		foreach ( $elements as $el ) {
			$pid = empty( $el['parent'] ) ? 0 : (string) $el['parent'];
			$by_parent[ $pid ][] = $el;
		}
		$build = function ( $parent ) use ( &$build, $by_parent ) {
			$out = [];
			foreach ( $by_parent[ $parent ] ?? [] as $el ) {
				$node = [ 'id' => $el['id'] ?? '?', 'name' => $el['name'] ?? '?' ];
				$kids = $build( $el['id'] ?? '' );
				if ( $kids ) {
					$node['children'] = $kids;
				}
				$out[] = $node;
			}
			return $out;
		};
		return $build( 0 );
	}

	/** Best-effort CSS regeneration (only meaningful in external-file CSS mode). */
	private static function maybe_regenerate_css( bool $requested ): array {
		$mode = ( class_exists( '\\Bricks\\Database' ) && method_exists( '\\Bricks\\Database', 'get_setting' ) )
			? \Bricks\Database::get_setting( 'cssLoading' )
			: null;

		if ( 'file' !== $mode ) {
			return [ 'regenerated' => false, 'reason' => 'CSS loading is inline (default) — Bricks generates CSS at render, no regen needed.' ];
		}
		if ( ! $requested ) {
			return [ 'regenerated' => false, 'reason' => 'Site uses external CSS files. Pass regenerate_css:true (or run `wp bricks regenerate_assets`) to rebuild them.' ];
		}
		if ( ! class_exists( '\\Bricks\\Assets_Files' ) ) {
			return [ 'regenerated' => false, 'reason' => 'Bricks\\Assets_Files unavailable; run `wp bricks regenerate_assets`.' ];
		}
		try {
			$files = \Bricks\Assets_Files::get_css_files_list( true );
			$count = 0;
			if ( is_array( $files ) ) {
				foreach ( $files as $index => $file ) {
					\Bricks\Assets_Files::regenerate_css_file( $file, $index, true );
					$count++;
				}
			}
			return [ 'regenerated' => true, 'files' => $count ];
		} catch ( \Throwable $e ) {
			return [ 'regenerated' => false, 'reason' => 'Regen failed: ' . $e->getMessage() . ' — run `wp bricks regenerate_assets`.' ];
		}
	}

	// ------------------------------------------------------------------
	// Templates (the bricks_template CPT) + globals (wp_options stores).
	// ------------------------------------------------------------------

	private static function template_type_key(): string {
		return defined( 'BRICKS_DB_TEMPLATE_TYPE' ) ? BRICKS_DB_TEMPLATE_TYPE : '_bricks_template_type';
	}
	private static function template_settings_key(): string {
		return defined( 'BRICKS_DB_TEMPLATE_SETTINGS' ) ? BRICKS_DB_TEMPLATE_SETTINGS : '_bricks_template_settings';
	}

	public static function template_list( array $args ) {
		if ( ! post_type_exists( 'bricks_template' ) ) {
			return new WP_Error( 'no_bricks', 'The bricks_template post type is not registered — Bricks is not active on this site.' );
		}
		$status = (string) ( $args['status'] ?? 'any' );
		$limit  = max( 1, min( 500, (int) ( $args['limit'] ?? 100 ) ) );
		$type   = strtolower( trim( (string) ( $args['type'] ?? '' ) ) );

		$query_args = [
			'post_type'      => 'bricks_template',
			'post_status'    => $status,
			'posts_per_page' => $limit,
			'orderby'        => 'title',
			'order'          => 'ASC',
		];
		if ( '' !== $type ) {
			$query_args['meta_query'] = [ [ 'key' => self::template_type_key(), 'value' => $type ] ]; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- optional filter by template type; small admin-only dataset.
		}
		$posts = get_posts( $query_args );

		$rows = [];
		foreach ( $posts as $p ) {
			$ttype = get_post_meta( $p->ID, self::template_type_key(), true );
			$areas = [];
			foreach ( [ 'content', 'header', 'footer' ] as $area ) {
				$els = get_post_meta( $p->ID, self::area_meta_key( $area ), true );
				if ( is_array( $els ) && $els ) {
					$areas[ $area ] = count( $els );
				}
			}
			$rows[] = [
				'id'             => $p->ID,
				'title'          => $p->post_title,
				'status'         => $p->post_status,
				'template_type'  => $ttype ?: '(unset)',
				'areas_with_content' => $areas ?: new stdClass(),
				'edit_url'       => admin_url( "post.php?post={$p->ID}&action=edit" ),
			];
		}
		return [
			'count'          => count( $rows ),
			'template_types' => [ 'header', 'footer', 'content', 'section', 'popup', 'archive', 'search', 'error' ],
			'note'           => 'Use bricks_page_read / bricks_page_write with a template id to read or build its elements. template_type "content" = Single template.',
			'templates'      => $rows,
		];
	}

	public static function template_create( array $args ) {
		if ( ! post_type_exists( 'bricks_template' ) ) {
			return new WP_Error( 'no_bricks', 'The bricks_template post type is not registered — Bricks is not active on this site.' );
		}
		$title = trim( (string) ( $args['title'] ?? '' ) );
		$type  = strtolower( trim( (string) ( $args['type'] ?? '' ) ) );
		$valid = [ 'header', 'footer', 'content', 'section', 'popup', 'archive', 'search', 'error' ];
		if ( '' === $title ) {
			return new WP_Error( 'missing_title', 'Pass a `title` for the template.' );
		}
		if ( ! in_array( $type, $valid, true ) ) {
			return new WP_Error( 'bad_type', 'type must be one of: ' . implode( ', ', $valid ) );
		}
		$status = (string) ( $args['status'] ?? 'publish' );

		$id = wp_insert_post( [
			'post_type'   => 'bricks_template',
			'post_status' => $status,
			'post_title'  => wp_slash( $title ),
		], true );
		if ( is_wp_error( $id ) ) {
			return $id;
		}

		update_post_meta( $id, self::template_type_key(), $type );
		update_post_meta( $id, defined( 'BRICKS_DB_EDITOR_MODE' ) ? BRICKS_DB_EDITOR_MODE : '_bricks_editor_mode', 'bricks' );
		if ( isset( $args['settings'] ) && is_array( $args['settings'] ) && $args['settings'] ) {
			update_post_meta( $id, self::template_settings_key(), wp_slash( $args['settings'] ) );
		}

		return [
			'id'            => (int) $id,
			'title'         => $title,
			'template_type' => $type,
			'status'        => $status,
			'next_step'     => "Call bricks_page_write with id {$id} to add elements. For a header/footer template, Bricks renders the `content` area as that region.",
			'edit_url'      => admin_url( "post.php?post={$id}&action=edit" ),
		];
	}

	public static function globals_read( array $args ) {
		if ( ! class_exists( '\\Bricks\\Theme' ) && ! defined( 'BRICKS_VERSION' ) ) {
			return new WP_Error( 'no_bricks', 'Bricks is not active on this site, so its global stores are unavailable.' );
		}
		$map = [
			'theme_styles'  => defined( 'BRICKS_DB_THEME_STYLES' ) ? BRICKS_DB_THEME_STYLES : 'bricks_theme_styles',
			'global_classes'=> defined( 'BRICKS_DB_GLOBAL_CLASSES' ) ? BRICKS_DB_GLOBAL_CLASSES : 'bricks_global_classes',
			'components'    => defined( 'BRICKS_DB_COMPONENTS' ) ? BRICKS_DB_COMPONENTS : 'bricks_components',
			'variables'     => defined( 'BRICKS_DB_GLOBAL_VARIABLES' ) ? BRICKS_DB_GLOBAL_VARIABLES : 'bricks_global_variables',
			'color_palette' => defined( 'BRICKS_DB_COLOR_PALETTE' ) ? BRICKS_DB_COLOR_PALETTE : 'bricks_color_palette',
			'breakpoints'   => defined( 'BRICKS_DB_BREAKPOINTS' ) ? BRICKS_DB_BREAKPOINTS : 'bricks_breakpoints',
		];

		$which = strtolower( trim( (string) ( $args['which'] ?? '' ) ) );
		if ( '' !== $which ) {
			if ( ! isset( $map[ $which ] ) ) {
				return new WP_Error( 'unknown_store', 'which must be one of: ' . implode( ', ', array_keys( $map ) ) );
			}
			$option = $map[ $which ];
			$value  = get_option( $option, [] );
			return [
				'store'    => $which,
				'option'   => $option,
				'count'    => is_array( $value ) ? count( $value ) : null,
				'data'     => $value,
				'tip'      => 'global_classes/components/variables entries carry stable `id`s — reference those ids from element settings (_cssGlobalClasses, component instances) rather than inlining styles.',
			];
		}

		$index = [];
		foreach ( $map as $key => $option ) {
			$value = get_option( $option, null );
			$index[ $key ] = [
				'option' => $option,
				'set'    => null !== $value && [] !== $value,
				'count'  => is_array( $value ) ? count( $value ) : ( null === $value ? 0 : 1 ),
			];
		}
		return [
			'note'   => 'Summary of Bricks global stores in wp_options. Call again with which:"<store>" for the full data of one.',
			'stores' => $index,
		];
	}

	/** Ensure Bricks elements are registered, return the registry array (or WP_Error). */
	private static function bricks_registry() {
		if ( ! class_exists( '\\Bricks\\Elements' ) ) {
			return new WP_Error( 'no_bricks', 'Bricks Builder is not active on this site, so element schemas are unavailable.' );
		}
		if ( empty( \Bricks\Elements::$elements ) && method_exists( '\\Bricks\\Elements', 'init_elements' ) ) {
			try { \Bricks\Elements::init_elements(); } catch ( \Throwable $e ) { /* fall through */ }
		}
		return is_array( \Bricks\Elements::$elements ) ? \Bricks\Elements::$elements : [];
	}

	/** Lightweight per-element identity (no full control load). */
	private static function bricks_element_info( string $name, array $registry ) {
		$entry = $registry[ $name ] ?? null;
		if ( ! is_array( $entry ) ) return null;
		$label    = (string) ( $entry['label'] ?? '' );
		$category = (string) ( $entry['category'] ?? '' );
		$nestable = $entry['nestable'] ?? null;
		$class    = $entry['class'] ?? '';

		// Pre-load registry entries only carry class/name/label — instantiate
		// (cheap: no load()) to read category + nestable from the class.
		if ( ( '' === $category || null === $nestable ) && $class && class_exists( $class ) ) {
			try {
				$el       = new $class();
				$label    = $label ?: ( method_exists( $el, 'get_label' ) ? (string) $el->get_label() : (string) ( $el->label ?? '' ) );
				$category = $category ?: (string) ( $el->category ?? '' );
				$nestable = $el->nestable ?? false;
			} catch ( \Throwable $e ) {
				$nestable = (bool) $nestable;
			}
		}

		return [
			'name'     => $name,
			'label'    => $label,
			'category' => $category,
			'nestable' => (bool) $nestable,
		];
	}

	/** Trim a Bricks control to the fields useful for emitting settings. */
	private static function compact_bricks_control( $control ) {
		if ( ! is_array( $control ) ) return $control;
		$out = [];
		foreach ( [ 'type', 'label', 'default', 'options', 'tab', 'group', 'required', 'placeholder', 'inline', 'multiple', 'min', 'max', 'units' ] as $k ) {
			if ( array_key_exists( $k, $control ) && '' !== $control[ $k ] && [] !== $control[ $k ] ) {
				$out[ $k ] = $control[ $k ];
			}
		}
		return $out ?: [ 'type' => $control['type'] ?? 'unknown' ];
	}
}
