<?php
/**
 * Product sync engine.
 *
 * @package KwiroAISalesAgent
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class Kwiro_Product_Sync
 *
 * Reads WooCommerce products and syncs them to the cloud service.
 */
class Kwiro_Product_Sync {

	/**
	 * API Client instance.
	 *
	 * @var Kwiro_API_Client
	 */
	private $api_client;

	/**
	 * Constructor.
	 *
	 * @param Kwiro_API_Client $api_client API client instance.
	 */
	public function __construct( $api_client ) {
		$this->api_client = $api_client;
	}

	/**
	 * Get all published products.
	 *
	 * @param array $args Query arguments.
	 * @return WC_Product[] Array of WC_Product objects.
	 */
	public function get_products( $args = array() ) {
		$defaults = array(
			'status' => 'publish',
			'limit'  => -1,
			'type'   => array( 'simple', 'variable', 'grouped', 'external' ),
		);

		$args = wp_parse_args( $args, $defaults );

		return wc_get_products( $args );
	}

	/**
	 * Get products in batches.
	 *
	 * @param int $page      Page number (1-based).
	 * @param int $per_page  Products per page.
	 * @return array {
	 *     @type WC_Product[] $products Array of products.
	 *     @type int          $total    Total number of products.
	 *     @type bool         $has_more Whether there are more pages.
	 * }
	 */
	public function get_products_paginated( $page = 1, $per_page = 50 ) {
		$products = wc_get_products(
			array(
				'status'  => 'publish',
				'limit'   => $per_page,
				'page'    => $page,
				'type'    => array( 'simple', 'variable', 'grouped', 'external' ),
				'orderby' => 'ID',
				'order'   => 'ASC',
			)
		);

		$total_ids = wc_get_products(
			array(
				'status' => 'publish',
				'limit'  => -1,
				'type'   => array( 'simple', 'variable', 'grouped', 'external' ),
				'return' => 'ids',
			)
		);

		$total_count = is_array( $total_ids ) ? count( $total_ids ) : 0;

		return array(
			'products' => $products,
			'total'    => $total_count,
			'has_more' => ( $page * $per_page ) < $total_count,
		);
	}

	/**
	 * Fetch a batch of products by offset. Using offset makes sync resilient
	 * to the adaptive batch sizer changing `per_page` mid-sync — each caller
	 * can pick whatever limit it likes and we always know how many products
	 * we've already processed.
	 *
	 * @param int $offset Products already processed.
	 * @param int $limit  Batch size for this call.
	 * @return array { products, total, has_more }
	 */
	public function get_products_by_offset( $offset = 0, $limit = 50 ) {
		$offset = max( 0, (int) $offset );
		$limit  = max( 1, (int) $limit );

		$ids   = wc_get_products(
			array(
				'status'  => 'publish',
				'limit'   => -1,
				'type'    => array( 'simple', 'variable', 'grouped', 'external' ),
				'return'  => 'ids',
				'orderby' => 'ID',
				'order'   => 'ASC',
			)
		);
		$ids   = is_array( $ids ) ? $ids : array();
		$total = count( $ids );

		$slice_ids = array_slice( $ids, $offset, $limit );
		$products  = array();
		foreach ( $slice_ids as $id ) {
			$p = wc_get_product( $id );
			if ( $p ) {
				$products[] = $p;
			}
		}

		return array(
			'products' => $products,
			'total'    => $total,
			'has_more' => ( $offset + count( $products ) ) < $total,
		);
	}

	/**
	 * Format a WC_Product for cloud sync.
	 *
	 * @param WC_Product $product WooCommerce product.
	 * @return array Formatted product data.
	 */
	public function format_product( $product ) {
		// Use get_regular_price() (not get_price(), which returns the sale price when on sale).
		// For variable products both getters return '' on the parent — fall back to cheapest variation.
		$price      = $product->get_regular_price();
		$sale_price = $product->get_sale_price();
		if ( $product->is_type( 'variable' ) ) {
			if ( '' === $price || null === $price ) {
				$price = $product->get_variation_regular_price( 'min', true );
			}
			if ( '' === $sale_price || null === $sale_price ) {
				$var_sale = $product->get_variation_sale_price( 'min', true );
				if ( '' !== $var_sale && null !== $var_sale && floatval( $var_sale ) < floatval( $price ) ) {
					$sale_price = $var_sale;
				}
			}
		}
		// Sale price must be strictly below regular price, else null.
		if ( '' !== $sale_price && null !== $sale_price && floatval( $sale_price ) >= floatval( $price ) ) {
			$sale_price = null;
		}
		$avg_rating = $product->get_average_rating();

		// Strip HTML, shortcodes, and excess whitespace from descriptions for clean AI context.
		$short_desc = $product->get_short_description();
		$long_desc  = $product->get_description();
		// strip_shortcodes() only strips registered shortcodes; also strip unregistered [tags].
		$short_desc = $short_desc ? trim( wp_strip_all_tags( preg_replace( '/\[[^\]]*\]/', '', strip_shortcodes( $short_desc ) ) ) ) : null;
		$long_desc  = $long_desc ? trim( wp_strip_all_tags( preg_replace( '/\[[^\]]*\]/', '', strip_shortcodes( $long_desc ) ) ) ) : null;
		$short_desc = $short_desc ? $short_desc : null;
		$long_desc  = $long_desc ? $long_desc : null;

		$data = array(
			'wc_product_id'     => absint( $product->get_id() ),
			'name'              => $product->get_name(),
			'short_description' => $short_desc,
			'description'       => $long_desc,
			'price'             => '' !== $price ? floatval( $price ) : null,
			'sale_price'        => '' !== $sale_price ? floatval( $sale_price ) : null,
			'categories'        => $this->get_product_categories( $product ),
			'tags'              => $this->get_product_tags( $product ),
			'stock_status'      => $product->get_stock_status(),
			'attributes'        => $this->get_product_attributes( $product ),
			'variations'        => $product->is_type( 'variable' ) ? $this->get_product_variations( $product ) : array(),
			'average_rating'    => '' !== $avg_rating ? floatval( $avg_rating ) : null,
			'review_count'      => $product->get_review_count(),
			'image_url'         => $this->get_product_image_url( $product ) ? $this->get_product_image_url( $product ) : null,
			'product_url'       => get_permalink( $product->get_id() ) ? get_permalink( $product->get_id() ) : null,
		);

		return $data;
	}

	/**
	 * Get the main product image URL.
	 *
	 * @param WC_Product $product Product object.
	 * @return string Image URL or empty string.
	 */
	private function get_product_image_url( $product ) {
		$external = get_post_meta( $product->get_id(), '_external_image_url', true );
		if ( $external ) {
			return $external;
		}
		$image_id = $product->get_image_id();
		if ( $image_id ) {
			$image_url = wp_get_attachment_image_url( $image_id, 'full' );
			return $image_url ? $image_url : '';
		}
		return '';
	}

	/**
	 * Get gallery image URLs.
	 *
	 * @param WC_Product $product Product object.
	 * @return array Array of image URLs.
	 */
	private function get_gallery_image_urls( $product ) {
		$urls = array();
		foreach ( $product->get_gallery_image_ids() as $image_id ) {
			$url = wp_get_attachment_image_url( $image_id, 'full' );
			if ( $url ) {
				$urls[] = $url;
			}
		}
		return $urls;
	}

	/**
	 * Get product categories.
	 *
	 * @param WC_Product $product Product object.
	 * @return array Array of category data.
	 */
	private function get_product_categories( $product ) {
		$categories = array();
		$term_ids   = $product->get_category_ids();

		foreach ( $term_ids as $term_id ) {
			$term = get_term( $term_id, 'product_cat' );
			if ( $term && ! is_wp_error( $term ) ) {
				$categories[] = $term->name;
			}
		}

		return $categories;
	}

	/**
	 * Get product tags.
	 *
	 * @param WC_Product $product Product object.
	 * @return array Array of tag data.
	 */
	private function get_product_tags( $product ) {
		$tags     = array();
		$term_ids = $product->get_tag_ids();

		foreach ( $term_ids as $term_id ) {
			$term = get_term( $term_id, 'product_tag' );
			if ( $term && ! is_wp_error( $term ) ) {
				$tags[] = $term->name;
			}
		}

		return $tags;
	}

	/**
	 * Get product attributes.
	 *
	 * @param WC_Product $product Product object.
	 * @return array Array of attribute data.
	 */
	private function get_product_attributes( $product ) {
		$attributes = array();

		foreach ( $product->get_attributes() as $attribute ) {
			if ( $attribute instanceof WC_Product_Attribute ) {
				$name = $attribute->get_name();

				// For taxonomy attributes, get human-readable term names.
				if ( $attribute->is_taxonomy() ) {
					$label   = wc_attribute_label( $name, $product );
					$options = wc_get_product_terms(
						$product->get_id(),
						$name,
						array( 'fields' => 'names' )
					);
				} else {
					$label   = $name;
					$options = $attribute->get_options();
				}

				// Ensure all option values are strings.
				$attributes[ $label ] = array_map( 'strval', $options );
			}
		}

		return (object) $attributes;
	}

	/**
	 * Get variations for a variable product.
	 *
	 * @param WC_Product_Variable $product Variable product.
	 * @return array Array of variation data.
	 */
	private function get_product_variations( $product ) {
		$variations = array();

		// Map taxonomy slugs (e.g. "red") → human labels (e.g. "Red") so the widget
		// can match against variant_attributes keyed by display names.
		$slug_to_label = array();
		foreach ( $product->get_attributes() as $attr ) {
			if ( ! $attr->is_taxonomy() ) {
				continue;
			}
			foreach ( wp_get_post_terms( $product->get_id(), $attr->get_name(), array( 'fields' => 'all' ) ) as $term ) {
				$slug_to_label[ $attr->get_name() . ':' . $term->slug ] = $term->name;
			}
		}

		foreach ( $product->get_available_variations() as $variation_data ) {
			$variation = wc_get_product( $variation_data['variation_id'] );
			if ( ! $variation ) {
				continue;
			}

			// Normalize attributes: { attribute_pa_size: "m" } → { size: "M" }.
			$attrs = array();
			foreach ( $variation->get_attributes() as $key => $value ) {
				if ( '' === $value ) {
					continue;
				}
				$clean_key           = preg_replace( '/^(attribute_)?(pa_)?/', '', $key );
				$taxonomy            = 'pa_' . $clean_key;
				$label               = $slug_to_label[ $taxonomy . ':' . $value ] ?? ucfirst( $value );
				$attrs[ $clean_key ] = $label;
			}

			$var_price      = $variation->get_price();
			$var_reg_price  = $variation->get_regular_price();
			$var_sale_price = $variation->get_sale_price();

			$variations[] = array(
				'variation_id' => absint( $variation->get_id() ),
				'sku'          => $variation->get_sku(),
				'price'        => '' !== $var_reg_price ? floatval( $var_reg_price ) : floatval( $var_price ),
				'sale_price'   => ( '' !== $var_sale_price && floatval( $var_sale_price ) < floatval( $var_reg_price ) ) ? floatval( $var_sale_price ) : null,
				'stock_status' => $variation->get_stock_status(),
				'attributes'   => $attrs,
				'image_url'    => $this->get_product_image_url( $variation ) ? $this->get_product_image_url( $variation ) : null,
			);
		}

		return $variations;
	}

	/**
	 * Format all products for sync.
	 *
	 * @param WC_Product[] $products Array of products.
	 * @return array Array of formatted product data.
	 */
	public function format_products( $products ) {
		return array_map( array( $this, 'format_product' ), $products );
	}

	/**
	 * Sync a batch of products to the cloud.
	 *
	 * @param WC_Product[] $products Products to sync.
	 * @param array        $args     Optional. Sync metadata: `batch_number`,
	 *                               `total_batches`, `trigger_source`.
	 * @return array|WP_Error Sync result.
	 */
	public function sync_batch( $products, $args = array() ) {
		$formatted = $this->format_products( $products );
		return $this->api_client->sync_products( $formatted, $args );
	}

	/**
	 * Read the lowest free-shipping threshold across all WooCommerce shipping
	 * zones. Returns null if no free-shipping rule applies anywhere.
	 *
	 * The widget's "X away from free shipping" nudge uses this so we don't
	 * have to hardcode a number that's wrong on most stores.
	 *
	 * Notes on edge cases handled:
	 *  - Zone 0 is "rest of the world" — included.
	 *  - Free-shipping methods can have requires = 'min_amount' or
	 *    'either' (min_amount OR coupon). Both still imply a min_amount when set.
	 *  - Coupon-only or unconditional free shipping → no threshold (return null).
	 *  - Disabled methods skipped.
	 *  - Multiple zones → return the lowest threshold so the nudge is generous.
	 *
	 * @return float|null
	 */
	public function get_free_shipping_threshold() {
		if ( ! class_exists( 'WC_Shipping_Zones' ) ) {
			return null;
		}

		$min_threshold = null;

		// Build zone list including the "rest of the world" zone (id 0).
		$zones   = WC_Shipping_Zones::get_zones();
		$zones[] = array( 'shipping_methods' => WC_Shipping_Zones::get_zone( 0 )->get_shipping_methods() );

		foreach ( $zones as $zone ) {
			$methods = isset( $zone['shipping_methods'] ) ? $zone['shipping_methods'] : array();
			foreach ( $methods as $method ) {
				if ( 'free_shipping' !== $method->id ) {
					continue;
				}
				if ( 'yes' !== $method->enabled ) {
					continue;
				}
				$requires = isset( $method->requires ) ? $method->requires : '';
				// Only consider methods that key off a min_amount.
				if ( ! in_array( $requires, array( 'min_amount', 'either', 'both' ), true ) ) {
					continue;
				}
				$amount = isset( $method->min_amount ) ? floatval( $method->min_amount ) : 0;
				if ( $amount <= 0 ) {
					continue;
				}
				if ( null === $min_threshold || $amount < $min_threshold ) {
					$min_threshold = $amount;
				}
			}
		}

		return $min_threshold;
	}

	/**
	 * Trigger a full product sync.
	 *
	 * @return array|WP_Error Result.
	 */
	public function trigger_full_sync() {
		update_option(
			'kwiro_sync_status',
			array(
				'last_sync'      => null,
				'products_count' => 0,
				'status'         => 'syncing',
				'current_page'   => 1,
			)
		);

		// Schedule immediate batch processing.
		wp_schedule_single_event( time(), 'kwiro_batch_sync' );

		return array( 'status' => 'started' );
	}

	/**
	 * Get the total count of published products.
	 *
	 * @return int
	 */
	public function get_product_count() {
		$ids = wc_get_products(
			array(
				'status' => 'publish',
				'limit'  => -1,
				'return' => 'ids',
			)
		);

		return is_array( $ids ) ? count( $ids ) : 0;
	}
}
