<?php declare(strict_types=1);

namespace LolaCore\Actions;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The string meta_key is used in LLM tool schemas, not for querying.

class ToolSchemaGenerator
{
    private AbilitiesRegistry $registry;

    public function __construct(AbilitiesRegistry $registry)
    {
        $this->registry = $registry;
    }

    /**
     * Retrieves all read-only actions from the registry and formats them as OpenAI/Anthropic tools.
     *
     * @param string $format 'openai' or 'anthropic'
     * @return array
     */
    public function getReadTools(string $format = 'openai'): array
    {
        return $this->formatTools($this->getReadToolDefinitions(), $format);
    }

    /**
     * Retrieves write/destructive actions and formats them as OpenAI/Anthropic tools.
     *
     * @param string $format 'openai' or 'anthropic'
     * @return array
     */
    public function getWriteTools(string $format = 'openai'): array
    {
        return $this->formatTools($this->getWriteToolDefinitions(), $format);
    }

    /**
     * Retrieves ALL available tools (read + write) for the LLM.
     *
     * @param string $format 'openai' or 'anthropic'
     * @return array
     */
    public function getAllTools(string $format = 'openai'): array
    {
        $all = array_merge($this->getReadToolDefinitions(), $this->getWriteToolDefinitions());
        return $this->formatTools($all, $format);
    }

    /**
     * Returns the list of tool names that require user confirmation before execution.
     *
     * @return array<string>
     */
    public function getWriteToolNames(): array
    {
        return array_map(fn(array $t) => $t['name'], $this->getWriteToolDefinitions());
    }

    /**
     * Read-only tool definitions (no confirmation needed).
     */
    private function getReadToolDefinitions(): array
    {
        return [
            [
                'name' => 'getSiteStatus',

                'category' => 'global',

                'keywords' => ['exact' => ['status', 'estado', 'informacion', 'salud', 'health', 'metrics', 'metricas', 'entorno', 'environment'], 'stem' => []],
                'description' => 'Retrieves general status of the WordPress site (versions, SSL, active theme, active plugins count, DB size).',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [],
                ]
            ],
            [
                'name' => 'auditPlugins',

                'category' => 'plugins',

                'keywords' => ['exact' => ['plugin', 'plugins', 'complemento', 'complementos', 'extension', 'extensiones', 'ver', 'show', 'list'], 'stem' => ['audit', 'revis', 'mostr', 'list', 'show']],
                'description' => 'Retrieves a list of all installed plugins, their active status, and if they have updates available.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [],
                ]
            ],
            [
                'name' => 'getDatabaseStatus',

                'category' => 'database',

                'keywords' => ['exact' => ['database', 'db', 'base de datos', 'revisiones', 'revisions', 'transients', 'rendimiento', 'performance'], 'stem' => []],
                'description' => 'Retrieves database metrics like size, number of revisions, and transients.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [],
                ]
            ],
            [
                'name' => 'getOptions',

                'category' => 'system',

                'keywords' => ['exact' => ['options', 'settings', 'configuration', 'opciones', 'ajustes', 'configuracion', 'sistema', 'system'], 'stem' => ['configur', 'ajust']],
                'description' => 'Retrieves specific non-sensitive WordPress options by key.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'keys' => [
                            'type' => 'array',
                            'items' => ['type' => 'string'],
                            'description' => 'List of option keys to retrieve (e.g., blogname, siteurl)'
                        ]
                    ],
                    'required' => ['keys']
                ]
            ],
            [
                'name' => 'getContent',

                'category' => 'content',

                'keywords' => ['exact' => ['post', 'page', 'content', 'article', 'blog', 'meta', 'trash', 'entrada', 'pagina', 'contenido', 'articulo', 'cuerpo', 'extracto', 'papelera', 'borrador', 'draft', 'title', 'titulo', 'ver', 'dame', 'show', 'list', 'find', 'display'], 'stem' => ['le', 'mostr', 'obten', 'list', 'show', 'find', 'busc', 'enseñ']],
                'description' => 'Retrieves posts, pages, or custom post types with full body content (HTML), excerpt, slug, and dates. Use post_type parameter to target specific types.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'post_type' => ['type' => 'string', 'description' => 'Post type to retrieve: post, page, product, or any CPT slug (default: post)'],
                        'status' => ['type' => 'string', 'description' => 'Post status filter: publish, draft, pending, private (default: publish)'],
                        'limit' => ['type' => 'integer', 'description' => 'Max number of results (default: 10, max: 50)'],
                        'search' => ['type' => 'string', 'description' => 'Search keyword to filter posts by title or content']
                    ]
                ]
            ],
            [
                'name' => 'diagnoseSeo',

                'category' => 'seo',

                'keywords' => ['exact' => ['seo', 'optimization', 'search engine', 'rank', 'meta description', 'optimizacion', 'motor de busqueda', 'ranking', 'posicionamiento'], 'stem' => ['diagnostic', 'optimiz', 'analiz']],
                'description' => 'Performs a comprehensive SEO audit: missing meta descriptions, images without alt text, permalink structure, search engine visibility. Returns findings by severity (critical, warning, info).',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'limit' => [
                            'type' => 'integer',
                            'description' => 'Maximum number of posts to analyze (default: 50)'
                        ]
                    ],
                ]
            ],
            [
                'name' => 'auditSecurity',

                'category' => 'security',

                'keywords' => ['exact' => ['security', 'audit', 'vulnerability', 'protection', 'safety', 'firewall', 'seguridad', 'auditoria', 'vulnerabilidad', 'proteccion', 'cortafuegos', 'virus', 'malware'], 'stem' => ['revis', 'audit', 'analiz']],
                'description' => 'Performs a security audit: exposed debug.log, WP_DEBUG status, weak admin usernames, XML-RPC, file editor access, WP version exposure. Returns findings with severity and recommendations.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [],
                ]
            ],
            [
                'name' => 'searchPlugins',

                'category' => 'plugins',

                'keywords' => ['exact' => ['plugin', 'plugins', 'complemento', 'complementos', 'extension', 'extensiones'], 'stem' => ['busc', 'search']],
                'description' => 'Searches the WordPress.org plugin repository by keyword. Returns top results with name, slug, version, active installs, rating, and compatibility. Use this to find plugins before installing.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'keyword' => [
                            'type' => 'string',
                            'description' => 'Search term (e.g., "seo", "woocommerce", "contact form")'
                        ],
                        'limit' => [
                            'type' => 'integer',
                            'description' => 'Max results to return (default 5, max 10)'
                        ]
                    ],
                    'required' => ['keyword']
                ]
            ],
            [
                'name' => 'getPostMeta',

                'category' => 'content',

                'keywords' => ['exact' => ['post', 'page', 'content', 'article', 'blog', 'meta', 'trash', 'entrada', 'pagina', 'contenido', 'articulo', 'cuerpo', 'extracto', 'papelera', 'borrador', 'draft', 'title', 'titulo', 'custom field', 'campo personalizado'], 'stem' => []],
                'description' => 'Reads meta values for a specific post, page, or product. Can return all meta or specific keys. Useful for reading plugin data like Yoast SEO fields, WooCommerce product data, ACF custom fields, etc.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'post_id' => [
                            'type' => 'integer',
                            'description' => 'The post/page/product ID to read meta from'
                        ],
                        'keys' => [
                            'type' => 'array',
                            'items' => ['type' => 'string'],
                            'description' => 'Specific meta keys to read (e.g., _yoast_wpseo_metadesc, _price). If empty, returns all meta.'
                        ]
                    ],
                    'required' => ['post_id']
                ]
            ],
            [
                'name' => 'getDiscoveredAbilities',

                'category' => 'global',

                'keywords' => ['exact' => ['habilidades', 'abilities', 'capacidades', 'capabilities', 'herramientas', 'tools', 'funciones'], 'stem' => ['herramienta', 'capabilit', 'abilit']],
                'description' => 'Lists all abilities registered in the WordPress Abilities API, including those from other plugins. Shows what Lola and other AI agents can do on this site.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [],
                ]
            ],
            [
                'name' => 'getUsers',

                'category' => 'users',

                'keywords' => ['exact' => ['user', 'users', 'author', 'subscriber', 'editor', 'administrator', 'role', 'profile', 'account', 'email', 'password', 'usuario', 'usuarios', 'autor', 'suscriptor', 'administrador', 'rol', 'perfil', 'cuenta', 'correo', 'contraseña', 'ver', 'show', 'list'], 'stem' => ['mostr', 'list', 'show']],
                'description' => 'Retrieves a list of WordPress users with their roles and IDs.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [],
                ]
            ],
            [
                'name' => 'getRoles',

                'category' => 'users',

                'keywords' => ['exact' => ['role', 'roles', 'capability', 'capabilities', 'permission', 'permissions', 'access', 'rol', 'roles', 'capacidad', 'capacidades', 'permiso', 'permisos', 'acceso'], 'stem' => []],
                'description' => 'Lists all WordPress roles with their display names, user count, and capabilities. Use this to understand what permissions each role has before creating or modifying roles.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'role_slug' => ['type' => 'string', 'description' => 'Optional: specific role slug to get full capability details for (e.g., editor, administrator). If omitted, lists all roles with summary info.']
                    ]
                ]
            ],
            [
                'name' => 'getTaxonomies',

                'category' => 'taxonomies',

                'keywords' => ['exact' => ['category', 'tag', 'taxonomy', 'term', 'hierarchy', 'categoria', 'categorias', 'etiqueta', 'etiquetas', 'taxonomia', 'termino', 'jerarquia'], 'stem' => ['organiz']],
                'description' => 'Lists categories, tags, or custom taxonomies with their terms and hierarchy.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'taxonomy' => ['type' => 'string', 'description' => 'The taxonomy slug (e.g., category, post_tag, product_cat). Omit to list all available taxonomies.'],
                        'hide_empty' => ['type' => 'boolean', 'description' => 'If true, only returns terms that are assigned to at least one post. Default is true.']
                    ]
                ]
            ],
            [
                'name' => 'getComments',

                'category' => 'comments',

                'keywords' => ['exact' => ['comment', 'comments', 'spam', 'trash', 'discussion', 'moderation', 'feedback', 'comentario', 'comentarios', 'papelera', 'discusion', 'moderacion', 'opinion'], 'stem' => []],
                'description' => 'Lists WordPress comments. Can filter by status (approve, hold, spam, trash).',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'status' => ['type' => 'string', 'enum' => ['approve', 'hold', 'spam', 'trash', 'all'], 'description' => 'Filter by comment status (default: approve)'],
                        'post_id' => ['type' => 'integer', 'description' => 'Filter comments by a specific post ID'],
                        'limit' => ['type' => 'integer', 'description' => 'Max comments to return (default: 20)']
                    ]
                ]
            ],
            [
                'name' => 'getMedia',

                'category' => 'media',

                'keywords' => ['exact' => ['media', 'image', 'images', 'attachment', 'file', 'photo', 'video', 'pdf', 'library', 'gallery', 'picture', 'medios', 'imagen', 'imagenes', 'adjunto', 'archivo', 'archivos', 'foto', 'galeria', 'biblioteca', 'documento'], 'stem' => ['subi', 'upload']],
                'description' => 'Lists files in the WordPress Media Library (images, PDFs, videos).',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'mime_type' => ['type' => 'string', 'description' => 'Filter by mime type (e.g., image/jpeg, application/pdf). Omit for all types.'],
                        'search' => ['type' => 'string', 'description' => 'Search keyword for media titles or descriptions.'],
                        'limit' => ['type' => 'integer', 'description' => 'Max files to return (default: 20)']
                    ]
                ]
            ],
            [
                'name' => 'getMenus',

                'category' => 'menus',

                'keywords' => ['exact' => ['menu', 'menus', 'navigation', 'link', 'subitem', 'header', 'footer', 'location', 'navbar', 'navegacion', 'enlace', 'subelemento', 'cabecera', 'pie', 'ubicacion', 'barra'], 'stem' => []],
                'description' => 'Lists all registered WordPress navigation menus and their items (links, pages, categories). Shows menu locations and item hierarchy.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'menu_id' => ['type' => 'integer', 'description' => 'Specific menu ID to retrieve items for. If omitted, lists all registered menus with their locations.']
                    ]
                ]
            ],
            [
                'name' => 'getSiteSettings',

                'category' => 'system',

                'keywords' => ['exact' => ['options', 'settings', 'configuration', 'opciones', 'ajustes', 'configuracion', 'sistema', 'system'], 'stem' => ['configur', 'ajust']],
                'description' => 'Retrieves grouped WordPress site settings in a structured payload: General (blogname, description, timezone, language, date/time format), Reading (posts_per_page, show_on_front, page_on_front), Discussion (comments, moderation), and Permalinks.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'group' => ['type' => 'string', 'enum' => ['general', 'reading', 'discussion', 'permalinks', 'all'], 'description' => 'Which settings group to retrieve (default: all)']
                    ]
                ]
            ],
            [
                'name' => 'getThemeInfo',

                'category' => 'themes',

                'keywords' => ['exact' => ['theme', 'themes', 'tema', 'temas', 'plantilla', 'plantillas', 'apariencia', 'appearance', 'design', 'diseño'], 'stem' => []],
                'description' => 'Gets info about the active WordPress theme and lists all installed themes with name, version, update availability, and parent/child relationships.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [],
                ]
            ],
            [
                'name' => 'searchThemes',

                'category' => 'themes',

                'keywords' => ['exact' => ['theme', 'themes', 'tema', 'temas', 'plantilla', 'plantillas', 'apariencia', 'appearance', 'design', 'diseño'], 'stem' => ['busc', 'search', 'find', 'discover']],
                'description' => 'Searches themes on the WordPress.org repository by keyword. Returns name, slug, version, author, description, rating, downloads, and PHP/WP requirements for up to 10 themes. Use the returned slug with installTheme.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'keyword' => ['type' => 'string', 'description' => 'Search keyword (e.g., "minimalist", "blog", "ecommerce")'],
                        'limit'   => ['type' => 'integer', 'description' => 'Maximum number of results (default 5, max 10)']
                    ],
                    'required' => ['keyword']
                ]
            ],
            [
                'name' => 'exportContent',

                'category' => 'content',

                'keywords' => ['exact' => ['post', 'page', 'content', 'article', 'blog', 'meta', 'trash', 'entrada', 'pagina', 'contenido', 'articulo', 'cuerpo', 'extracto', 'papelera', 'borrador', 'draft', 'title', 'titulo', 'xml', 'wxr'], 'stem' => ['export']],
                'description' => 'Generates a WordPress WXR (XML) export of posts, pages, or a specific custom post type. Returns a temporary download URL valid for 1 hour.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'post_type' => ['type' => 'string', 'description' => 'Post type to export: post, page, or any CPT slug (default: post)'],
                        'status' => ['type' => 'string', 'enum' => ['publish', 'draft', 'all'], 'description' => 'Post status filter (default: all)'],
                        'start_date' => ['type' => 'string', 'description' => 'Export posts from this date (YYYY-MM-DD). Optional.'],
                        'end_date' => ['type' => 'string', 'description' => 'Export posts up to this date (YYYY-MM-DD). Optional.']
                    ]
                ]
            ],
            // --- WooCommerce Read Tools ---
            [
                'name' => 'wcGetProducts',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => []],
                'description' => 'Lists WooCommerce products with name, price, stock status, and type. Only available if WooCommerce is active.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'status' => ['type' => 'string', 'enum' => ['publish', 'draft', 'pending', 'private'], 'description' => 'Filter by status (default: publish)'],
                        'limit' => ['type' => 'integer', 'description' => 'Max products to return (default: 20, max: 100)'],
                        'page' => ['type' => 'integer', 'description' => 'Page number for pagination (default: 1)'],
                        'search' => ['type' => 'string', 'description' => 'Search keyword to filter products by name'],
                        'category' => ['type' => 'string', 'description' => 'Filter by product category slug'],
                        'orderby' => ['type' => 'string', 'enum' => ['date', 'title', 'price', 'popularity', 'rating'], 'description' => 'Sort by field (default: date)'],
                        'order' => ['type' => 'string', 'enum' => ['ASC', 'DESC'], 'description' => 'Sort direction (default: DESC)'],
                        'type' => ['type' => 'string', 'enum' => ['simple', 'variable', 'grouped', 'external'], 'description' => 'Filter by product type']
                    ]
                ]
            ],
            [
                'name' => 'wcGetProduct',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => []],
                'description' => 'Gets detailed info for a single WooCommerce product: price, stock, SKU, categories, attributes, dimensions, weight, images, and sales data.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'product_id' => ['type' => 'integer', 'description' => 'The WooCommerce product ID']
                    ],
                    'required' => ['product_id']
                ]
            ],
            [
                'name' => 'wcGetOrders',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => []],
                'description' => 'Lists WooCommerce orders with status, total, customer, and date. Only available if WooCommerce is active.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'status' => ['type' => 'string', 'enum' => ['pending', 'processing', 'on-hold', 'completed', 'cancelled', 'refunded', 'failed'], 'description' => 'Filter by order status'],
                        'limit' => ['type' => 'integer', 'description' => 'Max orders to return (default: 20)'],
                        'customer_id' => ['type' => 'integer', 'description' => 'Filter by customer user ID']
                    ]
                ]
            ],
            [
                'name' => 'wcGetOrder',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => []],
                'description' => 'Gets full details of a single WooCommerce order: line items, shipping, billing address, payment method, totals, and notes.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'order_id' => ['type' => 'integer', 'description' => 'The WooCommerce order ID']
                    ],
                    'required' => ['order_id']
                ]
            ],
            [
                'name' => 'wcGetCustomers',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => []],
                'description' => 'Lists WooCommerce customers with name, email, order count, and total spent. Only available if WooCommerce is active.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'limit' => ['type' => 'integer', 'description' => 'Max customers to return (default: 20)'],
                        'orderby' => ['type' => 'string', 'enum' => ['registered', 'order_count', 'total_spent'], 'description' => 'Sort customers by (default: registered)']
                    ]
                ]
            ],
            [
                'name' => 'wcGetCategories',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => []],
                'description' => 'Lists WooCommerce product categories with name, slug, hierarchy, and product count. Only available if WooCommerce is active.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'hide_empty' => ['type' => 'boolean', 'description' => 'If true, only returns categories with products (default: false)'],
                        'parent' => ['type' => 'integer', 'description' => 'Filter by parent category ID. 0 = top-level only.']
                    ]
                ]
            ],
            [
                'name' => 'wcGetCoupons',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => []],
                'description' => 'Lists WooCommerce discount coupons with code, type, amount, usage count, usage limit, and expiry date. Only available if WooCommerce is active.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'limit' => ['type' => 'integer', 'description' => 'Max coupons to return (default: 20)'],
                        'status' => ['type' => 'string', 'enum' => ['publish', 'draft', 'pending'], 'description' => 'Filter by coupon status (default: publish)']
                    ]
                ]
            ],
            [
                'name' => 'wcGetReports',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => []],
                'description' => 'Returns a sales performance summary: total sales, net revenue, order count, items sold, and top-selling products. Only available if WooCommerce is active.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'period' => ['type' => 'string', 'enum' => ['today', '7days', '30days', 'month', 'year'], 'description' => 'Time period for the report (default: 7days)']
                    ]
                ]
            ],
            // --- Meta-tool: Tool Escalation ---
            [
                'name' => 'requestFullToolset',

                'category' => 'meta',

                'keywords' => ['exact' => [], 'stem' => []],
                'description' => 'ESCALATION ONLY — call when a needed tool is clearly absent from your current set and you have evidence it should exist. Reloads with all tools. Max once per conversation.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'reason' => [
                            'type' => 'string',
                            'description' => 'Why the missing tool is needed (e.g., "Need updateOption to change woocommerce_coming_soon")'
                        ]
                    ],
                    'required' => ['reason']
                ]
            ],
            // --- Memory Tools ---
            [
                'name' => 'addMemoryFact',

                'category' => 'memory',

                'keywords' => ['exact' => ['memory', 'remember', 'note', 'save', 'store', 'fact', 'preference', 'learn', 'memorize', 'memoria', 'recuerda', 'recordar', 'guardar', 'almacenar', 'nota', 'aprender', 'memorizar', 'preferencia'], 'stem' => ['memori', 'recuerd', 'guard', 'almacen', 'aprend', 'rememb', 'sav', 'stor', 'learn', 'memoriz']],
                'description' => 'Saves a fact to Lola\'s persistent memory (survives across sessions). Call when user asks to remember something, or when a significant decision/preference/config happens. Classify accurately.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'content' => [
                            'type' => 'string',
                            'description' => 'Clear, atomic statement (e.g., "User prefers dark mode", "Site uses WooCommerce for physical products")'
                        ],
                        'fact_type' => [
                            'type' => 'string',
                            'enum' => ['user_preference', 'business_context', 'site_state', 'decision_history'],
                            'description' => 'user_preference, business_context, site_state, or decision_history'
                        ],
                        'importance' => [
                            'type' => 'integer',
                            'description' => '1 (trivial) to 10 (critical). Default: 5.'
                        ]
                    ],
                    'required' => ['content', 'fact_type']
                ]
            ],
            // --- Skills Tools ---
            [
                'name' => 'addSkill',

                'category' => 'skills',

                'keywords' => ['exact' => ['skill', 'skills', 'overlay', 'behavior', 'behaviour', 'create skill', 'save skill', 'nueva skill', 'crear skill', 'guarda skill', 'comportamiento', 'pauta', 'directriz', 'regla'], 'stem' => ['skill', 'overlay', 'behav', 'comport', 'pauta', 'regla', 'directriz']],
                'description' => "Stores a new behavioral skill in inactive state. Inactive skills do not affect responses until the user activates them — there is no separate approval step, just create then optionally activate.\n\nCall this tool IMMEDIATELY in the same turn the user asks to create/save a skill. Do NOT announce your intent in text first.\n\nThe `content` parameter MUST follow this exact format (the `---` delimiters are mandatory):\n\n---\nname: kebab-case-name\ndescription: One-sentence summary, max 200 chars.\npriority: 50\nalways: true              # OR: omit `always` and define triggers (see below)\n---\nDirect instructions describing the behavioral rule, written as if Lola were reading them to herself. Be concrete and specific.\n\nActivation strategy — CRITICAL:\n- Tone, style, language, formatting, or any persistent behavior that should affect EVERY answer → set `always: true`. Skip triggers.\n- Topic-specific skill (e.g. SEO advice, fires only on relevant questions) → omit `always`, add a `triggers:` block with a `keywords:` list of 3-6 words.\n- When the user's request is about how you talk / write / respond in general → use `always: true`.\n\nOther rules:\n- `name` and `description` are required.\n- `priority` is optional (0-100, default 50).\n- If the user did not provide a `skill_name`, derive a sensible kebab-case one from the request (e.g. \"always respond in a formal tone\" → `formal-tone`).\n- The opening and closing `---` lines must be literal three-dash delimiters on their own line.\n\nAfter creation, ALWAYS finish your reply by asking the user whether to activate it now.",
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'skill_name' => [
                            'type' => 'string',
                            'description' => 'Kebab-case unique name for the skill (e.g. "seo-advisor", "formal-tone").'
                        ],
                        'content' => [
                            'type' => 'string',
                            'description' => "Full skill markdown. MUST start with `---`, frontmatter, `---`, then body.\n\nExample for an always-on tone skill:\n---\nname: formal-tone\ndescription: Always address the user formally and avoid colloquial expressions.\npriority: 50\nalways: true\n---\nAddress the user formally. Use precise, professional vocabulary. Never use emojis or colloquialisms.\n\nExample for a topic-triggered skill:\n---\nname: seo-advisor\ndescription: Surfaces concrete SEO suggestions when the user asks about search rankings.\npriority: 60\ntriggers:\n  keywords: [seo, ranking, google, index, search]\n---\nWhen the user asks about SEO, always check the active SEO plugin, then propose ONE concrete action with reasoning."
                        ]
                    ],
                    'required' => ['skill_name', 'content']
                ]
            ],
            // --- List skills (read — ground truth, prevents hallucinations) ---
            [
                'name' => 'listSkills',

                'category' => 'skills',

                'keywords' => ['exact' => ['list skills', 'lista skills', 'lista de skills', 'lista las skills', 'mis skills', 'my skills', 'show skills', 'muestra skills', 'cuantas skills', 'how many skills', 'qué skills', 'que skills', 'available skills'], 'stem' => ['list', 'lista', 'show', 'mostr', 'avail', 'disponib']],
                'description' => "Returns the real, current state of every skill registered in this site (built-in, addon, user-generated). Includes name, description, status (active / inactive / no-triggers / incompatible / unmet-dependencies / archived), origin, priority, and whether the skill is currently effective in the system prompt.\n\nALWAYS call this tool before answering any question about which skills exist, which are active, or what state a particular skill is in. NEVER answer from memory — memory has stale session summaries that lead to hallucinated skills.\n\nOptional `status` parameter filters by status. Omit it to return everything.",
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'status' => [
                            'type' => 'string',
                            'enum' => ['active', 'inactive', 'no-triggers', 'incompatible', 'unmet-dependencies', 'archived'],
                            'description' => 'Optional. Filter the result to skills in this status only.'
                        ]
                    ]
                ]
            ],
            // --- Update skill (write — requires confirmation) ---
            // Registered here for router discoverability; ActionExecutor flags it as
            // type=write, so it goes through the confirmation-card flow.
            [
                'name' => 'updateSkill',

                'category' => 'skills',

                'keywords' => ['exact' => ['edit skill', 'editar skill', 'update skill', 'actualizar skill', 'modify skill', 'modificar skill', 'mejora skill', 'mejorar skill', 'mejora la skill', 'refinar skill', 'refine skill', 'cambia la skill', 'cambiar skill', 'añadir triggers', 'add triggers'], 'stem' => ['edit', 'updat', 'modif', 'mejor', 'refin', 'cambi']],
                'description' => "Replaces the full markdown (frontmatter + body) of an existing skill. Status is preserved — an active skill stays active, an inactive one stays inactive.\n\nUse this when the user asks to modify, refine, improve, or add triggers/rules to an existing skill — for example: \"mejora la skill X añadiéndole...\", \"añade triggers a la skill Y\", \"refina la skill Z\".\n\nThe system shows a confirmation card before applying the change. Do not preview the YAML in text — just call the tool.\n\nThe new `content` MUST be a valid skill markdown with the same format expected by addSkill (frontmatter delimited by `---`, then body).",
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'skill_name' => [
                            'type' => 'string',
                            'description' => 'The kebab-case name of the existing skill to update.'
                        ],
                        'content' => [
                            'type' => 'string',
                            'description' => 'The complete new skill markdown (frontmatter + body) that replaces the current one. Same format as addSkill.'
                        ]
                    ],
                    'required' => ['skill_name', 'content']
                ]
            ],
            // --- Activate skill (write — requires confirmation) ---
            // Note: registered here in read-tools for discoverability by the router,
            // but ActionExecutor flags it as type=write, which routes it through the
            // confirmation-card flow in Controller.php.
            [
                'name' => 'activateSkill',

                'category' => 'skills',

                'keywords' => ['exact' => ['activate skill', 'activar skill', 'activala', 'actívala', 'aplicar skill', 'turn on skill', 'enciende skill', 'encender skill'], 'stem' => ['activ', 'aplic', 'encend']],
                'description' => "Switches an inactive skill on so it starts influencing every reply that matches its rules. There is only ONE concept of activation — whether the skill was just created by addSkill or had been previously deactivated by the user, this is the tool that turns it on.\n\nCall this tool when the user confirms they want a skill to start applying — for example after addSkill, you ask \"¿la activo?\" and they reply \"sí\".\n\nThe system shows a confirmation card before applying. Do not preview the action in text — just call the tool.",
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'skill_name' => [
                            'type' => 'string',
                            'description' => 'The skill_name (kebab-case) of the skill to activate.'
                        ]
                    ],
                    'required' => ['skill_name']
                ]
            ],
            // --- Deactivate skill (write — requires confirmation) ---
            [
                'name' => 'deactivateSkill',

                'category' => 'skills',

                'keywords' => ['exact' => ['desactivar skill', 'deactivate skill', 'pausa skill', 'pausar skill', 'pause skill', 'detener skill', 'apaga skill', 'apagar skill', 'disable skill', 'turn off skill'], 'stem' => ['desactiv', 'pausa', 'paus', 'apag', 'disabl', 'deactiv', 'turn off']],
                'description' => "Switches an active skill off so it stops firing in the system prompt. The skill is NOT deleted — its content is preserved and it can be reactivated later with activateSkill. Use this when the user wants to pause a skill temporarily.\n\nThe system shows a confirmation card before applying the change.",
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'skill_name' => [
                            'type' => 'string',
                            'description' => 'The skill_name (kebab-case) of the active skill to deactivate.'
                        ]
                    ],
                    'required' => ['skill_name']
                ]
            ],
            // --- Delete skill (write — requires confirmation, destructive) ---
            [
                'name' => 'deleteSkill',

                'category' => 'skills',

                'keywords' => ['exact' => ['delete skill', 'elimina skill', 'eliminar skill', 'borra skill', 'borrar skill', 'remove skill', 'remueve skill', 'descarta skill', 'discard skill', 'tira skill'], 'stem' => ['delet', 'elimin', 'borr', 'remov', 'remuev', 'descart', 'discard']],
                'description' => "Permanently removes a user-generated skill from the database. This is destructive and cannot be undone — the skill's content, frontmatter and history are wiped.\n\nOnly works on skills with origin=db (created by the user or by Lola). Built-in / addon / filesystem skills cannot be deleted from chat — those are managed elsewhere.\n\nThe system shows a confirmation card before applying. If the user has any doubt, suggest deactivateSkill instead (preserves the content).",
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'skill_name' => [
                            'type' => 'string',
                            'description' => 'The skill_name (kebab-case) of the skill to permanently delete.'
                        ]
                    ],
                    'required' => ['skill_name']
                ]
            ]
        ];
    }

    /**
     * Write/destructive tool definitions (require user confirmation).
     */
    private function getWriteToolDefinitions(): array
    {
        return [
            [
                'name' => 'updatePlugin',

                'category' => 'plugins',

                'keywords' => ['exact' => ['plugin', 'plugins', 'complemento', 'complementos', 'extension', 'extensiones'], 'stem' => ['actualiz', 'updat', 'subi']],
                'description' => 'Updates a WordPress plugin to its latest version. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission. Always warn the user about backup before updating.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'slug' => [
                            'type' => 'string',
                            'description' => 'The plugin directory slug (e.g., yoast-seo, woocommerce)'
                        ]
                    ],
                    'required' => ['slug']
                ]
            ],
            [
                'name' => 'optimizeDatabase',

                'category' => 'database',

                'keywords' => ['exact' => ['database', 'db', 'base de datos', 'revisiones', 'revisions', 'transients', 'rendimiento', 'performance'], 'stem' => ['optimiz', 'limpi']],
                'description' => 'Cleans up the WordPress database by removing post revisions, spam comments, orphaned postmeta, and expired transients. Processes in safe batches of 1000 rows to prevent timeouts on large databases. Returns actual space recovered. If interrupted by time limits, reports partial progress — the user can run it again to continue. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission. This is destructive and irreversible.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [],
                ]
            ],
            [
                'name' => 'createContent',

                'category' => 'content',

                'keywords' => ['exact' => ['post', 'page', 'content', 'article', 'blog', 'meta', 'trash', 'entrada', 'pagina', 'contenido', 'articulo', 'cuerpo', 'extracto', 'papelera', 'borrador', 'draft', 'title', 'titulo'], 'stem' => ['crea', 'public', 'añad', 'escrib']],
                'description' => 'Creates a new post or page in WordPress. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'title' => [
                            'type' => 'string',
                            'description' => 'The post title'
                        ],
                        'content' => [
                            'type' => 'string',
                            'description' => 'The post body content (HTML supported)'
                        ],
                        'type' => [
                            'type' => 'string',
                            'description' => 'Post type: post or page',
                            'enum' => ['post', 'page']
                        ],
                        'status' => [
                            'type' => 'string',
                            'description' => 'Post status',
                            'enum' => ['draft', 'publish']
                        ],
                        'categories' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of category term IDs to assign (optional)'],
                        'tags' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Array of tag names or IDs to assign (optional)'],
                        'featured_image' => ['type' => 'integer', 'description' => 'Attachment ID to set as featured image (optional)']
                    ],
                    'required' => ['title']
                ]
            ],
            [
                'name' => 'updateOption',

                'category' => 'system',

                'keywords' => ['exact' => ['options', 'settings', 'configuration', 'opciones', 'ajustes', 'configuracion', 'sistema', 'system', 'option', 'opcion', 'coming soon', 'maintenance mode', 'modo mantenimiento', 'store', 'tienda'], 'stem' => ['configur', 'ajust', 'cambi', 'modific', 'updat', 'chang']],
                'description' => 'Updates a WordPress database option value (wp_options table). YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission. '
                    . 'IMPORTANT: This tool CANNOT modify PHP constants defined in wp-config.php (WP_DEBUG, WP_DEBUG_LOG, '
                    . 'WP_DEBUG_DISPLAY, DISALLOW_FILE_EDIT, etc). If the user asks to change those, explain they must '
                    . 'edit wp-config.php manually via FTP/cPanel/SSH. Some options are blocked for security.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'key' => [
                            'type' => 'string',
                            'description' => 'The option key to update (must be a wp_options key, NOT a PHP constant)'
                        ],
                        'value' => [
                            'type' => 'string',
                            'description' => 'The new value to set'
                        ]
                    ],
                    'required' => ['key', 'value']
                ]
            ],
            [
                'name' => 'purgeCache',

                'category' => 'cache',

                'keywords' => ['exact' => ['cache', 'caché', 'rocket', 'w3tc', 'litespeed', 'rendimiento', 'velocidad', 'speed'], 'stem' => ['purg', 'limpi', 'vaci']],
                'description' => 'Purges all cache from detected caching plugins (WP Rocket, W3 Total Cache, etc). YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [],
                ]
            ],
            [
                'name' => 'togglePlugin',

                'category' => 'plugins',

                'keywords' => ['exact' => ['plugin', 'plugins', 'complemento', 'complementos', 'extension', 'extensiones'], 'stem' => ['activ', 'desactiv', 'toggl']],
                'description' => 'Activates or deactivates a WordPress plugin. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'slug' => ['type' => 'string', 'description' => 'Plugin directory slug (e.g., jetpack, woocommerce)'],
                        'action' => ['type' => 'string', 'enum' => ['activate', 'deactivate'], 'description' => 'Whether to activate or deactivate']
                    ],
                    'required' => ['slug', 'action']
                ]
            ],
            [
                'name' => 'installPlugin',

                'category' => 'plugins',

                'keywords' => ['exact' => ['plugin', 'plugins', 'complemento', 'complementos', 'extension', 'extensiones'], 'stem' => ['instal', 'subi']],
                'description' => 'Installs a plugin from the WordPress.org repository. Optionally activates it after installation. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'slug' => ['type' => 'string', 'description' => 'Plugin slug from WordPress.org (e.g., wordpress-seo, woocommerce, contact-form-7)'],
                        'activate' => ['type' => 'boolean', 'description' => 'Whether to activate the plugin after installing (default: true)']
                    ],
                    'required' => ['slug']
                ]
            ],
            [
                'name' => 'bulkUpdatePlugins',

                'category' => 'plugins',

                'keywords' => ['exact' => ['plugin', 'plugins', 'complemento', 'complementos', 'extension', 'extensiones'], 'stem' => ['actualiz', 'updat']],
                'description' => 'Updates multiple plugins at once. If no slugs provided, updates ALL plugins with pending updates. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'slugs' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Plugin slugs to update. Empty array = update all with pending updates.']
                    ],
                ]
            ],
            [
                'name' => 'editContent',

                'category' => 'content',

                'keywords' => ['exact' => ['post', 'page', 'content', 'article', 'blog', 'meta', 'trash', 'entrada', 'pagina', 'contenido', 'articulo', 'cuerpo', 'extracto', 'papelera', 'borrador', 'draft', 'title', 'titulo'], 'stem' => ['edit', 'modific', 'cambi', 'actualiz', 'updat', 'chang']],
                'description' => 'Edits an existing post or page. Only modifies the fields explicitly provided. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'post_id' => ['type' => 'integer', 'description' => 'The ID of the post/page to edit'],
                        'title'   => ['type' => 'string', 'description' => 'New title (optional)'],
                        'content' => ['type' => 'string', 'description' => 'New body content in HTML (optional)'],
                        'status'  => ['type' => 'string', 'enum' => ['draft', 'publish', 'pending'], 'description' => 'New status (optional)'],
                        'excerpt' => ['type' => 'string', 'description' => 'New excerpt (optional)'],
                        'slug'    => ['type' => 'string', 'description' => 'New URL slug (optional)'],
                        'categories' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of category term IDs to assign. Replaces existing categories (optional)'],
                        'tags' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Array of tag names or IDs to assign. Replaces existing tags (optional)'],
                        'featured_image' => ['type' => 'integer', 'description' => 'Attachment ID for featured image. Use 0 to remove (optional)']
                    ],
                    'required' => ['post_id']
                ]
            ],
            [
                'name' => 'deleteContent',

                'category' => 'content',

                'keywords' => ['exact' => ['post', 'page', 'content', 'article', 'blog', 'meta', 'trash', 'entrada', 'pagina', 'contenido', 'articulo', 'cuerpo', 'extracto', 'papelera', 'borrador', 'draft', 'title', 'titulo'], 'stem' => ['borr', 'elimin', 'delet']],
                'description' => 'Moves posts/pages to trash or deletes permanently. Default is trash (recoverable). If permanent=true, warn user about backup. Max 50 posts per call. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'post_ids'  => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of post/page IDs to delete'],
                        'permanent' => ['type' => 'boolean', 'description' => 'If true, permanently deletes. Default false (trash).']
                    ],
                    'required' => ['post_ids']
                ]
            ],
            [
                'name' => 'manageUsers',

                'category' => 'users',

                'keywords' => ['exact' => ['user', 'users', 'author', 'subscriber', 'editor', 'administrator', 'role', 'profile', 'account', 'email', 'password', 'usuario', 'usuarios', 'autor', 'suscriptor', 'administrador', 'rol', 'perfil', 'cuenta', 'correo', 'contraseña'], 'stem' => ['crea', 'borr', 'elimin', 'actualiz', 'modific']],
                'description' => 'Creates a new user, updates a user role, or deletes a user. For delete, reassign_to is mandatory to prevent content loss. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'action'      => ['type' => 'string', 'enum' => ['create', 'update_role', 'delete'], 'description' => 'The user management action'],
                        'user_id'     => ['type' => 'integer', 'description' => 'User ID (for update_role and delete)'],
                        'username'    => ['type' => 'string', 'description' => 'Username (for create)'],
                        'email'       => ['type' => 'string', 'description' => 'Email address (for create)'],
                        'role'        => ['type' => 'string', 'description' => 'Role slug. Standard: subscriber, contributor, author, editor, administrator. Also accepts any custom role slug created via manageRoles (e.g., lolacore_demo). Use getRoles to list all available roles.'],
                        'reassign_to' => ['type' => 'integer', 'description' => 'User ID to reassign content to (mandatory for delete)']
                    ],
                    'required' => ['action']
                ]
            ],
            [
                'name' => 'manageRoles',

                'category' => 'users',

                'keywords' => ['exact' => ['role', 'roles', 'capability', 'capabilities', 'permission', 'permissions', 'access', 'rol', 'roles', 'capacidad', 'capacidades', 'permiso', 'permisos', 'acceso'], 'stem' => ['crea', 'borr', 'elimin', 'modific', 'añad', 'quit']],
                'description' => 'Creates, deletes, or modifies WordPress roles and capabilities. Use getRoles first to inspect existing roles. IMPORTANT: Built-in roles (administrator, editor, author, contributor, subscriber) cannot be deleted. The administrator role\'s manage_options capability cannot be removed. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'action' => ['type' => 'string', 'enum' => ['create', 'delete', 'add_caps', 'remove_caps'], 'description' => 'The role management action'],
                        'role_slug' => ['type' => 'string', 'description' => 'The role slug to create, delete, or modify (e.g., lolacore_demo). For create, use lowercase with underscores.'],
                        'display_name' => ['type' => 'string', 'description' => 'Human-readable name for the role (required for create, e.g., "LolaCore Demo")'],
                        'clone_from' => ['type' => 'string', 'description' => 'Clone capabilities from an existing role when creating (optional, e.g., "editor"). If omitted, the new role starts with zero capabilities.'],
                        'capabilities' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Array of capability names to add or remove (required for add_caps/remove_caps, e.g., ["manage_options", "edit_posts"])']
                    ],
                    'required' => ['action', 'role_slug']
                ]
            ],
            [
                'name' => 'cleanupCronJobs',

                'category' => 'system',

                'keywords' => ['exact' => ['cron', 'cronjobs', 'tareas'], 'stem' => ['limpi']],
                'description' => 'Audits or cleans up WordPress cron jobs. "audit" lists all cron jobs and identifies orphaned ones. "clean_orphaned" removes cron hooks with no registered callback (leftover from uninstalled plugins). REQUIRES USER CONFIRMATION for clean_orphaned.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'action' => ['type' => 'string', 'enum' => ['audit', 'clean_orphaned'], 'description' => 'audit = list all, clean_orphaned = remove orphaned hooks'],
                        'hooks'  => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Optional: specific hooks to clean (for selective cleanup)']
                    ],
                    'required' => ['action']
                ]
            ],
            [
                'name' => 'updatePostMeta',

                'category' => 'content',

                'keywords' => ['exact' => ['post', 'page', 'content', 'article', 'blog', 'meta', 'trash', 'entrada', 'pagina', 'contenido', 'articulo', 'cuerpo', 'extracto', 'papelera', 'borrador', 'draft', 'title', 'titulo', 'custom field', 'campo personalizado'], 'stem' => ['actualiz', 'modific']],
                'description' => 'Updates a meta value on a specific post, page, or product. Use this to configure plugin settings per-post: Yoast SEO meta descriptions (_yoast_wpseo_metadesc), WooCommerce prices (_price, _regular_price), ACF fields, etc. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'post_id'  => ['type' => 'integer', 'description' => 'The post/page/product ID'],
                        'meta_key' => ['type' => 'string', 'description' => 'The meta key to update (e.g., _yoast_wpseo_metadesc, _price, rank_math_description)'],
                        'value'    => ['type' => 'string', 'description' => 'The new value to set']
                    ],
                    'required' => ['post_id', 'meta_key', 'value']
                ]
            ],
            [
                'name' => 'manageTaxonomies',

                'category' => 'taxonomies',

                'keywords' => ['exact' => ['category', 'tag', 'taxonomy', 'term', 'hierarchy', 'categoria', 'categorias', 'etiqueta', 'etiquetas', 'taxonomia', 'termino', 'jerarquia'], 'stem' => ['crea', 'borr', 'elimin', 'edit', 'modific']],
                'description' => 'Creates, updates, or deletes taxonomy terms (categories, tags). YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'action' => ['type' => 'string', 'enum' => ['create', 'update', 'delete'], 'description' => 'The operation to perform'],
                        'taxonomy' => ['type' => 'string', 'description' => 'The taxonomy slug (e.g., category, post_tag)'],
                        'term_id' => ['type' => 'integer', 'description' => 'The term ID (required for update and delete)'],
                        'name' => ['type' => 'string', 'description' => 'The term name (required for create, optional for update)'],
                        'slug' => ['type' => 'string', 'description' => 'The URL slug (optional)'],
                        'parent' => ['type' => 'integer', 'description' => 'Parent term ID for hierarchical taxonomies (optional)']
                    ],
                    'required' => ['action', 'taxonomy']
                ]
            ],
            [
                'name' => 'manageComments',

                'category' => 'comments',

                'keywords' => ['exact' => ['comment', 'comments', 'spam', 'trash', 'discussion', 'moderation', 'feedback', 'comentario', 'comentarios', 'papelera', 'discusion', 'moderacion', 'opinion'], 'stem' => ['aprob', 'borr', 'elimin', 'respond']],
                'description' => 'Updates the status of comments (approve, spam, trash) or deletes them permanently. Can operate on multiple comments at once. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'comment_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of comment IDs to manage'],
                        'action' => ['type' => 'string', 'enum' => ['approve', 'hold', 'spam', 'trash', 'delete'], 'description' => 'The status to set, or "delete" to permanently remove']
                    ],
                    'required' => ['comment_ids', 'action']
                ]
            ],
            [
                'name' => 'uploadMedia',

                'category' => 'media',

                'keywords' => ['exact' => ['media', 'image', 'images', 'attachment', 'file', 'photo', 'video', 'upload', 'sideload', 'medios', 'imagen', 'imagenes', 'adjunto', 'archivo', 'archivos', 'foto', 'galeria', 'biblioteca', 'subir', 'importar'], 'stem' => ['subi', 'import', 'upload']],
                'description' => 'Uploads (sideloads) a media file from an external URL into the WordPress Media Library. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'url'      => ['type' => 'string', 'description' => 'The external URL of the file to download and upload (required)'],
                        'title'    => ['type' => 'string', 'description' => 'The title for the attachment (optional)'],
                        'alt_text' => ['type' => 'string', 'description' => 'The alt text for the attachment (optional)'],
                        'caption'  => ['type' => 'string', 'description' => 'The caption for the attachment (optional)']
                    ],
                    'required' => ['url']
                ]
            ],
            [
                'name' => 'deleteMedia',

                'category' => 'media',

                'keywords' => ['exact' => ['media', 'image', 'images', 'attachment', 'file', 'photo', 'video', 'pdf', 'library', 'gallery', 'picture', 'medios', 'imagen', 'imagenes', 'adjunto', 'archivo', 'archivos', 'foto', 'galeria', 'biblioteca', 'documento'], 'stem' => ['borr', 'elimin', 'delet']],
                'description' => 'Permanently deletes an attachment from the WordPress Media Library, removing the physical file from the server. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'attachment_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of attachment post IDs to delete']
                    ],
                    'required' => ['attachment_ids']
                ]
            ],
            [
                'name' => 'manageMenus',

                'category' => 'menus',

                'keywords' => ['exact' => ['menu', 'menus', 'navigation', 'link', 'subitem', 'header', 'footer', 'location', 'navbar', 'navegacion', 'enlace', 'subelemento', 'cabecera', 'pie', 'ubicacion', 'barra'], 'stem' => ['crea', 'modific', 'añad', 'borr']],
                'description' => 'Creates a new navigation menu, adds items to an existing menu, or removes items. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'action' => ['type' => 'string', 'enum' => ['create_menu', 'add_item', 'remove_item'], 'description' => 'The menu management action'],
                        'menu_name' => ['type' => 'string', 'description' => 'Name for the new menu (required for create_menu)'],
                        'menu_id' => ['type' => 'integer', 'description' => 'Menu ID to modify (required for add_item, remove_item)'],
                        'item_title' => ['type' => 'string', 'description' => 'Title of the menu item to add'],
                        'item_url' => ['type' => 'string', 'description' => 'URL for a custom link menu item'],
                        'item_object_id' => ['type' => 'integer', 'description' => 'Post/page/category ID to link to'],
                        'item_object' => ['type' => 'string', 'enum' => ['page', 'post', 'category', 'custom'], 'description' => 'Type of object to link (default: custom)'],
                        'item_id' => ['type' => 'integer', 'description' => 'Menu item ID to remove (required for remove_item)'],
                        'parent_item_id' => ['type' => 'integer', 'description' => 'Parent menu item ID for creating sub-items (optional)']
                    ],
                    'required' => ['action']
                ]
            ],
            [
                'name' => 'switchTheme',

                'category' => 'themes',

                'keywords' => ['exact' => ['theme', 'themes', 'tema', 'temas', 'plantilla', 'plantillas', 'apariencia', 'appearance', 'design', 'diseño'], 'stem' => ['cambi', 'switch', 'activ']],
                'description' => 'Changes the active WordPress theme. WARNING: Switching themes may cause loss of theme-specific widgets, menus, and customizer settings. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'stylesheet' => ['type' => 'string', 'description' => 'The theme directory name (stylesheet) to activate (e.g., twentytwentyfive, astra)']
                    ],
                    'required' => ['stylesheet']
                ]
            ],
            [
                'name' => 'installTheme',

                'category' => 'themes',

                'keywords' => ['exact' => ['theme', 'themes', 'tema', 'temas', 'plantilla', 'plantillas', 'apariencia', 'appearance'], 'stem' => ['instal', 'descarg', 'subi']],
                'description' => 'Installs a theme from the WordPress.org repository. The theme is installed but NOT activated automatically — use switchTheme to activate it. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'slug' => ['type' => 'string', 'description' => 'Theme slug from WordPress.org (e.g., twentytwentyfive, astra, hello-elementor)']
                    ],
                    'required' => ['slug']
                ]
            ],
            [
                'name' => 'updateTheme',

                'category' => 'themes',

                'keywords' => ['exact' => ['theme', 'themes', 'tema', 'temas', 'plantilla', 'plantillas', 'apariencia', 'appearance'], 'stem' => ['actualiz', 'updat']],
                'description' => 'Updates a single installed theme to its latest version from WordPress.org. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'stylesheet' => ['type' => 'string', 'description' => 'The theme directory name (stylesheet) to update']
                    ],
                    'required' => ['stylesheet']
                ]
            ],
            [
                'name' => 'bulkUpdateThemes',

                'category' => 'themes',

                'keywords' => ['exact' => ['theme', 'themes', 'tema', 'temas', 'plantilla', 'plantillas', 'apariencia', 'appearance'], 'stem' => ['actualiz', 'updat']],
                'description' => 'Updates multiple themes at once. Pass an array of stylesheet names. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'stylesheets' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Theme stylesheets (directory names) to update']
                    ],
                    'required' => ['stylesheets']
                ]
            ],
            [
                'name' => 'deleteTheme',

                'category' => 'themes',

                'keywords' => ['exact' => ['theme', 'themes', 'tema', 'temas', 'plantilla', 'plantillas', 'apariencia', 'appearance'], 'stem' => ['elimin', 'borr', 'delet', 'remov', 'uninstal']],
                'description' => 'Permanently deletes an installed theme from the site. The active theme and the parent of an active child theme cannot be deleted. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'stylesheet' => ['type' => 'string', 'description' => 'The theme directory name (stylesheet) to delete']
                    ],
                    'required' => ['stylesheet']
                ]
            ],
            // --- WooCommerce Write Tools ---
            [
                'name' => 'wcUpdateProduct',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => ['actualiz', 'modific', 'cambi', 'edit', 'updat', 'chang', 'set']],
                'description' => 'Updates a WooCommerce product: price, stock, name, description, status, SKU, categories, etc. Only fields provided will be changed. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'product_id' => ['type' => 'integer', 'description' => 'The WooCommerce product ID'],
                        'name' => ['type' => 'string', 'description' => 'New product name (optional)'],
                        'regular_price' => ['type' => 'string', 'description' => 'New regular price (optional)'],
                        'sale_price' => ['type' => 'string', 'description' => 'New sale price (optional, empty string to remove sale)'],
                        'stock_quantity' => ['type' => 'integer', 'description' => 'New stock quantity (optional)'],
                        'stock_status' => ['type' => 'string', 'enum' => ['instock', 'outofstock', 'onbackorder'], 'description' => 'Stock status (optional)'],
                        'status' => ['type' => 'string', 'enum' => ['publish', 'draft', 'pending', 'private'], 'description' => 'Product visibility status (optional)'],
                        'sku' => ['type' => 'string', 'description' => 'Product SKU (optional)'],
                        'description' => ['type' => 'string', 'description' => 'Full product description in HTML (optional)'],
                        'short_description' => ['type' => 'string', 'description' => 'Short product description (optional)'],
                        'categories' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of product category term IDs to assign. Replaces all current categories. Use wcGetCategories to look up IDs first.'],
                        'tags' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of product tag term IDs to assign (optional)'],
                        'weight' => ['type' => 'string', 'description' => 'Product weight in the store\'s configured unit (optional)'],
                        'manage_stock' => ['type' => 'boolean', 'description' => 'Enable stock management for this product (optional)']
                    ],
                    'required' => ['product_id']
                ]
            ],
            [
                'name' => 'wcUpdateOrderStatus',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => ['actualiz', 'modific', 'complet', 'cancel', 'cambi', 'chang', 'updat']],
                'description' => 'Changes the status of a WooCommerce order (e.g., processing → completed). Optionally adds an order note. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'order_id' => ['type' => 'integer', 'description' => 'The WooCommerce order ID'],
                        'status' => ['type' => 'string', 'enum' => ['pending', 'processing', 'on-hold', 'completed', 'cancelled', 'refunded', 'failed'], 'description' => 'New order status'],
                        'note' => ['type' => 'string', 'description' => 'Optional note to add to the order (visible to customer if customer_note is true)'],
                        'customer_note' => ['type' => 'boolean', 'description' => 'If true, the note is sent to the customer via email (default: false)']
                    ],
                    'required' => ['order_id', 'status']
                ]
            ],
            [
                'name' => 'wcCreateProduct',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => ['crea', 'añad']],
                'description' => 'Creates a new WooCommerce product (simple). Sets name, prices, stock, SKU, description, and status. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'name' => ['type' => 'string', 'description' => 'Product name/title'],
                        'regular_price' => ['type' => 'string', 'description' => 'Regular price (required)'],
                        'sale_price' => ['type' => 'string', 'description' => 'Sale price (optional)'],
                        'description' => ['type' => 'string', 'description' => 'Full product description in HTML (optional)'],
                        'short_description' => ['type' => 'string', 'description' => 'Short product description (optional)'],
                        'sku' => ['type' => 'string', 'description' => 'Product SKU (optional)'],
                        'stock_quantity' => ['type' => 'integer', 'description' => 'Initial stock quantity (optional)'],
                        'status' => ['type' => 'string', 'enum' => ['publish', 'draft'], 'description' => 'Product status (default: draft)'],
                        'categories' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of product category IDs (optional)']
                    ],
                    'required' => ['name', 'regular_price']
                ]
            ],
            [
                'name' => 'wcManageCategories',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => ['crea', 'borr', 'modific']],
                'description' => 'Creates, updates, or deletes WooCommerce product categories. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'action' => ['type' => 'string', 'enum' => ['create', 'update', 'delete'], 'description' => 'The operation to perform'],
                        'term_id' => ['type' => 'integer', 'description' => 'Category term ID (required for update/delete)'],
                        'name' => ['type' => 'string', 'description' => 'Category name (required for create)'],
                        'slug' => ['type' => 'string', 'description' => 'Category URL slug (optional)'],
                        'parent' => ['type' => 'integer', 'description' => 'Parent category ID (optional)'],
                        'description' => ['type' => 'string', 'description' => 'Category description (optional)']
                    ],
                    'required' => ['action']
                ]
            ],
            [
                'name' => 'wcManageCoupons',

                'category' => 'woocommerce',

                'keywords' => ['exact' => ['woocommerce', 'woo', 'store', 'shop', 'product', 'ecommerce', 'order', 'customer', 'sale', 'stock', 'price', 'sku', 'coupon', 'discount', 'report', 'revenue', 'checkout', 'cart', 'tienda', 'producto', 'productos', 'comercio', 'pedido', 'cliente', 'venta', 'inventario', 'precio', 'cupon', 'descuento', 'informe', 'ingresos', 'facturacion', 'carrito'], 'stem' => ['crea', 'borr', 'modific']],
                'description' => 'Creates, updates, or deletes WooCommerce discount coupons. YOU MUST call this tool to execute this action. The system will automatically pause and ask the user for permission.',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'action' => ['type' => 'string', 'enum' => ['create', 'update', 'delete', 'list', 'get'], 'description' => 'The operation'],
                        'coupon_id' => ['type' => 'integer', 'description' => 'Coupon ID (required for update/delete/get)'],
                        'code' => ['type' => 'string', 'description' => 'Coupon code (required for create)'],
                        'limit' => ['type' => 'integer', 'description' => 'Number of coupons to list (optional, default 10)'],
                        'page' => ['type' => 'integer', 'description' => 'Page number for list (optional, default 1)'],
                        'discount_type' => ['type' => 'string', 'enum' => ['percent', 'fixed_cart', 'fixed_product'], 'description' => 'Type of discount'],
                        'amount' => ['type' => 'string', 'description' => 'Discount amount (e.g., "10" for 10% or $10)'],
                        'usage_limit' => ['type' => 'integer', 'description' => 'Total usage limit (optional)'],
                        'usage_limit_per_user' => ['type' => 'integer', 'description' => 'Usage limit per customer (optional)'],
                        'expiry_date' => ['type' => 'string', 'description' => 'Expiry date in YYYY-MM-DD format (optional)'],
                        'minimum_amount' => ['type' => 'string', 'description' => 'Minimum cart total required (optional)'],
                        'maximum_amount' => ['type' => 'string', 'description' => 'Maximum cart total allowed (optional)'],
                        'individual_use' => ['type' => 'boolean', 'description' => 'If true, cannot be combined with other coupons (optional)'],
                        'free_shipping' => ['type' => 'boolean', 'description' => 'If true, grants free shipping when coupon is applied (optional)'],
                        'product_categories' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of product category IDs the coupon applies to. Use wcGetCategories to look up IDs.'],
                        'excluded_product_categories' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of product category IDs excluded from the coupon (optional)'],
                        'product_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of specific product IDs the coupon applies to (optional)'],
                        'excluded_product_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Array of product IDs excluded from the coupon (optional)'],
                        'email_restrictions' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Array of email addresses allowed to use this coupon (optional)']
                    ],
                    'required' => ['action']
                ]
            ]
        ];
    }

    /**
     * Converts internal tool definitions to the format required by each provider.
     * Also normalizes empty 'properties' arrays to objects for correct JSON serialization.
     *
     * Both formats preserve 'category' and 'keywords' metadata required by the
     * intent router (WPAIClientAdapter::buildCategoryToolMap).
     *
     * @param array  $tools  Internal tool definitions.
     * @param string $format 'openai' or 'anthropic'.
     * @return array
     */
    private function formatTools(array $tools, string $format): array
    {
        if ($format === 'anthropic') {
            $result = [];
            foreach ($tools as $tool) {
                $formatted = [
                    'name'         => $tool['name'],
                    'description'  => $tool['description'],
                    'input_schema' => $this->normalizeParameters($tool['parameters']),
                ];
                // Preserve routing metadata for the intent router
                if (isset($tool['category'])) {
                    $formatted['category'] = $tool['category'];
                }
                if (isset($tool['keywords'])) {
                    $formatted['keywords'] = $tool['keywords'];
                }
                $result[] = $formatted;
            }
            return $result;
        }

        // Default OpenAI / DeepSeek format
        $result = [];
        foreach ($tools as $tool) {
            $category = $tool['category'] ?? null;
            $keywords = $tool['keywords'] ?? null;
            $tool['parameters'] = $this->normalizeParameters($tool['parameters']);
            $entry = [
                'type'     => 'function',
                'function' => $tool,
            ];
            if ($category !== null) {
                $entry['category'] = $category;
            }
            if ($keywords !== null) {
                $entry['keywords'] = $keywords;
            }
            $result[] = $entry;
        }

        return $result;
    }

    /**
     * Ensure empty 'properties' is serialized as a JSON object ({}) not a JSON array ([]).
     * PHP's json_encode outputs [] for empty arrays, but OpenAI/DeepSeek APIs require {}.
     *
     * @param array $params The parameters definition.
     * @return array The normalized parameters.
     */
    private function normalizeParameters(array $params): array
    {
        if (isset($params['properties']) && is_array($params['properties']) && empty($params['properties'])) {
            $params['properties'] = (object) [];
        }
        return $params;
    }
}
