<?php
/**
 * ActiveTrail Product Feed Generator
 *
 * Generates Google Shopping and Facebook Catalog XML/JSON product feeds.
 *
 * @package ActiveTrail_WooCommerce
 * @since 1.0.0
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * ActiveTrail Product Feed Generator Class
 */
class ActiveTrail_Product_Feed_Generator {

	/**
	 * WooCommerce currency code.
	 *
	 * @var string
	 */
	private $currency;

	/**
	 * Store name.
	 *
	 * @var string
	 */
	private $store_name;

	/**
	 * Store home URL.
	 *
	 * @var string
	 */
	private $store_url;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->currency   = get_woocommerce_currency();
		$this->store_name = get_bloginfo( 'name' );
		$this->store_url  = home_url();
	}

	/**
	 * Generate Google Merchant Center XML feed (RSS 2.0 with Google Shopping namespace).
	 *
	 * @return string XML string.
	 */
	public function generate_google_xml() {
		$products = $this->get_products();

		$xml  = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
		$xml .= '<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">' . "\n";
		$xml .= '  <channel>' . "\n";
		$xml .= '    <title>' . $this->xml_escape( $this->store_name ) . '</title>' . "\n";
		$xml .= '    <link>' . $this->xml_escape( $this->store_url ) . '</link>' . "\n";
		$xml .= '    <description>' . $this->xml_escape( get_bloginfo( 'description' ) ) . '</description>' . "\n";

		foreach ( $products as $item ) {
			$xml .= '    <item>' . "\n";
			$xml .= '      <g:id>' . $this->xml_escape( (string) $item['id'] ) . '</g:id>' . "\n";
			$xml .= '      <g:title>' . $this->xml_escape( $item['title'] ) . '</g:title>' . "\n";
			$xml .= '      <g:description>' . $this->xml_escape( $item['description'] ) . '</g:description>' . "\n";
			$xml .= '      <g:link>' . $this->xml_escape( $item['link'] ) . '</g:link>' . "\n";
			$xml .= '      <g:image_link>' . $this->xml_escape( $item['image_link'] ) . '</g:image_link>' . "\n";
			$xml .= '      <g:availability>' . $this->xml_escape( $item['availability'] ) . '</g:availability>' . "\n";
			$xml .= '      <g:price>' . $this->xml_escape( $item['price'] . ' ' . $this->currency ) . '</g:price>' . "\n";
			if ( ! empty( $item['sale_price'] ) ) {
				$xml .= '      <g:sale_price>' . $this->xml_escape( $item['sale_price'] . ' ' . $this->currency ) . '</g:sale_price>' . "\n";
			}
			$xml .= '      <g:condition>new</g:condition>' . "\n";
			if ( ! empty( $item['brand'] ) ) {
				$xml .= '      <g:brand>' . $this->xml_escape( $item['brand'] ) . '</g:brand>' . "\n";
			}
			if ( ! empty( $item['sku'] ) ) {
				$xml .= '      <g:mpn>' . $this->xml_escape( $item['sku'] ) . '</g:mpn>' . "\n";
			}
			if ( ! empty( $item['category'] ) ) {
				$xml .= '      <g:product_type>' . $this->xml_escape( $item['category'] ) . '</g:product_type>' . "\n";
			}
			if ( ! empty( $item['item_group_id'] ) ) {
				$xml .= '      <g:item_group_id>' . $this->xml_escape( (string) $item['item_group_id'] ) . '</g:item_group_id>' . "\n";
			}
			$xml .= '    </item>' . "\n";
		}

		$xml .= '  </channel>' . "\n";
		$xml .= '</rss>';

		return $xml;
	}

	/**
	 * Generate Facebook Catalog XML feed (RSS 2.0 without Google namespace).
	 *
	 * @return string XML string.
	 */
	public function generate_facebook_xml() {
		$products = $this->get_products();

		$xml  = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
		$xml .= '<rss version="2.0">' . "\n";
		$xml .= '  <channel>' . "\n";
		$xml .= '    <title>' . $this->xml_escape( $this->store_name ) . '</title>' . "\n";
		$xml .= '    <link>' . $this->xml_escape( $this->store_url ) . '</link>' . "\n";
		$xml .= '    <description>' . $this->xml_escape( get_bloginfo( 'description' ) ) . '</description>' . "\n";

		foreach ( $products as $item ) {
			$xml .= '    <item>' . "\n";
			$xml .= '      <id>' . $this->xml_escape( (string) $item['id'] ) . '</id>' . "\n";
			$xml .= '      <title>' . $this->xml_escape( $item['title'] ) . '</title>' . "\n";
			$xml .= '      <description>' . $this->xml_escape( $item['description'] ) . '</description>' . "\n";
			$xml .= '      <link>' . $this->xml_escape( $item['link'] ) . '</link>' . "\n";
			$xml .= '      <image_link>' . $this->xml_escape( $item['image_link'] ) . '</image_link>' . "\n";
			$xml .= '      <availability>' . $this->xml_escape( $item['availability'] ) . '</availability>' . "\n";
			$xml .= '      <price>' . $this->xml_escape( $item['price'] . ' ' . $this->currency ) . '</price>' . "\n";
			if ( ! empty( $item['sale_price'] ) ) {
				$xml .= '      <sale_price>' . $this->xml_escape( $item['sale_price'] . ' ' . $this->currency ) . '</sale_price>' . "\n";
			}
			$xml .= '      <condition>new</condition>' . "\n";
			if ( ! empty( $item['brand'] ) ) {
				$xml .= '      <brand>' . $this->xml_escape( $item['brand'] ) . '</brand>' . "\n";
			}
			if ( ! empty( $item['sku'] ) ) {
				$xml .= '      <mpn>' . $this->xml_escape( $item['sku'] ) . '</mpn>' . "\n";
			}
			if ( ! empty( $item['category'] ) ) {
				$xml .= '      <product_type>' . $this->xml_escape( $item['category'] ) . '</product_type>' . "\n";
			}
			if ( ! empty( $item['item_group_id'] ) ) {
				$xml .= '      <item_group_id>' . $this->xml_escape( (string) $item['item_group_id'] ) . '</item_group_id>' . "\n";
			}
			$xml .= '    </item>' . "\n";
		}

		$xml .= '  </channel>' . "\n";
		$xml .= '</rss>';

		return $xml;
	}

	/**
	 * Generate Facebook Catalog JSON feed.
	 *
	 * @return string JSON string.
	 */
	public function generate_facebook_json() {
		$products = $this->get_products();

		$data = array();
		foreach ( $products as $item ) {
			$entry = array(
				'id'           => (string) $item['id'],
				'title'        => $item['title'],
				'description'  => $item['description'],
				'availability' => $item['availability'],
				'condition'    => 'new',
				'price'        => $item['price'] . ' ' . $this->currency,
				'link'         => $item['link'],
				'image_link'   => $item['image_link'],
			);

			if ( ! empty( $item['sale_price'] ) ) {
				$entry['sale_price'] = $item['sale_price'] . ' ' . $this->currency;
			}
			if ( ! empty( $item['brand'] ) ) {
				$entry['brand'] = $item['brand'];
			}
			if ( ! empty( $item['sku'] ) ) {
				$entry['mpn'] = $item['sku'];
			}
			if ( ! empty( $item['category'] ) ) {
				$entry['product_type'] = $item['category'];
			}
			if ( ! empty( $item['item_group_id'] ) ) {
				$entry['item_group_id'] = (string) $item['item_group_id'];
			}

			$data[] = $entry;
		}

		return wp_json_encode( array( 'data' => $data ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
	}

	/**
	 * Get all published products (simple products + individual variations).
	 *
	 * @return array
	 */
	private function get_products() {
		$items = array();

		$product_ids = get_posts(
			array(
				'post_type'      => 'product',
				'post_status'    => 'publish',
				'posts_per_page' => -1,
				'fields'         => 'ids',
			)
		);

		foreach ( $product_ids as $pid ) {
			$product = wc_get_product( $pid );
			if ( ! $product || ! $product->is_visible() ) {
				continue;
			}

			if ( $product->is_type( 'variable' ) ) {
				// Output each visible variation as a separate feed item.
				foreach ( $product->get_children() as $vid ) {
					$variation = wc_get_product( $vid );
					if ( ! $variation || ! $variation->variation_is_visible() ) {
						continue;
					}
					$items[] = $this->get_variation_data( $variation, $product );
				}
			} else {
				$items[] = $this->get_product_data( $product );
			}
		}

		return $items;
	}

	/**
	 * Build feed item data for a simple / external / grouped product.
	 *
	 * @param WC_Product $product Product object.
	 * @return array
	 */
	private function get_product_data( $product ) {
		return array(
			'id'            => $product->get_id(),
			'title'         => $product->get_name(),
			'description'   => $this->get_description( $product ),
			'link'          => $this->make_absolute_url( get_permalink( $product->get_id() ) ?: home_url() ),
			'image_link'    => $this->get_image_url( $product ),
			'availability'  => $product->is_in_stock() ? 'in stock' : 'out of stock',
			'price'         => $this->format_price( wc_get_price_to_display( $product ) ),
			'sale_price'    => ( $product->is_on_sale() && '' !== $product->get_sale_price() ) ? $this->format_price( wc_get_price_to_display( $product, array( 'price' => $product->get_sale_price() ) ) ) : '',
			'sku'           => $product->get_sku() ?: (string) $product->get_id(),
			'brand'         => $this->get_brand( $product->get_id() ),
			'category'      => $this->get_category( $product->get_id() ),
			'item_group_id' => '',
		);
	}

	/**
	 * Build feed item data for a product variation.
	 *
	 * @param WC_Product_Variation $variation Variation object.
	 * @param WC_Product_Variable  $parent    Parent variable product.
	 * @return array
	 */
	private function get_variation_data( $variation, $parent ) {
		// Build title: parent name + variation attribute values.
		$title      = $parent->get_name();
		$attr_parts = array();
		foreach ( $variation->get_variation_attributes() as $attr_value ) {
			if ( ! empty( $attr_value ) ) {
				$attr_parts[] = $attr_value;
			}
		}
		if ( ! empty( $attr_parts ) ) {
			$title .= ' - ' . implode( ', ', $attr_parts );
		}

		// Use variation image if available, fall back to parent image.
		$image_url        = $this->get_image_url( $variation );
		$placeholder_url  = $this->make_absolute_url( wc_placeholder_img_src() );
		if ( empty( $image_url ) || $image_url === $placeholder_url ) {
			$image_url = $this->get_image_url( $parent );
		}

		return array(
			'id'            => $variation->get_id(),
			'title'         => $title,
			'description'   => $this->get_description( $variation, $parent ),
			'link'          => $this->make_absolute_url( get_permalink( $parent->get_id() ) ?: home_url() ),
			'image_link'    => $image_url,
			'availability'  => $variation->is_in_stock() ? 'in stock' : 'out of stock',
			'price'         => $this->format_price( wc_get_price_to_display( $variation ) ),
			'sale_price'    => ( $variation->is_on_sale() && '' !== $variation->get_sale_price() ) ? $this->format_price( wc_get_price_to_display( $variation, array( 'price' => $variation->get_sale_price() ) ) ) : '',
			'sku'           => $variation->get_sku() ?: $parent->get_sku() ?: (string) $variation->get_id(),
			'brand'         => $this->get_brand( $parent->get_id() ),
			'category'      => $this->get_category( $parent->get_id() ),
			'item_group_id' => $parent->get_id(),
		);
	}

	/**
	 * Get clean product description, falling back through description → short description → parent.
	 *
	 * @param WC_Product      $product Product object.
	 * @param WC_Product|null $parent  Parent product (for variations).
	 * @return string
	 */
	private function get_description( $product, $parent = null ) {
		$desc = $product->get_description();
		if ( empty( $desc ) && $parent ) {
			$desc = $parent->get_description();
		}
		if ( empty( $desc ) ) {
			$desc = $product->get_short_description();
		}
		if ( empty( $desc ) && $parent ) {
			$desc = $parent->get_short_description();
		}
		if ( empty( $desc ) ) {
			$desc = $parent ? $parent->get_name() : $product->get_name();
		}
		return wp_strip_all_tags( $desc );
	}

	/**
	 * Get product image URL, falling back to WooCommerce placeholder.
	 *
	 * @param WC_Product $product Product object.
	 * @return string
	 */
	private function get_image_url( $product ) {
		$image_id = $product->get_image_id();
		if ( $image_id ) {
			$url = wp_get_attachment_url( $image_id );
			if ( $url ) {
				return $this->make_absolute_url( $url );
			}
		}
		return $this->make_absolute_url( wc_placeholder_img_src() );
	}

	/**
	 * Ensure a URL is absolute and that its path segments are percent-encoded.
	 * Handles relative paths and non-ASCII characters (e.g. Hebrew filenames).
	 *
	 * @param string $url Raw URL (may be relative or absolute, encoded or not).
	 * @return string Always an absolute, properly encoded URL.
	 */
	private function make_absolute_url( $url ) {
		if ( empty( $url ) ) {
			return '';
		}
		if ( strpos( $url, 'http://' ) !== 0 && strpos( $url, 'https://' ) !== 0 ) {
			$url = set_url_scheme( home_url( '/' . ltrim( $url, '/' ) ) );
		}
		return $this->encode_url_path( $url );
	}

	/**
	 * Percent-encode non-ASCII characters in URL path segments, preserving slashes.
	 * Decodes each segment before re-encoding to avoid double-encoding already-encoded URLs.
	 *
	 * @param string $url Absolute URL.
	 * @return string URL with encoded path segments.
	 */
	private function encode_url_path( $url ) {
		$parts = parse_url( $url );
		if ( empty( $parts ) || empty( $parts['path'] ) ) {
			return $url;
		}
		$encoded_path = implode( '/', array_map(
			function( $segment ) {
				return rawurlencode( rawurldecode( $segment ) );
			},
			explode( '/', $parts['path'] )
		) );
		$result  = ( isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '' );
		$result .= ( isset( $parts['host'] )   ? $parts['host']           : '' );
		$result .= ( isset( $parts['port'] )   ? ':' . $parts['port']     : '' );
		$result .= $encoded_path;
		$result .= ( isset( $parts['query'] )    ? '?' . $parts['query']    : '' );
		$result .= ( isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '' );
		return $result;
	}

	/**
	 * Get primary product category as a string.
	 *
	 * @param int $product_id Product ID.
	 * @return string
	 */
	private function get_category( $product_id ) {
		$terms = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'names' ) );
		if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
			return implode( ' > ', $terms );
		}
		return __( 'Uncategorized', 'woocommerce' );
	}

	/**
	 * Get product brand from common brand taxonomies, falling back to store name.
	 *
	 * @param int $product_id Product ID.
	 * @return string
	 */
	private function get_brand( $product_id ) {
		foreach ( array( 'product_brand', 'pa_brand', 'brand' ) as $taxonomy ) {
			if ( taxonomy_exists( $taxonomy ) ) {
				$terms = wp_get_post_terms( $product_id, $taxonomy, array( 'fields' => 'names' ) );
				if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
					return $terms[0];
				}
			}
		}
		return $this->store_name;
	}

	/**
	 * Format a price to 2 decimal places with a period separator (no currency symbol).
	 *
	 * @param float $price Price value.
	 * @return string
	 */
	private function format_price( $price ) {
		return number_format( (float) $price, 2, '.', '' );
	}

	/**
	 * Escape a value for safe inclusion in XML.
	 * Decodes HTML entities first to prevent double-encoding (e.g. &amp; → &amp;amp;).
	 * Strips control characters that are invalid in XML 1.0 (e.g. values 0-8, 11, 12, 14-31)
	 * which can appear in content pasted from Word, Excel, or other external sources.
	 *
	 * @param string $value Raw string.
	 * @return string
	 */
	private function xml_escape( $value ) {
		$decoded = html_entity_decode( (string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$cleaned = preg_replace( '/[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', '', $decoded );
		return htmlspecialchars( $cleaned ?? $decoded, ENT_XML1 | ENT_COMPAT, 'UTF-8' );
	}
}
