<?php
/**
 * ERENQVP_Attributes — dynamic global-attribute engine.
 *
 * Replaces the original single-tenant hard-coded TAXMAP (pa_beden/pa_renk/pa_marka)
 * with a store-agnostic layer: list whatever global attributes the store already has,
 * create new ones on the fly, and resolve/create terms — all usable within the same request.
 *
 * @package ErenLabs\QuickVariableProducts
 */

defined( 'ABSPATH' ) || exit;

class ERENQVP_Attributes {

	/**
	 * All global product attributes (the `pa_*` taxonomies the store has defined).
	 *
	 * @return array[] List of { id, taxonomy (pa_slug), label, slug }.
	 */
	public static function all_global() {
		$out = array();
		foreach ( wc_get_attribute_taxonomies() as $tax ) {
			$slug     = $tax->attribute_name;            // e.g. "color"
			$taxonomy = wc_attribute_taxonomy_name( $slug ); // e.g. "pa_color"
			$out[]    = array(
				'id'       => (int) $tax->attribute_id,
				'taxonomy' => $taxonomy,
				'slug'     => $slug,
				'label'    => $tax->attribute_label ? $tax->attribute_label : $slug,
			);
		}
		return $out;
	}

	/**
	 * Terms of a given attribute taxonomy (for value pickers / datalists).
	 *
	 * @param string $taxonomy Full taxonomy name, e.g. "pa_color".
	 * @return array[] List of { slug, name }.
	 */
	public static function terms( $taxonomy ) {
		$taxonomy = self::sanitize_taxonomy( $taxonomy );
		if ( '' === $taxonomy ) {
			return array();
		}
		self::ensure_registered( $taxonomy );
		if ( ! taxonomy_exists( $taxonomy ) ) {
			return array();
		}
		$terms = get_terms(
			array(
				'taxonomy'   => $taxonomy,
				'hide_empty' => false,
				'number'     => 0,
			)
		);
		$out = array();
		if ( is_array( $terms ) ) {
			foreach ( $terms as $t ) {
				$out[] = array(
					'slug' => $t->slug,
					'name' => $t->name,
				);
			}
		}
		return $out;
	}

	/**
	 * Create a new global attribute from a human label, e.g. "Shoe Size" → pa_shoe-size.
	 * If an attribute with the same slug already exists, that one is returned (idempotent).
	 *
	 * Critically: registers the taxonomy immediately so terms can be inserted in the SAME request.
	 *
	 * @param string $label Human-readable attribute name.
	 * @return array|WP_Error { id, taxonomy, slug, label } or WP_Error.
	 */
	public static function create_global( $label ) {
		$label = trim( wp_strip_all_tags( (string) $label ) );
		if ( '' === $label ) {
			return new WP_Error( 'erenqvp_empty', __( 'Attribute name is empty.', 'quick-variable-products' ) );
		}

		// WooCommerce attribute slugs are limited to 28 bytes (the taxonomy gets a "pa_" prefix; total <= 32).
		// Cut by CHARACTERS first (mb_substr) so a multibyte Turkish/Cyrillic letter is never split mid-byte,
		// then enforce the 28-BYTE ceiling for slugs that are still too long after the character cut.
		$slug = wc_sanitize_taxonomy_name( $label );
		if ( function_exists( 'mb_substr' ) ) {
			$slug = erenqvp_substr( $slug, 0, 28 );
			while ( strlen( $slug ) > 28 && '' !== $slug ) {
				$slug = erenqvp_substr( $slug, 0, erenqvp_strlen( $slug ) - 1 );
			}
		} else {
			$slug = substr( $slug, 0, 28 );
		}
		if ( '' === $slug ) {
			return new WP_Error( 'erenqvp_slug', __( 'Could not derive a valid slug from the attribute name.', 'quick-variable-products' ) );
		}

		$existing_id = self::attribute_id_by_slug( $slug );
		if ( $existing_id ) {
			$taxonomy = wc_attribute_taxonomy_name( $slug );
			self::ensure_registered( $taxonomy );
			return array(
				'id'       => $existing_id,
				'taxonomy' => $taxonomy,
				'slug'     => $slug,
				'label'    => $label,
			);
		}

		$id = wc_create_attribute(
			array(
				'name'         => $label,
				'slug'         => $slug,
				'type'         => 'select',
				'order_by'     => 'menu_order',
				'has_archives' => false,
			)
		);
		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$taxonomy = wc_attribute_taxonomy_name( $slug );
		// Flush the cached taxonomy list so WooCommerce sees the new one, then register it now.
		delete_transient( 'wc_attribute_taxonomies' );
		self::ensure_registered( $taxonomy );

		return array(
			'id'       => (int) $id,
			'taxonomy' => $taxonomy,
			'slug'     => $slug,
			'label'    => $label,
		);
	}

	/**
	 * Resolve a term within an attribute taxonomy by name (or slug); create it if missing.
	 *
	 * @param string $taxonomy Full taxonomy name, e.g. "pa_color".
	 * @param string $name     Term label, e.g. "Red".
	 * @return WP_Term|null
	 */
	public static function resolve_term( $taxonomy, $name ) {
		$taxonomy = self::sanitize_taxonomy( $taxonomy );
		$name     = trim( (string) $name );
		if ( '' === $taxonomy || '' === $name ) {
			return null;
		}
		self::ensure_registered( $taxonomy );
		if ( ! taxonomy_exists( $taxonomy ) ) {
			return null;
		}

		$term = get_term_by( 'name', $name, $taxonomy );
		if ( $term ) {
			return $term;
		}
		$term = get_term_by( 'slug', sanitize_title( $name ), $taxonomy );
		if ( $term ) {
			return $term;
		}
		if ( ! self::can_create_terms() ) {
			return null; // no existing term matched and this user may not create new ones
		}
		$res = wp_insert_term( $name, $taxonomy );
		if ( is_wp_error( $res ) ) {
			// Race / already-exists fallback.
			if ( ! empty( $res->error_data['term_exists'] ) ) {
				$existing = get_term( (int) $res->error_data['term_exists'], $taxonomy );
				return $existing && ! is_wp_error( $existing ) ? $existing : null;
			}
			return null;
		}
		$created = get_term( $res['term_id'], $taxonomy );
		return $created && ! is_wp_error( $created ) ? $created : null;
	}

	/**
	 * Hierarchical, indented product category list for a <select>.
	 *
	 * @return array[] List of { id, name (indented) }.
	 */
	public static function categories() {
		$terms = get_terms(
			array(
				'taxonomy'   => 'product_cat',
				'hide_empty' => false,
				'number'     => 0,
			)
		);
		if ( is_wp_error( $terms ) || ! is_array( $terms ) ) {
			return array();
		}
		$by_parent = array();
		foreach ( $terms as $t ) {
			$by_parent[ (int) $t->parent ][] = $t;
		}
		$out  = array();
		$walk = function ( $parent, $depth ) use ( &$walk, &$out, $by_parent ) {
			if ( empty( $by_parent[ $parent ] ) ) {
				return;
			}
			foreach ( $by_parent[ $parent ] as $t ) {
				$out[] = array(
					'id'   => (int) $t->term_id,
					'name' => str_repeat( '— ', $depth ) . $t->name,
				);
				$walk( (int) $t->term_id, $depth + 1 );
			}
		};
		$walk( 0, 0 );
		return $out;
	}

	/**
	 * Whether the native WooCommerce Brand taxonomy (WC 9.6+) is available.
	 *
	 * @return bool
	 */
	public static function brand_supported() {
		return taxonomy_exists( 'product_brand' );
	}

	/**
	 * Existing product brands, for the brand datalist.
	 *
	 * @return array[] List of { id, name }.
	 */
	public static function brand_terms() {
		if ( ! taxonomy_exists( 'product_brand' ) ) {
			return array();
		}
		$terms = get_terms( array( 'taxonomy' => 'product_brand', 'hide_empty' => false, 'number' => 0 ) );
		$out = array();
		if ( is_array( $terms ) ) {
			foreach ( $terms as $t ) {
				$out[] = array( 'id' => (int) $t->term_id, 'name' => $t->name );
			}
		}
		return $out;
	}

	/**
	 * Whether the current user may CREATE new product terms (categories, tags, brands, attribute
	 * values). Attaching EXISTING terms is always allowed for anyone who can reach these screens;
	 * only minting a brand-new term needs the product-term management capability. Filterable so an
	 * intentionally-locked-down role can be re-granted, or a stricter site can revoke it.
	 *
	 * @return bool
	 */
	public static function can_create_terms() {
		return (bool) apply_filters( 'erenqvp_can_create_terms', current_user_can( 'manage_product_terms' ) );
	}

	/**
	 * Resolve a brand term by name (or slug); create it if missing.
	 *
	 * @param string $name Brand name.
	 * @return int term_id, or 0 if unavailable/empty.
	 */
	public static function resolve_brand( $name ) {
		$name = trim( (string) $name );
		if ( '' === $name || ! taxonomy_exists( 'product_brand' ) ) {
			return 0;
		}
		$term = get_term_by( 'name', $name, 'product_brand' );
		if ( ! $term ) {
			$term = get_term_by( 'slug', sanitize_title( $name ), 'product_brand' );
		}
		if ( $term ) {
			return (int) $term->term_id;
		}
		if ( ! self::can_create_terms() ) {
			return 0; // no existing brand matched and this user may not create new terms
		}
		$res = wp_insert_term( $name, 'product_brand' );
		if ( is_wp_error( $res ) ) {
			return ! empty( $res->error_data['term_exists'] ) ? (int) $res->error_data['term_exists'] : 0;
		}
		return (int) $res['term_id'];
	}

	/**
	 * Existing product tags, for the tags datalist.
	 *
	 * @return array[] List of { id, name }.
	 */
	public static function tag_terms() {
		$terms = get_terms( array( 'taxonomy' => 'product_tag', 'hide_empty' => false, 'number' => 0 ) );
		$out = array();
		if ( is_array( $terms ) ) {
			foreach ( $terms as $t ) {
				$out[] = array( 'id' => (int) $t->term_id, 'name' => $t->name );
			}
		}
		return $out;
	}

	/**
	 * Resolve a list of tag names to term ids, creating any that don't exist.
	 *
	 * @param array $names Tag names.
	 * @return int[] Unique term ids.
	 */
	public static function resolve_tag_ids( array $names ) {
		$ids = array();
		foreach ( $names as $n ) {
			$n = trim( (string) $n );
			if ( '' === $n ) {
				continue;
			}
			$t = get_term_by( 'name', $n, 'product_tag' );
			if ( ! $t ) {
				$t = get_term_by( 'slug', sanitize_title( $n ), 'product_tag' );
			}
			if ( ! $t && ! self::can_create_terms() ) {
				continue; // no existing tag and this user may not create new terms — skip it
			}
			if ( ! $t ) {
				$r = wp_insert_term( $n, 'product_tag' );
				if ( ! is_wp_error( $r ) ) {
					$t = get_term( $r['term_id'], 'product_tag' );
				} elseif ( ! empty( $r->error_data['term_exists'] ) ) {
					$t = get_term( (int) $r->error_data['term_exists'], 'product_tag' );
				}
			}
			if ( $t && ! is_wp_error( $t ) ) {
				$ids[] = (int) $t->term_id;
			}
		}
		return array_values( array_unique( $ids ) );
	}

	/* ───────────────────────── internals ───────────────────────── */

	/**
	 * Make sure an attribute taxonomy is registered in the current request.
	 * Needed right after wc_create_attribute(), and defensively elsewhere, because WC only
	 * registers `pa_*` taxonomies on `init` — which has already fired during admin-ajax.
	 *
	 * @param string $taxonomy Full taxonomy name, e.g. "pa_color".
	 * @return void
	 */
	public static function ensure_registered( $taxonomy ) {
		if ( taxonomy_exists( $taxonomy ) ) {
			return;
		}
		// Only register GENUINE WooCommerce attribute taxonomies: without this, a malformed
		// payload naming an arbitrary pa_* string would get a live taxonomy and junk terms.
		if ( ! wc_attribute_taxonomy_id_by_name( $taxonomy ) ) {
			return;
		}
		// Mirror WooCommerce's own registration args closely enough for term storage + admin use.
		register_taxonomy(
			$taxonomy,
			apply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy, array( 'product' ) ),
			apply_filters(
				'woocommerce_taxonomy_args_' . $taxonomy,
				array(
					'labels'       => array( 'name' => $taxonomy ),
					'hierarchical' => true,
					'show_ui'      => false,
					'query_var'    => true,
					'rewrite'      => false,
					'public'       => false,
				)
			)
		);
	}

	/**
	 * Look up an attribute id by its (pa-stripped) slug, e.g. "color".
	 *
	 * @param string $slug Attribute slug without the pa_ prefix.
	 * @return int 0 if not found.
	 */
	public static function attribute_id_by_slug( $slug ) {
		$slug = wc_sanitize_taxonomy_name( $slug );
		foreach ( wc_get_attribute_taxonomies() as $tax ) {
			if ( $tax->attribute_name === $slug ) {
				return (int) $tax->attribute_id;
			}
		}
		return 0;
	}

	/**
	 * Validate that a taxonomy string is a WooCommerce attribute taxonomy ("pa_*").
	 *
	 * @param string $taxonomy Raw input.
	 * @return string Clean taxonomy name, or '' if invalid.
	 */
	public static function sanitize_taxonomy( $taxonomy ) {
		$taxonomy = sanitize_text_field( (string) $taxonomy );
		if ( 0 !== strpos( $taxonomy, 'pa_' ) ) {
			return '';
		}
		// Allow only lowercase letters, digits, underscore and hyphen.
		if ( ! preg_match( '/^pa_[a-z0-9_\-]+$/', $taxonomy ) ) {
			return '';
		}
		return $taxonomy;
	}
}
