<?php
/**
 * Text, SEO and image generation orchestrator.
 *
 * @package CatalogPilot\AiStudio
 */

namespace CatalogPilot\AiStudio;

use CatalogPilot\AiStudio\Providers;

defined( 'ABSPATH' ) || exit;

final class Generator {

	/**
	 * Wrap a provider->complete() call with transient cache.
	 * Identical prompt + provider + model returns cached result (TTL-bounded).
	 */
	private static function cached_complete( $provider, $provider_id, $model, $system, $user ) {
		$cache_key = Cache::key_for(
			'complete',
			array(
				'provider' => $provider_id,
				'model'    => $model,
				'system'   => $system,
				'user'     => $user,
			)
		);
		$cached = Cache::get( $cache_key );
		if ( null !== $cached && is_array( $cached ) ) {
			$cached['cached'] = true;
			return $cached;
		}

		if ( Gateway::is_available() || ( Gateway::is_connected() && ! $provider->has_key() ) ) {
			$gw = Gateway::generate( array(
				'provider'   => $provider_id,
				'model'      => $model,
				'messages'   => array(
					array( 'role' => 'system', 'content' => $system ),
					array( 'role' => 'user',   'content' => $user ),
				),
				'max_tokens' => 1024,
			) );
			if ( ! is_wp_error( $gw ) ) {
				$result = array(
					'content' => (string) ( $gw['content'] ?? '' ),
					'usage'   => (array) ( $gw['usage'] ?? array() ),
					'model'   => $model,
					'via'     => 'gateway',
				);
				Cache::set( $cache_key, $result );
				delete_transient( 'cpais_gateway_quota' );
				return $result;
			}
			$code = $gw->get_error_code();
			if ( in_array( $code, array( 'cpais_quota_exceeded', 'cpais_rate_limited', 'cpais_no_install', 'cpais_no_signature', 'cpais_bad_signature', 'cpais_gateway_not_available_on_free' ), true ) ) {
				return $gw;
			}
		}

		$result = $provider->complete( $system, $user );
		if ( ! is_wp_error( $result ) ) {
			Cache::set( $cache_key, $result );
		}
		return $result;
	}

	private static function cached_vision( $provider, $provider_id, $model, $system, $user, array $image_urls ) {
		$cache_key = Cache::key_for(
			'vision',
			array(
				'provider' => $provider_id,
				'model'    => $model,
				'system'   => $system,
				'user'     => $user,
				'images'   => $image_urls,
			)
		);
		$cached = Cache::get( $cache_key );
		if ( null !== $cached && is_array( $cached ) ) {
			$cached['cached'] = true;
			return $cached;
		}
		$result = $provider->complete_vision( $system, $user, $image_urls );
		if ( ! is_wp_error( $result ) ) {
			Cache::set( $cache_key, $result );
		}
		return $result;
	}

	public static function language_label( $code ) {
		$map = array(
			'en' => 'English',
			'tr' => 'Turkish',
			'de' => 'German',
			'es' => 'Spanish',
			'fr' => 'French',
			'it' => 'Italian',
			'ar' => 'Arabic',
			'ru' => 'Russian',
		);
		return $map[ $code ] ?? 'English';
	}

	public static function product_context( $product ) {
		$attributes = array();
		foreach ( $product->get_attributes() as $attr ) {
			if ( $attr instanceof \WC_Product_Attribute ) {
				$attributes[ $attr->get_name() ] = $attr->is_taxonomy()
					? wc_get_product_terms( $product->get_id(), $attr->get_name(), array( 'fields' => 'names' ) )
					: $attr->get_options();
			}
		}

		$categories = wp_get_post_terms( $product->get_id(), 'product_cat', array( 'fields' => 'names' ) );

		return array(
			'title'         => $product->get_name(),
			'sku'           => $product->get_sku(),
			'price'         => $product->get_price(),
			'categories'    => is_wp_error( $categories ) ? array() : $categories,
			'current_short' => wp_strip_all_tags( $product->get_short_description() ),
			'current_long'  => wp_strip_all_tags( $product->get_description() ),
			'attributes'    => $attributes,
		);
	}

	public static function text_prompt( $product, $tone, $length, $language, array $parts ) {
		$length_map = array(
			'short'  => array( 'short_sentences' => 2, 'long_words' => 250 ),
			'medium' => array( 'short_sentences' => 3, 'long_words' => 500 ),
			'long'   => array( 'short_sentences' => 5, 'long_words' => 900 ),
		);
		$len = $length_map[ $length ] ?? $length_map['medium'];

		$schema_parts = array();
		if ( in_array( 'short_description', $parts, true ) ) {
			$schema_parts[] = sprintf( '"short_description": "string, %d concise sentence(s)"', $len['short_sentences'] );
		}
		if ( in_array( 'long_description', $parts, true ) ) {
			$schema_parts[] = sprintf( '"long_description": "string, HTML allowed, approx %d words, use <p> and <ul>/<li>"', $len['long_words'] );
		}
		if ( in_array( 'alternative_titles', $parts, true ) ) {
			$schema_parts[] = '"alternative_titles": ["string", "string", "string"]';
		}
		if ( in_array( 'features', $parts, true ) ) {
			$schema_parts[] = '"features": ["string bullet point", "..."]';
		}

		$system = sprintf(
			'You are a WooCommerce copywriter. Write in %s with a %s tone. Return ONLY a valid JSON object matching this schema: { %s }. Do not wrap the JSON in code fences. Do not include any commentary.',
			self::language_label( $language ),
			$tone,
			implode( ', ', $schema_parts )
		);

		$user = "Product context:\n" . wp_json_encode(
			self::product_context( $product ),
			JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
		);

		return array( 'system' => $system, 'user' => $user );
	}

	public static function seo_prompt( $product, $tone, $language ) {
		$schema = '{ "meta_title": "string, max 60 chars, include primary keyword", "meta_description": "string, 140-160 chars, include primary keyword and CTA", "focus_keyword": "string, 2-4 words", "slug": "string, kebab-case, no stopwords, ASCII only", "og_title": "string, max 60 chars, engaging", "og_description": "string, max 110 chars, social friendly", "twitter_title": "string, max 60 chars", "twitter_description": "string, max 110 chars" }';

		$system = sprintf(
			'You are an SEO specialist for WooCommerce stores. Write in %s with a %s tone. Return ONLY a valid JSON object matching this schema: %s. Do not wrap in code fences. Do not include commentary.',
			self::language_label( $language ),
			$tone,
			$schema
		);

		$user = "Product context:\n" . wp_json_encode(
			self::product_context( $product ),
			JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
		);

		return array( 'system' => $system, 'user' => $user );
	}

	/**
	 * Variant style descriptors used by `run_image_variants`.
	 * Returns an ordered array { key => human label + prompt suffix }.
	 *
	 * @return array<string, array{label: string, suffix: string}>
	 */
	public static function image_variant_definitions() {
		return array(
			'package' => array(
				'label'  => __( 'Package shot', 'catalogpilot-ai-studio-for-woocommerce' ),
				'suffix' => 'professional product package shot, clean white seamless background, even soft lighting, e-commerce catalog style, sharp focus on the product, no human, no text, no watermark',
			),
			'lifestyle' => array(
				'label'  => __( 'Lifestyle', 'catalogpilot-ai-studio-for-woocommerce' ),
				'suffix' => 'lifestyle photo of the product being used in a natural everyday environment, warm ambient light, shallow depth of field, candid composition, no text, no watermark',
			),
			'closeup' => array(
				'label'  => __( 'Close-up', 'catalogpilot-ai-studio-for-woocommerce' ),
				'suffix' => 'extreme close-up macro detail of the product surface, showing texture and material quality, dramatic side lighting, high resolution, no text, no watermark',
			),
			'hero' => array(
				'label'  => __( 'Hero shot', 'catalogpilot-ai-studio-for-woocommerce' ),
				'suffix' => 'cinematic hero marketing shot of the product, premium dramatic lighting, dark moody background, slight rim light, magazine-quality composition, no text, no watermark',
			),
		);
	}

	public static function image_prompt_for_product( $product, $style = 'studio' ) {
		$context   = self::product_context( $product );
		$style_map = array(
			'studio'         => 'professional studio product photography, white seamless background, soft diffused lighting, sharp focus, commercial quality',
			'lifestyle'      => 'lifestyle product photography, natural setting, warm ambient light, shallow depth of field, candid',
			'flatlay'        => 'flatlay product photography, top-down view, neutral background, soft shadows, minimalist composition',
			'artistic'       => 'artistic product render, creative composition, dramatic lighting, premium mood',
			'transparent'    => 'product cutout on transparent background, centered, clean edges, no shadow',
			'hero_shot'      => 'hero product shot for e-commerce banner, dramatic lighting from three-quarter angle, glossy reflective surface, cinematic quality, wide composition, premium look',
			'feature_detail' => 'extreme close-up product feature detail, macro photography, sharp textures, shallow depth of field, highlights the material and craftsmanship',
		);
		$style_desc = $style_map[ $style ] ?? $style_map['studio'];

		$pieces = array();
		$pieces[] = $context['title'];
		if ( ! empty( $context['categories'] ) ) {
			$pieces[] = 'category: ' . implode( ', ', $context['categories'] );
		}
		if ( ! empty( $context['current_short'] ) ) {
			$pieces[] = 'details: ' . wp_trim_words( $context['current_short'], 25 );
		}
		$pieces[] = $style_desc;
		$pieces[] = 'no text, no watermark';

		return implode( '. ', array_filter( $pieces ) );
	}

	public static function translate_prompt( $product, $source, $target, array $parts, array $current = array() ) {
		$schema_parts = array();
		$title        = ! empty( $current['title'] ) ? (string) $current['title'] : $product->get_name();
		$context      = array( 'title' => $title );

		if ( in_array( 'title', $parts, true ) ) {
			$schema_parts[] = '"title": "string"';
		}
		if ( in_array( 'short_description', $parts, true ) ) {
			$schema_parts[] = '"short_description": "string"';
			$short = ! empty( $current['short_description'] )
				? (string) $current['short_description']
				: $product->get_short_description();
			$context['short_description'] = wp_strip_all_tags( $short );
		}
		if ( in_array( 'long_description', $parts, true ) ) {
			$schema_parts[] = '"long_description": "string, preserve HTML tags and structure"';
			$long = ! empty( $current['long_description'] )
				? (string) $current['long_description']
				: $product->get_description();
			$context['long_description'] = $long;
		}

		$source_label = '' === $source ? 'the detected source language' : self::language_label( $source );

		$system = sprintf(
			'You are a professional translator. Translate the provided content from %s to %s. Keep brand names, SKUs, numbers, and URLs unchanged. Preserve HTML tags and markdown exactly as written — do not escape forward slashes (write </p>, not <\\/p>). Return ONLY a valid JSON object with this shape: { %s }. Do not wrap in code fences, do not add commentary.',
			$source_label,
			self::language_label( $target ),
			implode( ', ', $schema_parts )
		);

		$user = 'Source (' . $source_label . "):\n" . wp_json_encode( $context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT );

		return array( 'system' => $system, 'user' => $user );
	}

	public static function run_translate( $product, array $settings ) {
		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';

		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$source_raw = $settings['source_language'] ?? '';
		$source     = '' === $source_raw ? '' : sanitize_key( $source_raw );
		$target     = sanitize_key( $settings['target_language'] ?? 'tr' );
		if ( '' !== $source && $source === $target ) {
			return new \WP_Error( 'cpais_translate_same_lang', __( 'Source and target languages are the same.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$parts = $settings['parts'] ?? array( 'title', 'short_description', 'long_description' );

		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}

		$current = isset( $settings['current'] ) && is_array( $settings['current'] ) ? $settings['current'] : array();
		$prompts = self::translate_prompt( $product, $source, $target, $parts, $current );
		$result  = self::cached_complete( $provider, $provider_id, $model, $prompts['system'], $prompts['user'] );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$content = self::parse_json( $result['text'] );
		if ( null === $content ) {
			return new \WP_Error( 'cpais_parse_error', __( 'The AI response could not be parsed.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 502 ) );
		}

		$content = self::unescape_html_slashes( $content );

		return array(
			'content' => $content,
			'usage'   => $result['usage'],
			'meta'    => array(
				'provider'        => $provider_id,
				'model'           => $model,
				'source_language' => '' === $source ? 'auto' : $source,
				'target_language' => $target,
			),
		);
	}

	/**
	 * Some providers emit JSON string values that still contain literal `\/`
	 * sequences (over-escaped forward slashes). json_decode leaves these as
	 * `\/` because the backslash itself was escaped. Strip them from known
	 * HTML-bearing fields so tags like `<\/p>` don't leak into the editor.
	 */
	private static function unescape_html_slashes( array $content ) {
		foreach ( array( 'title', 'short_description', 'long_description', 'description' ) as $field ) {
			if ( isset( $content[ $field ] ) && is_string( $content[ $field ] ) ) {
				$content[ $field ] = str_replace( '\\/', '/', $content[ $field ] );
			}
		}
		return $content;
	}

	public static function run_text( $product, array $settings ) {
		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';

		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$tone     = sanitize_key( $settings['tone'] ?? $settings['default_tone'] ?? 'professional' );
		$length   = sanitize_key( $settings['length'] ?? $settings['default_length'] ?? 'medium' );
		$language = sanitize_key( $settings['language'] ?? Settings::resolve_language() );
		$parts    = $settings['parts'] ?? array( 'short_description', 'long_description', 'alternative_titles', 'features' );

		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}

		$prompts = self::text_prompt( $product, $tone, $length, $language, $parts );
		$result  = self::cached_complete( $provider, $provider_id, $model, $prompts['system'], $prompts['user'] );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$content = self::parse_json( $result['text'] );
		if ( null === $content ) {
			return new \WP_Error( 'cpais_parse_error', __( 'The AI response could not be parsed.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 502 ) );
		}

		$content = self::unescape_html_slashes( $content );

		return array(
			'content' => $content,
			'usage'   => $result['usage'],
			'meta'    => array(
				'provider' => $provider_id,
				'model'    => $model,
				'tone'     => $tone,
				'length'   => $length,
				'language' => $language,
			),
		);
	}

	public static function run_seo( $product, array $settings ) {
		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';

		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$tone     = sanitize_key( $settings['tone'] ?? $settings['default_tone'] ?? 'seo' );
		$language = sanitize_key( $settings['language'] ?? Settings::resolve_language() );

		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}

		$prompts = self::seo_prompt( $product, $tone, $language );
		$result  = self::cached_complete( $provider, $provider_id, $model, $prompts['system'], $prompts['user'] );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$content = self::parse_json( $result['text'] );
		if ( null === $content ) {
			return new \WP_Error( 'cpais_parse_error', __( 'The AI response could not be parsed.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 502 ) );
		}

		list( $content, $warnings ) = self::enforce_seo_lengths( $content );

		return array(
			'content'  => $content,
			'warnings' => $warnings,
			'usage'    => $result['usage'],
			'meta'     => array(
				'provider'    => $provider_id,
				'model'       => $model,
				'integration' => SEO::integration_label(),
			),
		);
	}

	public static function seo_length_limits() {
		return array(
			'meta_title'          => 60,
			'meta_description'    => 160,
			'focus_keyword'       => 60,
			'og_title'            => 60,
			'og_description'      => 110,
			'twitter_title'       => 60,
			'twitter_description' => 110,
		);
	}

	public static function enforce_seo_lengths( array $content ) {
		$limits   = self::seo_length_limits();
		$warnings = array();
		foreach ( $limits as $field => $max ) {
			if ( empty( $content[ $field ] ) ) {
				continue;
			}
			$value = (string) $content[ $field ];
			if ( function_exists( 'mb_strlen' ) ) {
				$len = mb_strlen( $value );
			} else {
				$len = strlen( $value );
			}
			if ( $len > $max ) {
				$truncated = function_exists( 'mb_substr' )
					? mb_substr( $value, 0, $max - 1 ) . '…'
					: substr( $value, 0, $max - 1 ) . '…';
				$warnings[ $field ] = sprintf(
					/* translators: 1: field name, 2: original length, 3: maximum length */
					__( '%1$s was %2$d chars (max %3$d) — truncated.', 'catalogpilot-ai-studio-for-woocommerce' ),
					$field,
					$len,
					$max
				);
				$content[ $field ] = $truncated;
			}
		}
		return array( $content, $warnings );
	}

	public static function run_image( $product, array $settings ) {
		$provider_id = $settings['image_provider'] ?? 'openai';
		$settings_all = Settings::get_all();
		$api_key     = $settings_all[ 'image_' . $provider_id . '_api_key' ] ?? '';

		// Fall back to text provider's key for OpenAI if image key not set.
		if ( '' === $api_key && 'openai' === $provider_id ) {
			$api_key = $settings_all['openai_api_key'] ?? '';
		}

		$model = $settings['image_model'] ?? ( $settings_all[ 'image_' . $provider_id . '_model' ] ?? '' );

		$style  = sanitize_key( $settings['style'] ?? 'studio' );
		$prompt = ! empty( $settings['prompt'] ) ? (string) $settings['prompt'] : self::image_prompt_for_product( $product, $style );
		$opts   = array(
			'size'            => $settings['size'] ?? '1024x1024',
			'count'           => (int) ( $settings['count'] ?? 1 ),
			'negative_prompt' => $settings['negative_prompt'] ?? '',
			'seed'            => $settings['seed'] ?? '',
			'quality'         => $settings['quality'] ?? null,
			'style'           => $settings['style_param'] ?? null,
		);

		// The managed gateway only proxies OpenAI image generation today. Use it when the
		// site is a paying subscriber without their own key — mirrors cached_complete()'s
		// text-generation gateway fallback.
		if ( 'openai' === $provider_id && '' === $api_key
			&& ( Gateway::is_available() || Gateway::is_connected() )
		) {
			$gw = Gateway::generate_image( array(
				'provider' => 'openai',
				'model'    => $model ?: 'gpt-image-1',
				'prompt'   => $prompt,
				'size'     => $opts['size'],
			) );
			if ( ! is_wp_error( $gw ) ) {
				$images = self::gateway_image_to_bytes( $gw['image'] ?? null );
				if ( is_wp_error( $images ) ) {
					return $images;
				}
			} else {
				$code = $gw->get_error_code();
				if ( in_array( $code, array( 'cpais_quota_exceeded', 'cpais_rate_limited', 'cpais_no_install', 'cpais_no_signature', 'cpais_bad_signature', 'cpais_gateway_not_available_on_free' ), true ) ) {
					return $gw;
				}
				// Any other gateway failure (e.g. provider_error) falls through to the
				// normal "no key configured" response below, same as before this fix.
			}
		}

		if ( ! isset( $images ) ) {
			if ( '' === $api_key ) {
				return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the selected image provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
			}

			$provider = Providers\cpais_image_factory( $provider_id, $api_key, $model );
			if ( is_wp_error( $provider ) ) {
				return $provider;
			}

			$images = $provider->generate( $prompt, $opts );
			if ( is_wp_error( $images ) ) {
				return $images;
			}
		}

		$saved = array();
		$alt   = $product->get_name();
		foreach ( $images as $idx => $img ) {
			$filename = 'product-' . $product->get_id() . '-' . ( $idx + 1 );
			$attach_id = Media::sideload_bytes( $img['bytes'], $img['mime'], $product->get_id(), $filename, $alt );
			if ( is_wp_error( $attach_id ) ) {
				continue;
			}
			$saved[] = array(
				'id'  => $attach_id,
				'url' => wp_get_attachment_url( $attach_id ),
			);
		}

		if ( empty( $saved ) ) {
			return new \WP_Error( 'cpais_image_save_failed', __( 'Failed to save images to media library.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}

		$attach_as = $settings['attach_as'] ?? 'none';
		if ( in_array( $attach_as, array( 'featured', 'gallery' ), true ) ) {
			foreach ( $saved as $i => $item ) {
				if ( 'featured' === $attach_as && 0 === $i ) {
					Media::attach_to_product( $item['id'], $product->get_id(), 'featured' );
				} elseif ( 'gallery' === $attach_as ) {
					Media::attach_to_product( $item['id'], $product->get_id(), 'gallery' );
				}
			}
		}

		return array(
			'images' => $saved,
			'prompt' => $prompt,
			'meta'   => array(
				'provider' => $provider_id,
				'model'    => $model,
			),
		);
	}

	/**
	 * Convert the gateway's single-image OpenAI response (b64_json or url) into the
	 * [ [ 'bytes' => binary, 'mime' => string ], ... ] shape the rest of run_image() expects.
	 */
	private static function gateway_image_to_bytes( $item ) {
		if ( empty( $item ) || ! is_array( $item ) ) {
			return new \WP_Error( 'cpais_image_empty', __( 'Gateway returned no image.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		if ( ! empty( $item['b64_json'] ) ) {
			$bytes = base64_decode( $item['b64_json'], true ); // phpcs:ignore
			if ( false === $bytes ) {
				return new \WP_Error( 'cpais_image_empty', __( 'Gateway returned an invalid image.', 'catalogpilot-ai-studio-for-woocommerce' ) );
			}
			return array( array( 'bytes' => $bytes, 'mime' => 'image/png' ) );
		}
		if ( ! empty( $item['url'] ) ) {
			$response = wp_remote_get( $item['url'], array( 'timeout' => 60 ) );
			if ( is_wp_error( $response ) ) {
				return $response;
			}
			$code = wp_remote_retrieve_response_code( $response );
			if ( $code < 200 || $code >= 300 ) {
				return new \WP_Error( 'cpais_image_download', sprintf( 'Download failed (%d)', $code ) );
			}
			$bytes = wp_remote_retrieve_body( $response );
			$mime  = wp_remote_retrieve_header( $response, 'content-type' );
			return array( array( 'bytes' => $bytes, 'mime' => $mime ?: 'image/png' ) );
		}
		return new \WP_Error( 'cpais_image_empty', __( 'Gateway returned no image.', 'catalogpilot-ai-studio-for-woocommerce' ) );
	}

	public static function run_alt_text( $attachment_id, $context, array $settings ) {
		$attachment_id = absint( $attachment_id );
		$attachment    = $attachment_id ? get_post( $attachment_id ) : null;
		if ( ! $attachment || 'attachment' !== $attachment->post_type ) {
			return new \WP_Error( 'cpais_invalid_attachment', __( 'Attachment not found.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 404 ) );
		}

		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';
		if ( '' === $api_key ) { // vision has no managed path — BYOK key required
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$language = sanitize_key( $settings['language'] ?? Settings::resolve_language() );

		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}

		$image_url = wp_get_attachment_url( $attachment_id );
		if ( ! $image_url ) {
			return new \WP_Error( 'cpais_no_image_url', __( 'Could not resolve image URL.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}

		$system = sprintf(
			'You write concise SEO-friendly alt text for product images. Write in %s. Reply with ONLY the alt text — a single sentence, max 125 characters, descriptive, no "image of" or "picture of" prefix, no quotes, no markdown.',
			self::language_label( $language )
		);
		$user = 'Product context: ' . wp_json_encode( $context, JSON_UNESCAPED_UNICODE );

		$result = self::cached_vision( $provider, $provider_id, $model, $system, $user, array( $image_url ) );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$alt = trim( (string) $result['text'] );
		$alt = trim( $alt, " \t\n\r\0\x0B\"'" );
		if ( function_exists( 'mb_strlen' ) && mb_strlen( $alt ) > 125 ) {
			$alt = mb_substr( $alt, 0, 124 ) . '…';
		}

		return array(
			'alt_text' => $alt,
			'usage'    => $result['usage'] ?? array(),
			'meta'     => array(
				'provider'      => $provider_id,
				'model'         => $model,
				'attachment_id' => $attachment_id,
			),
		);
	}

	public static function apply_alt_text( $attachment_id, $alt_text ) {
		$attachment_id = absint( $attachment_id );
		if ( ! $attachment_id ) {
			return false;
		}
		update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $alt_text ) );
		return true;
	}

	public static function run_taxonomy( $term_id, $taxonomy, array $settings ) {
		$term_id  = absint( $term_id );
		$taxonomy = sanitize_key( $taxonomy );
		$term     = $term_id ? get_term( $term_id, $taxonomy ) : null;
		if ( ! $term || is_wp_error( $term ) ) {
			return new \WP_Error( 'cpais_invalid_term', __( 'Term not found.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 404 ) );
		}

		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';
		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$tone     = sanitize_key( $settings['tone']     ?? $settings['default_tone']     ?? 'professional' );
		$length   = sanitize_key( $settings['length']   ?? $settings['default_length']   ?? 'medium' );
		$language = sanitize_key( $settings['language'] ?? Settings::resolve_language() );

		$word_map = array( 'short' => 80, 'medium' => 150, 'long' => 280 );
		$target_words = $word_map[ $length ] ?? 150;

		$is_tag = ( 'product_tag' === $taxonomy );
		$label  = $is_tag ? 'product tag' : 'product category';

		$sample_products = array();
		$products = get_posts(
			array(
				'post_type'      => 'product',
				'posts_per_page' => 8,
				'fields'         => 'ids',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Product related-items lookup on category page; small result set (posts_per_page=8), acceptable.
				'tax_query'      => array(
					array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_id ),
				),
			)
		);
		foreach ( $products as $pid ) {
			$sample_products[] = get_the_title( $pid );
		}

		$context = array(
			'taxonomy'             => $taxonomy,
			'term_name'            => $term->name,
			'current_description'  => wp_strip_all_tags( $term->description ),
			'parent'               => $term->parent ? get_term_field( 'name', $term->parent, $taxonomy ) : '',
			'sample_product_names' => $sample_products,
		);

		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}

		$system = sprintf(
			'You are a WooCommerce copywriter. Write a %s description in %s with a %s tone, approximately %d words. Focus on the shopper — what they will find in this %s and why it matters. HTML allowed (use <p> and <ul>/<li>). Return ONLY a valid JSON object: { "description": "string" }. No code fences, no commentary.',
			$label,
			self::language_label( $language ),
			$tone,
			$target_words,
			$label
		);

		$user = "Term context:\n" . wp_json_encode( $context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT );

		$result = self::cached_complete( $provider, $provider_id, $model, $system, $user );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$parsed = self::parse_json( $result['text'] );
		if ( ! is_array( $parsed ) || empty( $parsed['description'] ) ) {
			return new \WP_Error( 'cpais_parse_error', __( 'The AI response could not be parsed.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 502 ) );
		}

		$parsed = self::unescape_html_slashes( $parsed );

		return array(
			'description' => $parsed['description'],
			'usage'       => $result['usage'],
			'meta'        => array(
				'provider' => $provider_id,
				'model'    => $model,
				'term_id'  => $term_id,
				'taxonomy' => $taxonomy,
			),
		);
	}

	public static function apply_taxonomy_description( $term_id, $taxonomy, $description ) {
		$term_id  = absint( $term_id );
		$taxonomy = sanitize_key( $taxonomy );
		if ( ! $term_id || ! taxonomy_exists( $taxonomy ) ) {
			return new \WP_Error( 'cpais_invalid_term', __( 'Term not found.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		wp_update_term( $term_id, $taxonomy, array( 'description' => wp_kses_post( $description ) ) );
		return true;
	}

	/**
	 * Generate four (or chosen subset) of distinct image variants for a product.
	 * Each variant is a separate provider call with a different prompt suffix.
	 *
	 * @param \WC_Product $product
	 * @param array       $settings  Same keys as run_image, plus:
	 *                                - variants (string[])  Subset of keys from image_variant_definitions().
	 *                                                       Defaults to all four.
	 * @return array|\WP_Error  { variants: [{ key, label, image: { id, url } | null, error: string|null }, ...], meta }
	 */
	public static function run_image_variants( $product, array $settings ) {
		$provider_id  = $settings['image_provider'] ?? 'openai';
		$settings_all = Settings::get_all();
		$api_key      = $settings_all[ 'image_' . $provider_id . '_api_key' ] ?? '';
		if ( '' === $api_key && 'openai' === $provider_id ) {
			$api_key = $settings_all['openai_api_key'] ?? '';
		}
		$model = $settings['image_model'] ?? ( $settings_all[ 'image_' . $provider_id . '_model' ] ?? '' );

		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the selected image provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$provider = Providers\cpais_image_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}

		$definitions = self::image_variant_definitions();
		$requested   = isset( $settings['variants'] ) && is_array( $settings['variants'] ) && ! empty( $settings['variants'] )
			? array_values( array_intersect( array_keys( $definitions ), array_map( 'sanitize_key', $settings['variants'] ) ) )
			: array_keys( $definitions );

		if ( empty( $requested ) ) {
			return new \WP_Error( 'cpais_no_variants', __( 'At least one variant must be selected.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$context = self::product_context( $product );
		$base    = $context['title'];
		if ( ! empty( $context['categories'] ) ) {
			$base .= '. category: ' . implode( ', ', $context['categories'] );
		}
		if ( ! empty( $context['current_short'] ) ) {
			$base .= '. details: ' . wp_trim_words( $context['current_short'], 25 );
		}

		$attach_as = sanitize_key( $settings['attach_as'] ?? 'gallery' );
		$results   = array();
		$saved_any = false;

		foreach ( $requested as $key ) {
			$def    = $definitions[ $key ];
			$prompt = $base . '. ' . $def['suffix'];

			$opts = array(
				'size'  => $settings['size'] ?? '1024x1024',
				'count' => 1,
			);

			$images = $provider->generate( $prompt, $opts );
			if ( is_wp_error( $images ) ) {
				$results[] = array(
					'key'   => $key,
					'label' => $def['label'],
					'image' => null,
					'error' => $images->get_error_message(),
				);
				continue;
			}

			$first = $images[0] ?? null;
			if ( ! $first || empty( $first['bytes'] ) ) {
				$results[] = array(
					'key'   => $key,
					'label' => $def['label'],
					'image' => null,
					'error' => __( 'Provider returned no image data.', 'catalogpilot-ai-studio-for-woocommerce' ),
				);
				continue;
			}

			$filename  = 'product-' . $product->get_id() . '-' . $key;
			$attach_id = Media::sideload_bytes( $first['bytes'], $first['mime'], $product->get_id(), $filename, $product->get_name() );
			if ( is_wp_error( $attach_id ) ) {
				$results[] = array(
					'key'   => $key,
					'label' => $def['label'],
					'image' => null,
					'error' => $attach_id->get_error_message(),
				);
				continue;
			}

			if ( in_array( $attach_as, array( 'featured', 'gallery' ), true ) ) {
				// Featured only for the first successful one when explicitly requested.
				if ( 'featured' === $attach_as && ! $saved_any ) {
					Media::attach_to_product( $attach_id, $product->get_id(), 'featured' );
				} else {
					Media::attach_to_product( $attach_id, $product->get_id(), 'gallery' );
				}
			}

			$saved_any = true;
			$results[] = array(
				'key'   => $key,
				'label' => $def['label'],
				'image' => array(
					'id'  => (int) $attach_id,
					'url' => wp_get_attachment_url( $attach_id ),
				),
				'error' => null,
			);
		}

		return array(
			'variants' => $results,
			'meta'     => array(
				'provider' => $provider_id,
				'model'    => $model,
				'attached' => $attach_as,
			),
		);
	}

	public static function faq_prompt( $product, $language, $count ) {
		$context = self::product_context( $product );
		$lang    = self::language_label( $language );

		$system = sprintf(
			'You are an e-commerce FAQ writer. Based on the product data, generate a list of %d unique, useful frequently-asked questions customers would ask, each with a clear concise answer (1-2 sentences). Use specific product details (attributes, materials, compatibility, use cases) — avoid generic filler like "What is shipping?". Write in %s. Return ONLY a valid JSON object { "faq": [ { "question": "string", "answer": "string" } ] }. Do not wrap in code fences, do not add commentary.',
			max( 1, min( 15, (int) $count ) ),
			$lang
		);

		$user = 'Product data:' . "\n" . wp_json_encode( $context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT );

		return array( 'system' => $system, 'user' => $user );
	}

	public static function run_faq( $product, array $settings ) {
		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';

		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$language = sanitize_key( $settings['language'] ?? Settings::resolve_language() );
		$count    = (int) ( $settings['faq_count'] ?? $settings['faq_default_count'] ?? 5 );
		$count    = max( 1, min( 15, $count ) );

		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}

		$prompts = self::faq_prompt( $product, $language, $count );
		$result  = self::cached_complete( $provider, $provider_id, $model, $prompts['system'], $prompts['user'] );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$content = self::parse_json( $result['text'] );
		if ( null === $content || empty( $content['faq'] ) || ! is_array( $content['faq'] ) ) {
			return new \WP_Error( 'cpais_parse_error', __( 'The AI response could not be parsed.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 502 ) );
		}

		$items = array();
		foreach ( $content['faq'] as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$q = isset( $item['question'] ) ? trim( (string) $item['question'] ) : '';
			$a = isset( $item['answer'] )   ? trim( (string) $item['answer'] )   : '';
			if ( '' === $q || '' === $a ) {
				continue;
			}
			$items[] = array( 'question' => $q, 'answer' => $a );
		}

		return array(
			'faq'   => $items,
			'usage' => $result['usage'],
			'meta'  => array(
				'provider' => $provider_id,
				'model'    => $model,
				'language' => $language,
				'count'    => count( $items ),
			),
		);
	}

	public static function review_summary_prompt( $product, array $reviews_data, $language ) {
		$lang = self::language_label( $language );

		$samples_text = '';
		foreach ( $reviews_data['samples'] as $i => $r ) {
			$samples_text .= ( $i + 1 ) . '. ' . $r . "\n";
		}

		$system = sprintf(
			'You are summarizing customer reviews for an e-commerce product. Read the reviews and write ONE short, punchy testimonial-style sentence (max 25 words) that captures what customers consistently love or note. Avoid generic praise; quote specific recurring traits when possible. Write in %s. Also classify overall sentiment as one of: positive, mixed, negative. Return ONLY a valid JSON object { "summary": "string", "sentiment": "positive|mixed|negative" }. No code fences.',
			$lang
		);

		$user = sprintf(
			"Product: %s\nAverage rating: %s/5 (%d reviews provided)\n\nReviews:\n%s",
			$product->get_name(),
			$reviews_data['avg_rating'] > 0 ? $reviews_data['avg_rating'] : 'n/a',
			$reviews_data['count'],
			$samples_text
		);

		return array( 'system' => $system, 'user' => $user );
	}

	public static function run_review_summary( $product, array $settings ) {
		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';

		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}

		$reviews   = Review::fetch_reviews( $product->get_id(), 50 );
		$min_count = max( 1, (int) ( $settings['review_min_count'] ?? 3 ) );
		if ( $reviews['count'] < $min_count ) {
			return new \WP_Error(
				'cpais_review_too_few',
				sprintf(
					/* translators: 1: required count, 2: actual count */
					__( 'Need at least %1$d reviews to summarize (this product has %2$d).', 'catalogpilot-ai-studio-for-woocommerce' ),
					$min_count,
					$reviews['count']
				),
				array( 'status' => 400 )
			);
		}

		$language = sanitize_key( $settings['language'] ?? Settings::resolve_language() );

		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}

		$prompts = self::review_summary_prompt( $product, $reviews, $language );
		$result  = self::cached_complete( $provider, $provider_id, $model, $prompts['system'], $prompts['user'] );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$content = self::parse_json( $result['text'] );
		if ( null === $content || empty( $content['summary'] ) ) {
			return new \WP_Error( 'cpais_parse_error', __( 'The AI response could not be parsed.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 502 ) );
		}

		return array(
			'summary'    => trim( (string) $content['summary'] ),
			'sentiment'  => isset( $content['sentiment'] ) ? sanitize_key( $content['sentiment'] ) : '',
			'count'      => $reviews['count'],
			'avg_rating' => $reviews['avg_rating'],
			'usage'      => $result['usage'],
			'meta'       => array(
				'provider' => $provider_id,
				'model'    => $model,
				'language' => $language,
			),
		);
	}

	public static function apply_text_to_product( $product, array $content ) {
		$update = array( 'ID' => $product->get_id() );
		$dirty  = false;
		if ( ! empty( $content['short_description'] ) ) {
			$update['post_excerpt'] = wp_kses_post( $content['short_description'] );
			$dirty = true;
		}
		if ( ! empty( $content['long_description'] ) ) {
			$update['post_content'] = wp_kses_post( $content['long_description'] );
			$dirty = true;
		}
		if ( $dirty ) {
			wp_update_post( $update );
		}
	}

	public static function parse_json( $text ) {
		$text = trim( (string) $text );
		if ( '' === $text ) {
			return null;
		}
		$text    = preg_replace( '/^```(?:json)?\s*/i', '', $text );
		$text    = preg_replace( '/\s*```$/', '', $text );
		$decoded = json_decode( $text, true );
		if ( is_array( $decoded ) ) {
			return $decoded;
		}
		if ( preg_match( '/\{.*\}/s', $text, $m ) ) {
			$decoded = json_decode( $m[0], true );
			if ( is_array( $decoded ) ) {
				return $decoded;
			}
		}
		return null;
	}

	/**
	 * Suggest 3-8 product tags via AI.
	 *
	 * @param \WC_Product $product
	 * @param array       $settings
	 * @return array|\WP_Error {tags: string[], usage: array}
	 */
	public static function run_tags( $product, array $settings ) {
		if ( ! ( $product instanceof \WC_Product ) ) {
			return new \WP_Error( 'cpais_no_product', __( 'Product not found.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		$language = sanitize_key( $settings['language'] ?? Settings::resolve_language( $product->get_id() ) );
		$count    = max( 3, min( 15, (int) ( $settings['tag_count'] ?? 6 ) ) );
		$context  = self::product_context( $product );

		$system = sprintf(
			'You are an e-commerce merchandising assistant. Given the product data, propose %d short SEO-friendly product tags (1-3 words each, lowercase, singular where possible). Write in %s. Return ONLY valid JSON: { "tags": ["tag1","tag2",...] }. No commentary, no code fences.',
			$count,
			self::language_label( $language )
		);
		$user = sprintf(
			"Title: %s\nCategories: %s\nShort description: %s\nExisting tags: %s",
			$context['title'],
			implode( ', ', (array) ( $context['categories'] ?? array() ) ),
			$context['current_short'] ?? '',
			implode( ', ', wp_get_post_terms( $product->get_id(), 'product_tag', array( 'fields' => 'names' ) ) ?: array() )
		);

		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';
		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}
		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}
		$result = self::cached_complete( $provider, $provider_id, $model, $system, $user );
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		$parsed = self::parse_json( (string) ( $result['text'] ?? $result['content'] ?? '' ) );
		$tags   = array();
		if ( is_array( $parsed ) && isset( $parsed['tags'] ) && is_array( $parsed['tags'] ) ) {
			foreach ( $parsed['tags'] as $t ) {
				$t = sanitize_text_field( (string) $t );
				if ( '' !== $t ) {
					$tags[] = $t;
				}
			}
		}
		return array( 'tags' => $tags, 'usage' => $result['usage'] ?? array() );
	}

	/**
	 * Apply AI-suggested tags to a product (creates missing terms).
	 */
	public static function apply_tags( $product, array $tags ) {
		if ( ! ( $product instanceof \WC_Product ) ) {
			return new \WP_Error( 'cpais_no_product', __( 'Product not found.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		$clean = array_values( array_unique( array_filter( array_map( 'sanitize_text_field', $tags ) ) ) );
		if ( empty( $clean ) ) {
			return array( 'applied' => 0 );
		}
		$result = wp_set_object_terms( $product->get_id(), $clean, 'product_tag', true );
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		return array( 'applied' => count( (array) $result ) );
	}

	/**
	 * Suggest custom attribute name+value pairs via AI.
	 *
	 * Attributes returned as free-form key/value strings (not linked to global
	 * WooCommerce attributes); site admins can review before applying.
	 *
	 * @param \WC_Product $product
	 * @param array       $settings
	 * @return array|\WP_Error {attributes: array<int,array{name:string,value:string}>, usage: array}
	 */
	public static function run_attributes( $product, array $settings ) {
		if ( ! ( $product instanceof \WC_Product ) ) {
			return new \WP_Error( 'cpais_no_product', __( 'Product not found.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		$language = sanitize_key( $settings['language'] ?? Settings::resolve_language( $product->get_id() ) );
		$count    = max( 3, min( 12, (int) ( $settings['attribute_count'] ?? 6 ) ) );
		$context  = self::product_context( $product );

		$system = sprintf(
			'You are a product data assistant. Given the product data, propose %d useful attribute name/value pairs a shopper would look for (material, dimensions, weight, compatibility, warranty, care instructions, capacity, etc.). Write in %s. Return ONLY valid JSON: { "attributes": [ { "name": "string", "value": "string" }, ... ] }. Concise values (2-6 words). No commentary, no code fences.',
			$count,
			self::language_label( $language )
		);
		$user = sprintf(
			"Title: %s\nCategories: %s\nShort description: %s",
			$context['title'],
			implode( ', ', (array) ( $context['categories'] ?? array() ) ),
			$context['current_short'] ?? ''
		);

		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';
		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}
		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}
		$result = self::cached_complete( $provider, $provider_id, $model, $system, $user );
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		$parsed = self::parse_json( (string) ( $result['text'] ?? $result['content'] ?? '' ) );
		$attrs  = array();
		if ( is_array( $parsed ) && isset( $parsed['attributes'] ) && is_array( $parsed['attributes'] ) ) {
			foreach ( $parsed['attributes'] as $row ) {
				if ( ! is_array( $row ) ) {
					continue;
				}
				$name  = sanitize_text_field( (string) ( $row['name'] ?? '' ) );
				$value = sanitize_text_field( (string) ( $row['value'] ?? '' ) );
				if ( '' !== $name && '' !== $value ) {
					$attrs[] = array( 'name' => $name, 'value' => $value );
				}
			}
		}
		return array( 'attributes' => $attrs, 'usage' => $result['usage'] ?? array() );
	}

	/**
	 * Apply AI-suggested attributes to a product as custom (non-global) attributes.
	 * Existing WooCommerce attributes are preserved and appended to.
	 */
	public static function apply_attributes( $product, array $attributes ) {
		if ( ! ( $product instanceof \WC_Product ) ) {
			return new \WP_Error( 'cpais_no_product', __( 'Product not found.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		if ( empty( $attributes ) ) {
			return array( 'applied' => 0 );
		}
		$existing = (array) $product->get_attributes();
		$position = count( $existing );
		$added    = 0;
		foreach ( $attributes as $pair ) {
			if ( ! is_array( $pair ) ) {
				continue;
			}
			$name  = sanitize_text_field( (string) ( $pair['name'] ?? '' ) );
			$value = sanitize_text_field( (string) ( $pair['value'] ?? '' ) );
			if ( '' === $name || '' === $value ) {
				continue;
			}
			$slug = sanitize_title( $name );
			if ( isset( $existing[ $slug ] ) ) {
				continue;
			}
			$attr = new \WC_Product_Attribute();
			$attr->set_name( $name );
			$attr->set_options( array( $value ) );
			$attr->set_visible( true );
			$attr->set_variation( false );
			$attr->set_position( $position++ );
			$existing[ $slug ] = $attr;
			$added++;
		}
		$product->set_attributes( $existing );
		$product->save();
		return array( 'applied' => $added );
	}

	/**
	 * Generate a description for a WooCommerce variation via AI.
	 *
	 * @param \WC_Product_Variation $variation
	 * @param array                 $settings
	 * @return array|\WP_Error {description: string, usage: array}
	 */
	public static function run_variation_description( $variation, array $settings ) {
		if ( ! ( $variation instanceof \WC_Product_Variation ) ) {
			return new \WP_Error( 'cpais_no_variation', __( 'Variation not found.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		$parent = wc_get_product( $variation->get_parent_id() );
		if ( ! $parent ) {
			return new \WP_Error( 'cpais_no_parent', __( 'Parent product not found.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		$language = sanitize_key( $settings['language'] ?? Settings::resolve_language( $variation->get_id() ) );
		$len      = (int) ( $settings['variation_words'] ?? 60 );

		$attrs = $variation->get_variation_attributes();
		$pretty = array();
		foreach ( $attrs as $k => $v ) {
			$pretty[] = str_replace( array( 'attribute_pa_', 'attribute_' ), '', (string) $k ) . ': ' . (string) $v;
		}

		$system = sprintf(
			'You are a WooCommerce copywriter. Write a %d-word description for ONE specific product variation. Focus on what makes THIS combination unique. Write in %s. Return ONLY valid JSON: { "description": "string, HTML allowed" }. No commentary, no code fences.',
			$len,
			self::language_label( $language )
		);
		$user = sprintf(
			"Parent title: %s\nParent short description: %s\nVariation attributes: %s\nVariation price: %s",
			$parent->get_name(),
			wp_strip_all_tags( $parent->get_short_description() ),
			implode( ', ', $pretty ),
			$variation->get_price()
		);

		$provider_id = $settings['active_provider'] ?? 'openai';
		$api_key     = $settings[ $provider_id . '_api_key' ] ?? '';
		$model       = $settings[ $provider_id . '_model' ] ?? '';
		if ( '' === $api_key && ! Gateway::is_connected() ) {
			return new \WP_Error( 'cpais_missing_key', __( 'No API key configured for the active provider.', 'catalogpilot-ai-studio-for-woocommerce' ), array( 'status' => 400 ) );
		}
		$provider = Providers\cpais_factory( $provider_id, $api_key, $model );
		if ( is_wp_error( $provider ) ) {
			return $provider;
		}
		$result = self::cached_complete( $provider, $provider_id, $model, $system, $user );
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		$parsed = self::parse_json( (string) ( $result['text'] ?? $result['content'] ?? '' ) );
		$description = '';
		if ( is_array( $parsed ) && isset( $parsed['description'] ) ) {
			$description = wp_kses_post( (string) $parsed['description'] );
		}
		return array( 'description' => $description, 'usage' => $result['usage'] ?? array() );
	}

	/**
	 * Save a variation description to the variation post_content.
	 */
	public static function apply_variation_description( $variation_id, $description ) {
		$variation_id = (int) $variation_id;
		if ( $variation_id <= 0 ) {
			return new \WP_Error( 'cpais_no_variation', __( 'Variation not found.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		$post = get_post( $variation_id );
		if ( ! $post || 'product_variation' !== $post->post_type ) {
			return new \WP_Error( 'cpais_no_variation', __( 'Variation not found.', 'catalogpilot-ai-studio-for-woocommerce' ) );
		}
		wp_update_post( array(
			'ID'           => $variation_id,
			'post_content' => wp_kses_post( (string) $description ),
		) );
		return array( 'updated' => true );
	}
}
