<?php
/**
 * Field registry: defines which product attributes can be used in rules,
 * their value types, available operators, and how to resolve a product's value.
 *
 * @package SmartCategories
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class SCWC_Fields
 */
class SCWC_Fields {

	/* ---------------------------------------------------------------------
	 * Operator sets per value type.
	 * ------------------------------------------------------------------- */

	/**
	 * Per-request cache of the built registry and key→def map.
	 *
	 * @var array|null
	 */
	private static $cache_all = null;

	/**
	 * Per-request key→definition lookup map.
	 *
	 * @var array|null
	 */
	private static $cache_map = null;

	const OPS_TEXT    = array( 'equals', 'not_equals', 'contains', 'not_contains', 'starts_with', 'not_starts_with', 'ends_with', 'not_ends_with', 'matches_pattern', 'not_matches_pattern', 'regex', 'not_regex', 'is_empty', 'is_not_empty' );
	const OPS_NUMBER  = array( 'equals', 'not_equals', 'gt', 'lt', 'gte', 'lte', 'between', 'not_between' );
	const OPS_DATE    = array( 'before', 'after', 'on', 'between', 'in_last_days', 'in_last_hours', 'in_next_days', 'on_day_of_week', 'in_month', 'on_day_of_month' );
	const OPS_TAX     = array( 'in', 'contains_all', 'not_in', 'in_or_under', 'exists', 'not_exists' );
	const OPS_BOOL    = array( 'is_true', 'is_false' );
	const OPS_SELECT  = array( 'equals', 'not_equals', 'in', 'not_in' );

	/**
	 * Human labels for every operator (localised).
	 *
	 * @return array<string,string>
	 */
	public static function operator_labels() {
		return array(
			'equals'       => __( 'is equal to', 'smart-categories-for-woocommerce' ),
			'not_equals'   => __( 'is not equal to', 'smart-categories-for-woocommerce' ),
			'contains'     => __( 'contains', 'smart-categories-for-woocommerce' ),
			'not_contains' => __( 'does not contain', 'smart-categories-for-woocommerce' ),
			'starts_with'  => __( 'starts with', 'smart-categories-for-woocommerce' ),
			'not_starts_with' => __( 'does not start with', 'smart-categories-for-woocommerce' ),
			'ends_with'    => __( 'ends with', 'smart-categories-for-woocommerce' ),
			'not_ends_with' => __( 'does not end with', 'smart-categories-for-woocommerce' ),
			'matches_pattern' => __( 'matches pattern (wildcards * ?)', 'smart-categories-for-woocommerce' ),
			'not_matches_pattern' => __( 'does not match pattern', 'smart-categories-for-woocommerce' ),
			'regex'        => __( 'matches regex', 'smart-categories-for-woocommerce' ),
			'not_regex'    => __( 'does not match regex', 'smart-categories-for-woocommerce' ),
			'is_empty'     => __( 'is empty', 'smart-categories-for-woocommerce' ),
			'is_not_empty' => __( 'is not empty', 'smart-categories-for-woocommerce' ),
			'gt'           => __( 'is greater than', 'smart-categories-for-woocommerce' ),
			'lt'           => __( 'is less than', 'smart-categories-for-woocommerce' ),
			'gte'          => __( 'is greater than or equal to', 'smart-categories-for-woocommerce' ),
			'lte'          => __( 'is less than or equal to', 'smart-categories-for-woocommerce' ),
			'between'      => __( 'is between', 'smart-categories-for-woocommerce' ),
			'not_between'  => __( 'is not between', 'smart-categories-for-woocommerce' ),
			'before'       => __( 'is before', 'smart-categories-for-woocommerce' ),
			'after'        => __( 'is after', 'smart-categories-for-woocommerce' ),
			'on'           => __( 'is on', 'smart-categories-for-woocommerce' ),
			'in_last_days' => __( 'in the last N days', 'smart-categories-for-woocommerce' ),
			'in_last_hours' => __( 'in the last N hours', 'smart-categories-for-woocommerce' ),
			'in_next_days' => __( 'in the next N days', 'smart-categories-for-woocommerce' ),
			'on_day_of_week' => __( 'day of week is', 'smart-categories-for-woocommerce' ),
			'in_month'     => __( 'month is', 'smart-categories-for-woocommerce' ),
			'on_day_of_month' => __( 'day of month is', 'smart-categories-for-woocommerce' ),
			'in'           => __( 'is any of', 'smart-categories-for-woocommerce' ),
			'contains_all' => __( 'contains all of', 'smart-categories-for-woocommerce' ),
			'not_in'       => __( 'is none of', 'smart-categories-for-woocommerce' ),
			'in_or_under'  => __( 'is (or under)', 'smart-categories-for-woocommerce' ),
			'exists'       => __( 'exists', 'smart-categories-for-woocommerce' ),
			'not_exists'   => __( 'does not exist', 'smart-categories-for-woocommerce' ),
			'is_true'      => __( 'yes', 'smart-categories-for-woocommerce' ),
			'is_false'     => __( 'no', 'smart-categories-for-woocommerce' ),
		);
	}

	/**
	 * Field group definitions (used to organise the field dropdown).
	 *
	 * @return array<int,array{key:string,label:string}>
	 */
	public static function groups() {
		return array(
			array(
				'key'   => 'general',
				'label' => __( 'General', 'smart-categories-for-woocommerce' ),
			),
			array(
				'key'   => 'price_stock',
				'label' => __( 'Price & Stock', 'smart-categories-for-woocommerce' ),
			),
			array(
				'key'   => 'performance',
				'label' => __( 'Performance', 'smart-categories-for-woocommerce' ),
			),
			array(
				'key'   => 'taxonomy',
				'label' => __( 'Taxonomy', 'smart-categories-for-woocommerce' ),
			),
			array(
				'key'   => 'shipping',
				'label' => __( 'Shipping & Dimensions', 'smart-categories-for-woocommerce' ),
			),
			array(
				'key'   => 'meta',
				'label' => __( 'Custom Fields', 'smart-categories-for-woocommerce' ),
			),
		);
	}

	/**
	 * The full field registry. Each entry:
	 *   key       string  unique field identifier ("price", "tax:product_tag", "meta:")
	 *   label     string  human label
	 *   group     string  group key
	 *   type      string  text|number|date|taxonomy|boolean|select
	 *   operators string[] operator keys valid for this field
	 *   taxonomy  string  (taxonomy fields) taxonomy name to load term options from
	 *   choices   array   (select fields) fixed value/label choices
	 *   custom_key bool   (meta) user supplies the meta key in the value
	 *
	 * @return array<int,array<string,mixed>>
	 */
	public static function all() {
		if ( null !== self::$cache_all ) {
			return self::$cache_all;
		}

		$fields = array();

		/* ---- General ---- */
		$fields[] = self::def( 'title', __( 'Product name', 'smart-categories-for-woocommerce' ), 'general', 'text', self::OPS_TEXT );
		$fields[] = self::def(
			'anywhere',
			__( 'Anywhere (name, description, SKU…)', 'smart-categories-for-woocommerce' ),
			'general',
			'text',
			array( 'contains', 'not_contains' )
		);
		$fields[] = self::def( 'sku', __( 'SKU', 'smart-categories-for-woocommerce' ), 'general', 'text', self::OPS_TEXT );
		$fields[] = self::def(
			'product_type',
			__( 'Product type', 'smart-categories-for-woocommerce' ),
			'general',
			'select',
			self::OPS_SELECT,
			array( 'choices' => self::product_type_choices() )
		);
		$fields[] = self::def( 'featured', __( 'Featured product', 'smart-categories-for-woocommerce' ), 'general', 'boolean', self::OPS_BOOL );
		$fields[] = self::def( 'date_created', __( 'Date created', 'smart-categories-for-woocommerce' ), 'general', 'date', self::OPS_DATE );
		$fields[] = self::def( 'date_modified', __( 'Date modified', 'smart-categories-for-woocommerce' ), 'general', 'date', self::OPS_DATE );
		$fields[] = self::def( 'barcode', __( 'Barcode / GTIN', 'smart-categories-for-woocommerce' ), 'general', 'text', self::OPS_TEXT );
		$fields[] = self::def( 'has_image', __( 'Has image', 'smart-categories-for-woocommerce' ), 'general', 'boolean', self::OPS_BOOL );
		$fields[] = self::def( 'image_count', __( 'Image count', 'smart-categories-for-woocommerce' ), 'general', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'variation_count', __( 'Variation count', 'smart-categories-for-woocommerce' ), 'general', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'is_virtual', __( 'Virtual product', 'smart-categories-for-woocommerce' ), 'general', 'boolean', self::OPS_BOOL );
		$fields[] = self::def( 'is_downloadable', __( 'Downloadable product', 'smart-categories-for-woocommerce' ), 'general', 'boolean', self::OPS_BOOL );

		/* ---- Price & stock ---- */
		$fields[] = self::def( 'price', __( 'Active price', 'smart-categories-for-woocommerce' ), 'price_stock', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'regular_price', __( 'Regular price', 'smart-categories-for-woocommerce' ), 'price_stock', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'sale_price', __( 'Sale price', 'smart-categories-for-woocommerce' ), 'price_stock', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'on_sale', __( 'On sale', 'smart-categories-for-woocommerce' ), 'price_stock', 'boolean', self::OPS_BOOL );
		$fields[] = self::def(
			'stock_status',
			__( 'Stock status', 'smart-categories-for-woocommerce' ),
			'price_stock',
			'select',
			self::OPS_SELECT,
			array( 'choices' => self::stock_status_choices() )
		);
		$fields[] = self::def( 'stock_quantity', __( 'Stock quantity', 'smart-categories-for-woocommerce' ), 'price_stock', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'discount_percentage', __( 'Discount percentage (%)', 'smart-categories-for-woocommerce' ), 'price_stock', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'cost', __( 'Cost', 'smart-categories-for-woocommerce' ), 'price_stock', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'profit_margin', __( 'Profit margin (%)', 'smart-categories-for-woocommerce' ), 'price_stock', 'number', self::OPS_NUMBER );

		/* ---- Performance ---- */
		$fields[] = self::def( 'total_sales', __( 'Total sales count', 'smart-categories-for-woocommerce' ), 'performance', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'average_rating', __( 'Average rating', 'smart-categories-for-woocommerce' ), 'performance', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'rating_count', __( 'Review count', 'smart-categories-for-woocommerce' ), 'performance', 'number', self::OPS_NUMBER );

		/* ---- Taxonomy ---- */
		$fields[] = self::def(
			'tax:product_cat',
			__( 'Category', 'smart-categories-for-woocommerce' ),
			'taxonomy',
			'taxonomy',
			self::OPS_TAX,
			array( 'taxonomy' => 'product_cat' )
		);
		$fields[] = self::def(
			'tax:product_tag',
			__( 'Tag', 'smart-categories-for-woocommerce' ),
			'taxonomy',
			'taxonomy',
			self::OPS_TAX,
			array( 'taxonomy' => 'product_tag' )
		);

		// Brand taxonomy (WooCommerce 9.6+ core "product_brand", or popular brand plugins).
		foreach ( array( 'product_brand', 'pwb-brand', 'yith_product_brand' ) as $brand_tax ) {
			if ( taxonomy_exists( $brand_tax ) ) {
				$fields[] = self::def(
					'tax:' . $brand_tax,
					__( 'Brand', 'smart-categories-for-woocommerce' ),
					'taxonomy',
					'taxonomy',
					self::OPS_TAX,
					array( 'taxonomy' => $brand_tax )
				);
				break;
			}
		}

		// Global product attributes (pa_*).
		if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
			foreach ( wc_get_attribute_taxonomies() as $attribute ) {
				$tax = wc_attribute_taxonomy_name( $attribute->attribute_name );
				if ( ! taxonomy_exists( $tax ) ) {
					continue;
				}
				$fields[] = self::def(
					'tax:' . $tax,
					/* translators: %s: attribute label */
					sprintf( __( 'Attribute: %s', 'smart-categories-for-woocommerce' ), $attribute->attribute_label ),
					'taxonomy',
					'taxonomy',
					self::OPS_TAX,
					array( 'taxonomy' => $tax )
				);
			}
		}

		/* ---- Shipping & dimensions ---- */
		if ( taxonomy_exists( 'product_shipping_class' ) ) {
			$fields[] = self::def(
				'tax:product_shipping_class',
				__( 'Shipping class', 'smart-categories-for-woocommerce' ),
				'shipping',
				'taxonomy',
				self::OPS_TAX,
				array( 'taxonomy' => 'product_shipping_class' )
			);
		}
		$fields[] = self::def( 'weight', __( 'Weight', 'smart-categories-for-woocommerce' ), 'shipping', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'length', __( 'Length', 'smart-categories-for-woocommerce' ), 'shipping', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'width', __( 'Width', 'smart-categories-for-woocommerce' ), 'shipping', 'number', self::OPS_NUMBER );
		$fields[] = self::def( 'height', __( 'Height', 'smart-categories-for-woocommerce' ), 'shipping', 'number', self::OPS_NUMBER );

		/* ---- Custom meta ---- */
		$fields[] = self::def(
			'meta:',
			__( 'Custom field (meta)', 'smart-categories-for-woocommerce' ),
			'meta',
			'text',
			array_merge( self::OPS_TEXT, array( 'gt', 'lt', 'gte', 'lte' ) ),
			array( 'custom_key' => true )
		);

		/**
		 * Filter the registered fields so add-ons can extend the rule vocabulary.
		 *
		 * @param array $fields Field definitions.
		 */
		self::$cache_all = apply_filters( 'scwc_fields', $fields );
		return self::$cache_all;
	}

	/**
	 * Reset the per-request field cache (e.g. after attributes change in tests).
	 */
	public static function flush_cache() {
		self::$cache_all = null;
		self::$cache_map = null;
	}

	/**
	 * Build a single field definition array.
	 *
	 * @param string $key       Field key.
	 * @param string $label     Label.
	 * @param string $group     Group key.
	 * @param string $type      Value type.
	 * @param array  $operators Operator keys.
	 * @param array  $extra     Extra props (taxonomy, choices, custom_key).
	 * @return array
	 */
	private static function def( $key, $label, $group, $type, $operators, $extra = array() ) {
		return array_merge(
			array(
				'key'       => $key,
				'label'     => $label,
				'group'     => $group,
				'type'      => $type,
				'operators' => array_values( $operators ),
			),
			$extra
		);
	}

	/**
	 * Look up a single field definition by key.
	 *
	 * @param string $key Field key. For meta/tax dynamic keys, matches the prefix.
	 * @return array|null
	 */
	public static function get( $key ) {
		if ( null === self::$cache_map ) {
			self::$cache_map = array();
			foreach ( self::all() as $field ) {
				self::$cache_map[ $field['key'] ] = $field;
			}
		}

		if ( isset( self::$cache_map[ $key ] ) ) {
			return self::$cache_map[ $key ];
		}

		// Dynamic meta keys: "meta:my_key" maps to the generic "meta:" definition.
		if ( 0 === strpos( $key, 'meta:' ) && isset( self::$cache_map['meta:'] ) ) {
			return self::$cache_map['meta:'];
		}

		return null;
	}

	/**
	 * Product type choices.
	 *
	 * @return array<int,array{value:string,label:string}>
	 */
	private static function product_type_choices() {
		$types   = function_exists( 'wc_get_product_types' ) ? wc_get_product_types() : array();
		$choices = array();
		foreach ( $types as $value => $label ) {
			$choices[] = array(
				'value' => $value,
				'label' => $label,
			);
		}
		return $choices;
	}

	/**
	 * Stock status choices.
	 *
	 * @return array<int,array{value:string,label:string}>
	 */
	private static function stock_status_choices() {
		$statuses = function_exists( 'wc_get_product_stock_status_options' ) ? wc_get_product_stock_status_options() : array(
			'instock'     => __( 'In stock', 'smart-categories-for-woocommerce' ),
			'outofstock'  => __( 'Out of stock', 'smart-categories-for-woocommerce' ),
			'onbackorder' => __( 'On backorder', 'smart-categories-for-woocommerce' ),
		);
		$choices = array();
		foreach ( $statuses as $value => $label ) {
			$choices[] = array(
				'value' => $value,
				'label' => $label,
			);
		}
		return $choices;
	}

	/**
	 * Resolve a product's cost. Defaults to the common "Cost of Goods" meta key
	 * used by WooCommerce cost extensions; overridable via filter.
	 *
	 * @param WC_Product $product Product.
	 * @return float|null
	 */
	private static function cost_of( $product ) {
		$cost = $product->get_meta( '_wc_cog_cost', true );
		if ( '' === $cost || null === $cost ) {
			$cost = $product->get_meta( '_cost_price', true );
		}
		/**
		 * Filter the resolved product cost (for profit-margin / cost fields).
		 *
		 * @param mixed      $cost    Raw cost value.
		 * @param WC_Product $product Product.
		 */
		$cost = apply_filters( 'scwc_product_cost', $cost, $product );
		return ( '' === $cost || null === $cost ) ? null : (float) $cost;
	}


	/**
	 * Resolve a product's value for a given field key.
	 *
	 * @param WC_Product $product Product object.
	 * @param string     $field   Field key.
	 * @return mixed Scalar, array (taxonomy term IDs), bool, or null when unset.
	 */
	public static function get_value( $product, $field ) {
		// Taxonomy fields → array of term IDs.
		if ( 0 === strpos( $field, 'tax:' ) ) {
			$taxonomy = substr( $field, 4 );
			$ids      = wc_get_product_term_ids( $product->get_id(), $taxonomy );
			return is_array( $ids ) ? array_map( 'intval', $ids ) : array();
		}

		// Custom meta fields → raw meta value.
		if ( 0 === strpos( $field, 'meta:' ) ) {
			$meta_key = substr( $field, 5 );
			if ( '' === $meta_key ) {
				return null;
			}
			return $product->get_meta( $meta_key, true );
		}

		switch ( $field ) {
			case 'title':
				return $product->get_name();
			case 'anywhere':
				$parts    = array(
					$product->get_name(),
					$product->get_slug(),
					$product->get_sku(),
					$product->get_short_description(),
					$product->get_description(),
				);
				$haystack = trim( wp_strip_all_tags( implode( ' ', array_filter( $parts ) ) ) );
				/**
				 * Filter the searchable text for the "Her yer" (anywhere) field.
				 * Add category/tag/attribute names or custom meta here if desired.
				 *
				 * @param string     $haystack Concatenated searchable text.
				 * @param WC_Product $product  Product object.
				 */
				return apply_filters( 'scwc_anywhere_haystack', $haystack, $product );
			case 'sku':
				return $product->get_sku();
			case 'product_type':
				return $product->get_type();
			case 'featured':
				return (bool) $product->is_featured();
			case 'date_created':
				$date = $product->get_date_created();
				return $date ? $date->getTimestamp() : null;
			case 'date_modified':
				$dm = $product->get_date_modified();
				return $dm ? $dm->getTimestamp() : null;
			case 'barcode':
				if ( is_callable( array( $product, 'get_global_unique_id' ) ) ) {
					$gtin = $product->get_global_unique_id();
					if ( '' !== $gtin && null !== $gtin ) {
						return $gtin;
					}
				}
				$barcode = $product->get_meta( '_gtin', true );
				if ( '' === $barcode ) {
					$barcode = $product->get_meta( '_barcode', true );
				}
				/** Filter the product barcode/GTIN source. */
				return apply_filters( 'scwc_product_barcode', $barcode, $product );
			case 'has_image':
				return (bool) $product->get_image_id();
			case 'image_count':
				$gallery = $product->get_gallery_image_ids();
				return (float) ( ( $product->get_image_id() ? 1 : 0 ) + ( is_array( $gallery ) ? count( $gallery ) : 0 ) );
			case 'variation_count':
				return (float) ( $product->is_type( 'variable' ) ? count( $product->get_children() ) : 0 );
			case 'is_virtual':
				return (bool) $product->is_virtual();
			case 'is_downloadable':
				return (bool) $product->is_downloadable();
			case 'cost':
				return self::cost_of( $product );
			case 'discount_percentage':
				$reg = (float) $product->get_regular_price();
				$act = '' === $product->get_price() ? null : (float) $product->get_price();
				if ( $reg <= 0 || null === $act || $act >= $reg ) {
					return ( $reg > 0 && null !== $act ) ? 0.0 : null;
				}
				return ( ( $reg - $act ) / $reg ) * 100;
			case 'profit_margin':
				$cost  = self::cost_of( $product );
				$price = '' === $product->get_price() ? null : (float) $product->get_price();
				if ( null === $cost || null === $price || $price <= 0 ) {
					return null;
				}
				return ( ( $price - $cost ) / $price ) * 100;
			case 'price':
				return '' === $product->get_price() ? null : (float) $product->get_price();
			case 'regular_price':
				return '' === $product->get_regular_price() ? null : (float) $product->get_regular_price();
			case 'sale_price':
				return '' === $product->get_sale_price() ? null : (float) $product->get_sale_price();
			case 'on_sale':
				return (bool) $product->is_on_sale();
			case 'stock_status':
				return $product->get_stock_status();
			case 'stock_quantity':
				$qty = $product->get_stock_quantity();
				return null === $qty ? null : (float) $qty;
			case 'total_sales':
				return (float) $product->get_total_sales();
			case 'average_rating':
				return (float) $product->get_average_rating();
			case 'rating_count':
				return (float) $product->get_rating_count();
			case 'weight':
				return '' === $product->get_weight() ? null : (float) $product->get_weight();
			case 'length':
				return '' === $product->get_length() ? null : (float) $product->get_length();
			case 'width':
				return '' === $product->get_width() ? null : (float) $product->get_width();
			case 'height':
				return '' === $product->get_height() ? null : (float) $product->get_height();
		}

		/**
		 * Resolve the value for a custom field key registered via the scwc_fields filter.
		 *
		 * @param mixed      $value   Default null.
		 * @param string     $field   Field key.
		 * @param WC_Product $product Product object.
		 */
		return apply_filters( 'scwc_get_field_value', null, $field, $product );
	}
}
