<?php
/**
 * Mapping Handler – centralises all mapping-related business logic that was
 * previously embedded in multiple template-mapping controllers.
 *
 * @package    MOMCS_Sync
 * @subpackage MOMCS_Sync\includes\common\handlers
 * @author     miniOrange
 */

declare(strict_types=1);

namespace MOMCS_Sync\Includes\Common\Handlers;

use MOMCS_Sync\Includes\Common\Models\Multichannels_Model;
use MOMCS_Sync\Includes\Common\Models\Product_Mapping_Model;
use MOMCS_Sync\Includes\Common\Utils\Logger;
use MOMCS_Sync\Includes\Common\Utils\Utils;

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

/**
 * Class Mapping_Handler
 *
 * Responsibilities:
 * 1. Persist and retrieve default & template field mappings for a channel.
 * 2. Provide thin wrappers compatible with legacy AJAX endpoints so the
 *    JS/front-end code can continue to call the same actions while the heavy
 *    lifting resides here.
 */
class Mapping_Handler {

	/**
	 * Product mapping model instance.
	 *
	 * @var Product_Mapping_Model
	 */
	private Product_Mapping_Model $product_mapping_model;
	/**
	 * Multichannels model instance.
	 *
	 * @var Multichannels_Model
	 */
	private Multichannels_Model $multichannels_model;
	/**
	 * Logger instance.
	 *
	 * @var Logger
	 */
	private Logger $logger;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->product_mapping_model = new Product_Mapping_Model();
		$this->multichannels_model   = new Multichannels_Model();
		$this->logger                = new Logger( 'mapping-handler' );
	}

	/**
	 * Save unified mapping configuration for a marketplace/channel.
	 *
	 * @param string $marketplace        Marketplace slug (e.g. 'amazon').
	 * @param string $category_mapping_json JSON encoded category mappings.
	 * @param string $field_mapping_json    JSON encoded field mappings.
	 * @param string $selected_category    Selected category string.
	 * @param string $general_mapping_json  JSON encoded default mapping key/values.
	 * @param string $channel_id           Channel UUID (required for multi-channel support).
	 *
	 * @return string|null Saved template_config row UUID when a template row was written; null otherwise.
	 * @throws \RuntimeException On validation / DB failure.
	 */
	public function save_unified_mapping( string $marketplace, string $category_mapping_json, string $field_mapping_json, string $selected_category, string $general_mapping_json, string $channel_id ): ?string {
		// Validate that the provided channel_id exists and matches the marketplace.
		$channel_data = $this->multichannels_model->get_channel_by_uuid( $channel_id );
		if ( ! $channel_data ) {
			throw new \RuntimeException( 'Invalid channel ID provided.' );
		}
		if ( strtolower( $channel_data['marketplace'] ?? '' ) !== strtolower( $marketplace ) ) {
			throw new \RuntimeException( 'Channel ID does not match the specified marketplace.' );
		}

		// 1. Default mappings.
		$general_mappings = $this->maybe_decode_json( $general_mapping_json );
		if ( ! empty( $general_mappings ) ) {
			$this->persist_mapping( $channel_id, 'default_mapping', $general_mappings );
		}

		// 2. Template-specific mappings.
		$saved_template_uuid = null;
		$has_template_data   = $category_mapping_json || $field_mapping_json || $selected_category;
		if ( $has_template_data ) {
			$sanitized_field_mappings = $this->sanitize_field_mapping_keys( $this->maybe_decode_json( $field_mapping_json ) );
			$template_config          = array(
				'category_mappings' => $this->maybe_decode_json( $category_mapping_json ),
				'selected_category' => $selected_category,
				'field_mappings'    => $sanitized_field_mappings,
			);
			$wc_category     = (string) ( $template_config['category_mappings']['wc_category'] ?? '' );
			$amazon_category = (string) ( $template_config['category_mappings']['amazon_category'] ?? $selected_category );
			if ( '' !== $wc_category && '' !== $amazon_category ) {
				$saved_template_uuid = $this->persist_template_config_by_pair( $channel_id, $template_config );
			} else {
				$this->persist_mapping( $channel_id, 'template_config', $template_config );
				$row = $this->product_mapping_model->get_mapping_by_channel_and_config( $channel_id, 'template_config' );
				if ( $row && ! empty( $row['uuid'] ) ) {
					$saved_template_uuid = (string) $row['uuid'];
				}
			}
		}

		// Flag that mapping exists – used by the UI wizard.
		update_option( 'momcs_' . strtolower( $marketplace ) . '_mapping_saved', true );

		return $saved_template_uuid;
	}

	/**
	 * Fetch unified mapping dataset.
	 *
	 * @param string $marketplace Marketplace slug.
	 * @param string $channel_id  Channel UUID (required for multi-channel support).
	 *
	 * @return array<string,mixed>  keys: default_mappings, category_mappings, selected_category, field_mappings
	 * @throws \RuntimeException On channel not found.
	 */
	public function get_unified_mapping( string $marketplace, string $channel_id ): array {
		// Validate that the provided channel_id exists and matches the marketplace.
		$channel_data = $this->multichannels_model->get_channel_by_uuid( $channel_id );
		if ( ! $channel_data ) {
			throw new \RuntimeException( 'Invalid channel ID provided.' );
		}
		if ( strtolower( $channel_data['marketplace'] ?? '' ) !== strtolower( $marketplace ) ) {
			throw new \RuntimeException( 'Channel ID does not match the specified marketplace.' );
		}

		$default  = $this->product_mapping_model->get_mapping_by_channel_and_config( $channel_id, 'default_mapping' );
		$template = $this->product_mapping_model->get_mapping_by_channel_and_config( $channel_id, 'template_config' );

		$default_cfg  = $default && $default['config'] ? $this->maybe_decode_json( $default['config'] ) : array();
		$template_cfg = $template && $template['config'] ? $this->maybe_decode_json( $template['config'] ) : array();

		$field_mappings = $template_cfg['field_mappings'] ?? array();
		$valid_slugs    = array_keys( Utils::get_wc_product_attributes() );
		$field_mappings = $this->normalize_field_mapping_slugs( $field_mappings, $valid_slugs );

		return array(
			'default_mappings'  => $default_cfg,
			'category_mappings' => $template_cfg['category_mappings'] ?? array(),
			'selected_category' => $template_cfg['selected_category'] ?? '',
			'field_mappings'    => $field_mappings,
		);
	}

	/**
	 * Create WooCommerce global attributes from a JSON string (if valid).
	 *
	 * @param string $new_attributes_json JSON-encoded attributes payload.
	 *
	 * @return void
	 * @throws \RuntimeException On JSON decode error.
	 */
	public function create_new_woocommerce_attributes_from_json( string $new_attributes_json ): void {
		if ( '' === $new_attributes_json ) {
			return;
		}
		$attributes_data = json_decode( $new_attributes_json, true );
		if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $attributes_data ) ) {
			return;
		}
		$this->create_woocommerce_attributes( $attributes_data );
	}

	/**
	 * Save template mapping.
	 *
	 * @param string $marketplace        Marketplace slug (e.g. 'amazon').
	 * @param string $mapping_json       JSON encoded mapping.
	 * @param string $channel_id         Channel UUID (required for multi-channel support).
	 *
	 * @return void
	 */
	public function save_template_mapping( string $marketplace, string $mapping_json, string $channel_id ): void {
		$this->save_unified_mapping( $marketplace, $mapping_json, '', '', '', $channel_id );
	}

	/**
	 * Get template mapping.
	 *
	 * @param string $marketplace Marketplace slug (e.g. 'amazon').
	 * @param string $channel_id  Channel UUID (required for multi-channel support).
	 *
	 * @return array<string,mixed> Template mapping.
	 */
	public function get_template_mapping( string $marketplace, string $channel_id ): array {
		$data = $this->get_unified_mapping( $marketplace, $channel_id );
		return $data['category_mappings'] ?? array();
	}

	/**
	 * Save field mapping.
	 *
	 * @param string $marketplace        Marketplace slug (e.g. 'amazon').
	 * @param string $mapping_json       JSON encoded mapping.
	 * @param string $product_type       Product type.
	 * @param string $channel_id         Channel UUID (required for multi-channel support).
	 *
	 * @return void
	 */
	public function save_field_mapping( string $marketplace, string $mapping_json, string $product_type, string $channel_id ): void {
		// Update only field mappings & selected category.
		$data = $this->get_unified_mapping( $marketplace, $channel_id );
		$this->save_unified_mapping( $marketplace, wp_json_encode( $data['category_mappings'] ), $mapping_json, $product_type, wp_json_encode( $data['default_mappings'] ), $channel_id );
	}

	/**
	 * Get field mapping.
	 *
	 * @param string $marketplace Marketplace slug (e.g. 'amazon').
	 * @param string $channel_id  Channel UUID (required for multi-channel support).
	 *
	 * @return array<string,mixed> Field mapping.
	 */
	public function get_field_mapping( string $marketplace, string $channel_id ): array {
		$data = $this->get_unified_mapping( $marketplace, $channel_id );
		return array(
			'selected_category' => $data['selected_category'] ?? '',
			'field_mappings'    => $data['field_mappings'] ?? array(),
		);
	}

	/**
	 * Get field mappings with nested structure expanded for payload building.
	 * Works with the new unified collection format supporting both simple and hierarchical mappings.
	 *
	 * @param string $marketplace Marketplace slug.
	 * @param string $channel_id  Channel UUID (required for multi-channel support).
	 * @return array<string,mixed> Field mappings with nested structure preserved.
	 */
	public function get_expanded_field_mappings( string $marketplace, string $channel_id ): array {
		$data           = $this->get_unified_mapping( $marketplace, $channel_id );
		$field_mappings = $data['field_mappings'] ?? array();
		return $field_mappings;
	}

	/**
	 * Persist a mapping configuration for a channel.
	 *
	 * @param string $channel_id    Channel UUID.
	 * @param string $config_name   Configuration name.
	 * @param array  $payload       Mapping payload.
	 *
	 * @return void
	 * @throws \RuntimeException On DB failure.
	 */
	private function persist_mapping( string $channel_id, string $config_name, array $payload ): void {
		$data = array(
			'uuid'        => wp_generate_uuid4(),
			'channel_id'  => $channel_id,
			'config_name' => $config_name,
			'config'      => wp_json_encode( $payload ),
			'created_at'  => current_time( 'mysql' ),
			'updated_at'  => current_time( 'mysql' ),
		);

		$ok = $this->product_mapping_model->update_or_create_mapping( $channel_id, $config_name, $data );
		if ( ! $ok ) {
			throw new \RuntimeException( 'Failed to persist mapping data.' );
		}
	}

	/**
	 * Get unified mapping for a specific (wc_category, amazon_category) pair.
	 *
	 * @param string $marketplace      Marketplace slug.
	 * @param string $wc_category      WooCommerce category ID.
	 * @param string $amazon_category  Amazon category/product type.
	 * @param string $channel_id       Channel UUID (required for multi-channel support).
	 * @return array<string,mixed>
	 * @throws \RuntimeException On channel not found.
	 */
	public function get_unified_mapping_by_pair( string $marketplace, string $wc_category, string $amazon_category, string $channel_id ): array {
		// Validate that the provided channel_id exists and matches the marketplace.
		$channel_data = $this->multichannels_model->get_channel_by_uuid( $channel_id );
		if ( ! $channel_data ) {
			throw new \RuntimeException( 'Invalid channel ID provided.' );
		}
		if ( strtolower( $channel_data['marketplace'] ?? '' ) !== strtolower( $marketplace ) ) {
			throw new \RuntimeException( 'Channel ID does not match the specified marketplace.' );
		}
		$rows = $this->product_mapping_model->get_mappings_by_channel_id( $channel_id );
		foreach ( $rows as $row ) {
			if ( ( $row['config_name'] ?? '' ) !== 'template_config' || empty( $row['config'] ) ) {
				continue;
			}
			$cfg = json_decode( (string) $row['config'], true );
			if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $cfg ) ) {
				continue;
			}
			$row_wc  = isset( $cfg['category_mappings']['wc_category'] ) ? (string) $cfg['category_mappings']['wc_category'] : '';
			$row_amz = isset( $cfg['category_mappings']['amazon_category'] ) ? (string) $cfg['category_mappings']['amazon_category'] : ( isset( $cfg['selected_category'] ) ? (string) $cfg['selected_category'] : '' );
			if ( $row_wc === (string) $wc_category && $row_amz === (string) $amazon_category ) {
				return array(
					'default_mappings'  => array(),
					'category_mappings' => $cfg['category_mappings'] ?? array(),
					'selected_category' => $cfg['selected_category'] ?? '',
					'field_mappings'    => $cfg['field_mappings'] ?? array(),
				);
			}
		}
		return array(
			'default_mappings'  => array(),
			'category_mappings' => array(),
			'selected_category' => '',
			'field_mappings'    => array(),
		);
	}

	/**
	 * Persist or update a template_config row keyed by (wc_category, amazon_category).

	 * In free plan, ensures only a single template_config row exists by deleting previous ones,
	 * effectively overwriting. In premium, multiple rows (pairs) are allowed.
	 *
	 * @param string              $channel_id Channel UUID.
	 * @param array<string,mixed> $payload    Template config payload.
	 * @return string UUID of the created or updated template_config row.
	 */
	private function persist_template_config_by_pair( string $channel_id, array $payload ): string {
		$rows            = $this->product_mapping_model->get_mappings_by_channel_id( $channel_id );
		$target_uuid     = '';
		$wc_category     = (string) ( $payload['category_mappings']['wc_category'] ?? '' );
		$amazon_category = (string) ( $payload['category_mappings']['amazon_category'] ?? ( $payload['selected_category'] ?? '' ) );

		foreach ( $rows as $row ) {
			if ( ( $row['config_name'] ?? '' ) !== 'template_config' || empty( $row['config'] ) ) {
				continue;
			}
			$cfg = json_decode( (string) $row['config'], true );
			if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $cfg ) ) {
				continue;
			}
			$row_wc  = isset( $cfg['category_mappings']['wc_category'] ) ? (string) $cfg['category_mappings']['wc_category'] : '';
			$row_amz = isset( $cfg['category_mappings']['amazon_category'] ) ? (string) $cfg['category_mappings']['amazon_category'] : ( isset( $cfg['selected_category'] ) ? (string) $cfg['selected_category'] : '' );
			if ( $row_wc === $wc_category && $row_amz === $amazon_category ) {
				$target_uuid = (string) ( $row['uuid'] ?? '' );
				break;
			}
		}

		$cfg               = $payload;
		$cfg['updated_at'] = gmdate( 'c' );
		if ( $target_uuid ) {
			$this->product_mapping_model->update_mapping_by_uuid( $target_uuid, array( 'config' => wp_json_encode( $cfg ) ) );
			return $target_uuid;
		}

		// Plan-specific behavior via filter: free -> keep only one row (overwrite); premium -> allow multiple.
		$allow_multiple = (bool) apply_filters( 'momcs_allow_multiple_templates', false );
		if ( ! $allow_multiple ) {
			foreach ( $rows as $row ) {
				if ( ( $row['config_name'] ?? '' ) === 'template_config' && ! empty( $row['uuid'] ) ) {
					$this->product_mapping_model->delete_mapping_by_uuid( (string) $row['uuid'] );
				}
			}
		}

		$new_uuid = wp_generate_uuid4();
		$data     = array(
			'uuid'        => $new_uuid,
			'channel_id'  => $channel_id,
			'config_name' => 'template_config',
			'config'      => wp_json_encode( $cfg ),
			'created_at'  => current_time( 'mysql' ),
			'updated_at'  => current_time( 'mysql' ),
		);
		$this->product_mapping_model->add_mapping( $data );

		return $new_uuid;
	}

	/**
	 * Persist or update a template_config row keyed by WC category (one per category).
	 *
	 * @param string $channel_id Channel UUID.
	 * @param array  $payload    Template config payload including category_mappings.wc_category.
	 * @return void
	 * @throws \RuntimeException On channel not found.
	 */
	private function persist_template_config_by_wc_category( string $channel_id, array $payload ): void {
		$rows = $this->product_mapping_model->get_mappings_by_channel_id( $channel_id );
		$target_uuid = '';
		$wc_category = (string) ( $payload['category_mappings']['wc_category'] ?? '' );
		foreach ( $rows as $row ) {
			if ( ( $row['config_name'] ?? '' ) !== 'template_config' || empty( $row['config'] ) ) {
				continue;
			}
			$cfg = json_decode( (string) $row['config'], true );
			if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $cfg ) ) {
				continue;
			}
			if ( isset( $cfg['category_mappings']['wc_category'] ) && (string) $cfg['category_mappings']['wc_category'] === $wc_category ) {
				$target_uuid = (string) ( $row['uuid'] ?? '' );
				break;
			}
		}

		$cfg               = $payload;
		$cfg['updated_at'] = gmdate( 'c' );
		if ( $target_uuid ) {
			$this->product_mapping_model->update_mapping_by_uuid( $target_uuid, array( 'config' => wp_json_encode( $cfg ) ) );
			return;
		}

		// Create new row.
		$data = array(
			'uuid'        => wp_generate_uuid4(),
			'channel_id'  => $channel_id,
			'config_name' => 'template_config',
			'config'      => wp_json_encode( $cfg ),
			'created_at'  => current_time( 'mysql' ),
			'updated_at'  => current_time( 'mysql' ),
		);
		$this->product_mapping_model->add_mapping( $data );
	}

	/**
	 * Decode a JSON string into an array.
	 *
	 * @param string $json JSON string.
	 *
	 * @return array<string,mixed> Decoded array.
	 */
	private function maybe_decode_json( string $json ): array {
		if ( '' === $json ) {
			return array();
		}
		$arr = json_decode( $json, true );
		if ( JSON_ERROR_NONE !== json_last_error() ) {
			return array();
		}
		if ( ! is_array( $arr ) ) {
			return array();
		}
		return $arr;
	}

	/**
	 * Get the channel ID by marketplace.
	 *
	 * @param string $marketplace Marketplace slug.
	 *
	 * @return string|null Channel ID.
	 */
	private function get_channel_id_by_marketplace( string $marketplace ): ?string {
		// Fetch the most recent active channel for the marketplace.
		$channels = $this->multichannels_model->get_channels_by_marketplace( $marketplace );
		if ( empty( $channels ) ) {
			return null;
		}
		// get_channels_by_marketplace returns rows ordered DESC by id, so first is latest.
		return $channels[0]['uuid'];
	}

	/**
	 * Create WooCommerce global attributes for new attribute mappings.
	 *
	 * @param array $attributes_data Array of Amazon field => attribute data.
	 *
	 * @return void
	 * @throws \RuntimeException When attribute creation fails.
	 */
	private function create_woocommerce_attributes( array $attributes_data ): void {
		foreach ( $attributes_data as $amazon_field => $attr_data ) {
			$attribute_name = $attr_data['name'] ?? '';
			$attribute_slug = $attr_data['slug'] ?? '';

			if ( empty( $attribute_name ) || empty( $attribute_slug ) ) {
				continue;
			}

			$sanitized_slug = $this->normalize_new_attribute_slug( (string) $attribute_slug );
			if ( '' === $sanitized_slug ) {
				continue;
			}

			// Check if attribute already exists.
			if ( taxonomy_exists( 'pa_' . $sanitized_slug ) ) {
				continue;
			}

			// Check if WooCommerce functions are available.
			if ( ! function_exists( 'wc_create_attribute' ) ) {
				throw new \RuntimeException( 'WooCommerce functions not available' );
			}

			// Create the attribute.
			$attribute_id = wc_create_attribute(
				array(
					'name'         => $attribute_name,
					'slug'         => $sanitized_slug,
					'type'         => 'text',
					'order_by'     => 'menu_order',
					'has_archives' => false,
				)
			);

			if ( is_wp_error( $attribute_id ) ) {
				throw new \RuntimeException( 'Failed to create attribute: ' . esc_html( $attribute_id->get_error_message() ) );
			}

			// Register the taxonomy for this attribute.
			register_taxonomy(
				'pa_' . $sanitized_slug,
				'product',
				array(
					'labels'       => array(
						'name' => $attribute_name,
					),
					'hierarchical' => false,
					'public'       => false,
					'show_ui'      => false,
					'query_var'    => true,
					'rewrite'      => false,
				)
			);

			$this->logger->info( "Created WooCommerce attribute: {$attribute_name} (pa_{$sanitized_slug})" );
		}
	}

	/**
	 * Normalize a new attribute slug provided by the mapping UI.
	 *
	 * Uses the frontend-provided slug as the source of truth so the created
	 * WooCommerce attribute matches the taxonomy saved in field mappings.
	 *
	 * @param string $attribute_slug Raw attribute slug from the mapping request.
	 * @return string Normalized WooCommerce attribute slug without the `pa_` prefix.
	 */
	private function normalize_new_attribute_slug( string $attribute_slug ): string {
		$normalized_slug = sanitize_title( $attribute_slug );
		if ( 0 !== strpos( $normalized_slug, 'momcs-' ) ) {
			$normalized_slug = 'momcs-' . ltrim( $normalized_slug, '-' );
		}

		if ( strlen( $normalized_slug ) > 28 ) {
			$normalized_slug = substr( $normalized_slug, 0, 28 );
		}

		return trim( $normalized_slug, '-' );
	}

	/**
	 * Sanitize keys of field mappings from the unified collection approach.
	 *
	 * New unified approach supports:
	 * 1. Simple mappings: "field_name" => "wc_field_value"
	 * 2. Multi-level hierarchical mappings: "parent_field" => {"child_field" => {"nested_field" => "wc_field_value"}}
	 *
	 * @param array<string,mixed> $field_mappings Raw field mappings from unified client collection.
	 * @return array<string,mixed> Sanitized mapping ready for storage.
	 */
	private function sanitize_field_mapping_keys( array $field_mappings ): array {
		$clean = $this->sanitize_nested_mappings( $field_mappings );
		return $clean;
	}

	/**
	 * Normalize a mapping value that references a WC product attribute taxonomy (pa_*).
	 * Removes stray leading/trailing hyphens from the slug segment so stored mappings
	 * match slugs created via normalize_new_attribute_slug() (e.g. after JS truncation).
	 *
	 * @param string $val Sanitized leaf value.
	 * @return string
	 */
	private function normalize_mapping_taxonomy_value( string $val ): string {
		if ( 0 !== strpos( $val, 'pa_' ) ) {
			return $val;
		}
		$slug = substr( $val, 3 );
		$slug = trim( $slug, '-' );
		if ( '' === $slug ) {
			return $val;
		}
		return 'pa_' . $slug;
	}

	/**
	 * Recursively sanitize nested field mappings with unlimited depth support.
	 *
	 * @param array<string,mixed> $mappings Raw field mappings.
	 * @return array<string,mixed> Sanitized mappings.
	 */
	private function sanitize_nested_mappings( array $mappings ): array {
		$clean = array();

		foreach ( $mappings as $key => $val ) {
			$sanitized_key = sanitize_key( $key );

			if ( is_array( $val ) && ! is_numeric( key( $val ) ) ) {
				// Handle nested structure - recurse deeper.
				$clean_children = $this->sanitize_nested_mappings( $val );
				if ( ! empty( $clean_children ) ) {
					$clean[ $sanitized_key ] = $clean_children;
				}
			} elseif ( is_string( $val ) && ! empty( $val ) ) {
				// Handle leaf field mapping.
				$clean[ $sanitized_key ] = $this->normalize_mapping_taxonomy_value( sanitize_text_field( $val ) );
			}
		}

		return $clean;
	}

	/**
	 * Resolve a stored attribute slug to the actual WooCommerce taxonomy slug.
	 *
	 * When a new attribute is created, the plugin truncates the slug to 21 chars (suffix)
	 * so the full slug fits WC's 28-char limit. If the stored value was derived elsewhere
	 * with a different truncation (e.g. one extra character like 'o'), it won't match any
	 * WC attribute. This method maps such stored slugs to the actual WC slug so the UI
	 * can pre-select the correct option (e.g. pa_momcs-item-length-descriptio → pa_momcs-item-length-descripti).
	 *
	 * @param string $stored_slug    Slug as stored in the database (e.g. pa_momcs-item-length-descriptio).
	 * @param array  $valid_wc_slugs List of valid WC attribute slugs (e.g. from get_wc_product_attributes keys).
	 * @return string The slug to use for display/selection (stored if valid, else best match).
	 */
	private function resolve_attribute_slug_for_display( string $stored_slug, array $valid_wc_slugs ): string {
		if ( in_array( $stored_slug, $valid_wc_slugs, true ) ) {
			return $stored_slug;
		}
		// Only resolve pa_* taxonomy slugs.
		if ( strpos( $stored_slug, 'pa_' ) !== 0 ) {
			return $stored_slug;
		}
		// Stored slug may be one character longer (e.g. truncation mismatch: descriptio vs descripti).
		$without_last_char = substr( $stored_slug, 0, -1 );
		if ( in_array( $without_last_char, $valid_wc_slugs, true ) ) {
			return $without_last_char;
		}
		// Check if any valid slug is the stored slug minus one character (other positions).
		foreach ( $valid_wc_slugs as $valid ) {
			if ( strpos( $valid, 'pa_' ) !== 0 ) {
				continue;
			}
			// Same prefix: stored could be valid + one extra char at end.
			if ( strlen( $stored_slug ) === strlen( $valid ) + 1 && strpos( $stored_slug, $valid ) === 0 ) {
				return $valid;
			}
		}
		return $stored_slug;
	}

	/**
	 * Recursively normalize pa_* slugs in field mappings to actual WooCommerce attribute slugs.
	 *
	 * Ensures stored slugs that differ by truncation (e.g. extra 'o') are resolved so the
	 * mapping UI pre-selects the correct WC attribute on load.
	 *
	 * @param array<string,mixed> $field_mappings Raw field mappings from storage.
	 * @param array               $valid_slugs   Valid WC attribute slugs (option keys).
	 * @return array<string,mixed> Field mappings with slugs resolved for display.
	 */
	private function normalize_field_mapping_slugs( array $field_mappings, array $valid_slugs ): array {
		$out = array();
		foreach ( $field_mappings as $key => $val ) {
			if ( is_array( $val ) && ! is_numeric( key( $val ) ) ) {
				$out[ $key ] = $this->normalize_field_mapping_slugs( $val, $valid_slugs );
			} elseif ( is_array( $val ) && isset( $val['value'] ) && is_string( $val['value'] ) ) {
				$resolved = $this->resolve_attribute_slug_for_display( $val['value'], $valid_slugs );
				$out[ $key ] = array( 'value' => $resolved );
			} elseif ( is_string( $val ) ) {
				$out[ $key ] = $this->resolve_attribute_slug_for_display( $val, $valid_slugs );
			} else {
				$out[ $key ] = $val;
			}
		}
		return $out;
	}

	/**
	 * List all template_config rows for a channel with minimal metadata.
	 * Auto-assign label if missing (free → premium upgrade path).
	 *
	 * @param string $channel_id Channel UUID.
	 * @return array<int,array<string,mixed>>
	 */
	public function list_templates( string $channel_id ): array {
		$rows      = $this->product_mapping_model->get_mappings_by_channel_id( $channel_id );
		$templates = array();

		// Build a set of existing labels to ensure uniqueness during auto-labeling.
		$existing_labels = array();
		foreach ( $rows as $row ) {
			if ( isset( $row['config_name'] ) && 'template_config' === $row['config_name'] ) {
				$cfg = ! empty( $row['config'] ) ? json_decode( (string) $row['config'], true ) : array();
				if ( isset( $cfg['label'] ) && '' !== trim( (string) $cfg['label'] ) ) {
					$existing_labels[ strtolower( (string) $cfg['label'] ) ] = true;
				}
			}
		}

		foreach ( $rows as $row ) {
			if ( empty( $row['config_name'] ) || 'template_config' !== $row['config_name'] ) {
				continue;
			}
			$uuid = (string) ( $row['uuid'] ?? '' );
			$cfg  = ! empty( $row['config'] ) ? json_decode( (string) $row['config'], true ) : array();
			if ( ! is_array( $cfg ) ) {
				$cfg = array();
			}

			$label_before = isset( $cfg['label'] ) ? (string) $cfg['label'] : '';
			if ( '' === trim( $label_before ) ) {
				$auto               = $this->generate_auto_label( $channel_id, $cfg, $existing_labels );
				$cfg['label']       = $auto;
				$cfg['updated_at']  = gmdate( 'c' );
				$this->product_mapping_model->update_mapping_by_uuid( $uuid, array( 'config' => wp_json_encode( $cfg ) ) );
				$existing_labels[ strtolower( $auto ) ] = true;
			}

			$templates[] = array(
				'uuid'              => $uuid,
				'label'             => $cfg['label'] ?? '',
				'selected_category' => $cfg['selected_category'] ?? '',
				'category_mappings' => $cfg['category_mappings'] ?? array(),
				'updated_at'        => $cfg['updated_at'] ?? ( $row['updated_at'] ?? '' ),
			);
		}

		usort(
			$templates,
			static function ( $a, $b ) {
				return strcmp( $b['updated_at'] ?? '', $a['updated_at'] ?? '' );
			}
		);

		return $templates;
	}

	/**
	 * Count template_config rows for a channel.
	 *
	 * @param string $channel_id Channel UUID.
	 * @return int Count of templates.
	 */
	public function count_templates( string $channel_id ): int {
		$rows = $this->product_mapping_model->get_mappings_by_channel_id( $channel_id );
		$n    = 0;
		foreach ( $rows as $row ) {
			if ( isset( $row['config_name'] ) && 'template_config' === $row['config_name'] ) {
				++$n;
			}
		}
		return $n;
	}

	/**
	 * Create a new template_config row.
	 *
	 * @param string $channel_id Channel UUID.
	 * @param string $label      Template label.
	 * @param array  $payload    Template config payload.
	 * @return string            New template UUID.
	 */
	public function create_template( string $channel_id, string $label, array $payload ): string {
		$uuid = wp_generate_uuid4();
		$cfg  = $payload;
		$cfg['label']      = $label;
		$cfg['updated_at'] = gmdate( 'c' );

		$data = array(
			'uuid'        => $uuid,
			'channel_id'  => $channel_id,
			'config_name' => 'template_config',
			'config'      => wp_json_encode( $cfg ),
			'created_at'  => current_time( 'mysql' ),
			'updated_at'  => current_time( 'mysql' ),
		);
		$ok = $this->product_mapping_model->add_mapping( $data );
		if ( false === $ok ) {
			throw new \RuntimeException( 'Failed to create template.' );
		}
		return $uuid;
	}

	/**
	 * Update an existing template by UUID.
	 *
	 * @param string               $channel_id    Channel UUID.
	 * @param string               $template_uuid Template UUID.
	 * @param array<string,mixed>  $payload       Template payload.
	 *
	 * @throws \RuntimeException When template not found or save fails.
	 * @return void
	 */
	public function update_template( string $channel_id, string $template_uuid, array $payload ): void {
		$row = $this->product_mapping_model->get_mapping_by_uuid( $template_uuid );
		if ( ! $row || ( $row['channel_id'] ?? '' ) !== $channel_id ) {
			throw new \RuntimeException( 'Template not found.' );
		}
		$cfg               = is_array( $payload ) ? $payload : array();
		$cfg['updated_at'] = gmdate( 'c' );
		$ok                = $this->product_mapping_model->update_mapping_by_uuid( $template_uuid, array( 'config' => wp_json_encode( $cfg ) ) );
		if ( false === $ok ) {
			throw new \RuntimeException( 'Failed to update template.' );
		}
	}

	/**
	 * Delete template by UUID.
	 *
	 * @param string $channel_id    Channel UUID.
	 * @param string $template_uuid Template UUID.
	 *
	 * @throws \RuntimeException When template not found or delete fails.
	 * @return void
	 */
	public function delete_template( string $channel_id, string $template_uuid ): void {
		$row = $this->product_mapping_model->get_mapping_by_uuid( $template_uuid );
		if ( ! $row || ( $row['channel_id'] ?? '' ) !== $channel_id ) {
			throw new \RuntimeException( 'Template not found.' );
		}
		$ok = $this->product_mapping_model->delete_mapping_by_uuid( $template_uuid );
		if ( false === $ok ) {
			throw new \RuntimeException( 'Failed to delete template.' );
		}
	}

	/**
	 * Clone template by UUID with a new label.
	 *
	 * @param string $channel_id    Channel UUID.
	 * @param string $template_uuid Template UUID to clone.
	 * @param string $new_label     Label for the new template.
	 *
	 * @throws \RuntimeException When template not found or create fails.
	 * @return string New template UUID.
	 */
	public function clone_template( string $channel_id, string $template_uuid, string $new_label ): string {
		$row = $this->product_mapping_model->get_mapping_by_uuid( $template_uuid );
		if ( ! $row || ( $row['channel_id'] ?? '' ) !== $channel_id ) {
			throw new \RuntimeException( 'Template not found.' );
		}
		$cfg          = ! empty( $row['config'] ) ? json_decode( (string) $row['config'], true ) : array();
		$cfg['label'] = $new_label;
		unset( $cfg['uuid'] );
		return $this->create_template( $channel_id, $new_label, $cfg );
	}

	/**
	 * Get unified mapping dataset for a specific template UUID.
	 *
	 * @param string $template_uuid Template UUID.
	 * @return array<string,mixed>
	 */
	public function get_unified_mapping_by_uuid( string $template_uuid ): array {
		$row = $this->product_mapping_model->get_mapping_by_uuid( $template_uuid );
		if ( ! $row || 'template_config' !== ( $row['config_name'] ?? '' ) ) {
			return array(
				'default_mappings'  => array(),
				'category_mappings' => array(),
				'selected_category' => '',
				'field_mappings'    => array(),
			);
		}
		$cfg = ! empty( $row['config'] ) ? json_decode( (string) $row['config'], true ) : array();
		return array(
			'default_mappings'  => array(),
			'category_mappings' => $cfg['category_mappings'] ?? array(),
			'selected_category' => $cfg['selected_category'] ?? '',
			'field_mappings'    => $cfg['field_mappings'] ?? array(),
		);
	}

	/**
	 * Generate an auto label for unlabeled template (free → premium upgrade path).
	 * Ensures uniqueness using provided $existing lowercased labels set.
	 *
	 * @param string              $channel_id Channel UUID.
	 * @param array<string,mixed> $cfg        Template config payload.
	 * @param array<string,bool>  $existing   Existing labels map.
	 * @return string
	 */
	private function generate_auto_label( string $channel_id, array $cfg, array $existing ): string {
		$base = '';
		if ( ! empty( $cfg['selected_category'] ) ) {
			$base = sprintf( /* translators: %s: category */ __( '%s (from Free)', 'multi-channel-for-woocommerce' ), (string) $cfg['selected_category'] );
		} elseif ( ! empty( $cfg['category_mappings']['wc_category'] ) ) {
			$base = __( 'Template', 'multi-channel-for-woocommerce' );
		} else {
			$base = __( 'Template', 'multi-channel-for-woocommerce' );
		}

		$label = $base;
		$i     = 1;
		while ( isset( $existing[ strtolower( $label ) ] ) ) {
			++$i;
			$label = $base . ' ' . $i;
		}
		return $label;
	}

	/**
	 * Validate parent category mapping for potential conflicts and issues.
	 *
	 * @param string $marketplace        Marketplace slug.
	 * @param string $wc_category        WooCommerce category ID.
	 * @param string $amazon_category    Amazon category.
	 * @param string $channel_id         Channel UUID (required for multi-channel support).
	 *
	 * @return array Validation result with warnings and suggestions.
	 */
	public function validate_parent_category_mapping( string $marketplace, string $wc_category, string $amazon_category, string $channel_id ): array {
		$result = array(
			'is_parent'         => false,
			'child_count'       => 0,
			'children'          => array(),
			'conflicts'         => array(),
			'recommendations'   => array(),
			'severity'          => 'info', // info, warning, error.
		);

		// Check if this is a parent category.
		$category = get_term( (int) $wc_category, 'product_cat' );
		if ( ! $category || is_wp_error( $category ) ) {
			return $result;
		}

		// Validate that the provided channel_id exists and matches the marketplace.
		$channel_data = $this->multichannels_model->get_channel_by_uuid( $channel_id );
		if ( ! $channel_data ) {
			throw new \RuntimeException( 'Invalid channel ID provided.' );
		}
		if ( strtolower( $channel_data['marketplace'] ?? '' ) !== strtolower( $marketplace ) ) {
			throw new \RuntimeException( 'Channel ID does not match the specified marketplace.' );
		}

		// Get child categories.
		$children = get_terms(
			array(
				'taxonomy'   => 'product_cat',
				'parent'     => (int) $wc_category,
				'hide_empty' => false,
				'fields'     => 'all',
			)
		);

		if ( ! empty( $children ) && ! is_wp_error( $children ) ) {
			$result['is_parent']   = true;
			$result['child_count'] = count( $children );
			$result['children']    = array_slice( wp_list_pluck( $children, 'name' ), 0, 10 );

			// Check for existing child category templates.
			$conflicts = $this->find_child_category_conflicts( $channel_id, $children );
			$result['conflicts'] = $conflicts;

			if ( ! empty( $conflicts ) ) {
				$result['severity'] = 'warning';
				$result['recommendations'][] = 'Consider keeping the existing child category templates instead of overriding with a parent template.';
			}

			// Add general recommendations for parent mapping.
			if ( count( $children ) > 5 ) {
				$result['recommendations'][] = sprintf( 'This parent category has %d subcategories. Parent mapping will apply the same Amazon category (%s) to all of them.', count( $children ), $amazon_category );
			}

			// Check for potentially incompatible Amazon product types.
			$incompatible_warning = $this->check_amazon_product_type_compatibility( $amazon_category, $children );
			if ( $incompatible_warning ) {
				$result['recommendations'][] = $incompatible_warning;
				$result['severity'] = 'warning';
			}
		}

		return $result;
	}

	/**
	 * Find existing child category templates that would conflict with parent mapping.
	 *
	 * @param string $channel_id Channel UUID.
	 * @param array  $children   Child category terms.
	 *
	 * @return array Array of conflicting templates.
	 */
	private function find_child_category_conflicts( string $channel_id, array $children ): array {
		$conflicts    = array();
		$child_ids    = wp_list_pluck( $children, 'term_id' );
		$mappings     = $this->product_mapping_model->get_mappings_by_channel_id( $channel_id );

		foreach ( $mappings as $mapping ) {
			if ( ( $mapping['config_name'] ?? '' ) !== 'template_config' || empty( $mapping['config'] ) ) {
				continue;
			}
			$config = json_decode( (string) $mapping['config'], true );
			if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $config ) ) {
				continue;
			}
			if ( isset( $config['category_mappings']['wc_category'] ) ) {
				$template_category = (int) $config['category_mappings']['wc_category'];
				if ( in_array( $template_category, $child_ids, true ) ) {
					$category = get_term( $template_category, 'product_cat' );
					if ( $category && ! is_wp_error( $category ) ) {
						$conflicts[] = array(
							'category_name'   => $category->name,
							'amazon_category' => $config['selected_category'] ?? $config['category_mappings']['amazon_category'] ?? '',
							'uuid'            => $mapping['uuid'] ?? '',
						);
					}
				}
			}
		}

		return $conflicts;
	}

	/**
	 * Check if Amazon product type might be incompatible with diverse child categories.
	 *
	 * @param string $amazon_category Amazon product type.
	 * @param array  $children        Child category terms.
	 *
	 * @return string|null Warning message or null if no issues detected.
	 */
	private function check_amazon_product_type_compatibility( string $amazon_category, array $children ): ?string {
		// Define potentially problematic parent-child combinations.
		$problematic_types = array(
			'WIRELESS_PHONE' => array( 'laptop', 'tablet', 'headphone', 'accessory' ),
			'SHIRT'          => array( 'pant', 'shoe', 'bag', 'accessory' ),
			'BEAUTY'         => array( 'supplement', 'medical', 'food' ),
			'ABIS_BOOK'      => array( 'ebook', 'audiobook', 'magazine' ),
		);

		$child_names = wp_list_pluck( $children, 'name' );
		$child_slugs = wp_list_pluck( $children, 'slug' );
		$all_child_text = implode( ' ', array_merge( $child_names, $child_slugs ) );
		$all_child_text = strtolower( $all_child_text );

		if ( isset( $problematic_types[ $amazon_category ] ) ) {
			foreach ( $problematic_types[ $amazon_category ] as $problematic_term ) {
				if ( strpos( $all_child_text, $problematic_term ) !== false ) {
					return sprintf( 'Warning: Amazon product type "%s" may not be suitable for all subcategories. Some subcategories might need different Amazon product types.', $amazon_category );
				}
			}
		}

		return null;
	}
}
