<?php
/**
 * Google Merchant Feed Generator
 *
 * Generates XML product feeds for Google Merchant Center.
 *
 * @package GGM_Commerce
 * @since 1.0.0
 */

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter

/**
 * Class GGM_Commerce_Google_Merchant_Feed
 *
 * Main class for generating Google Merchant product feeds.
 */
class GGM_Commerce_Google_Merchant_Feed {

	/**
	 * Feed directory name.
	 */
	const FEED_DIR = 'ggm-commerce-feeds';

	/**
	 * Singleton instance.
	 *
	 * @var GGM_Commerce_Google_Merchant_Feed|null
	 */
	private static $instance = null;

	/**
	 * Settings instance.
	 *
	 * @var GGM_Commerce_Google_Merchant_Settings
	 */
	private $settings;

	/**
	 * Feed manager instance.
	 *
	 * @var GGM_Commerce_Google_Merchant_Feed_Manager
	 */
	private $feed_manager;

	/**
	 * Validator instance.
	 *
	 * @var GGM_Commerce_Google_Merchant_Validator|null
	 */
	private $validator;

	/**
	 * Get singleton instance.
	 *
	 * @return GGM_Commerce_Google_Merchant_Feed
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Private constructor for singleton.
	 */
	private function __construct() {
		$this->settings     = GGM_Commerce_Google_Merchant_Settings::instance();
		$this->feed_manager = GGM_Commerce_Google_Merchant_Feed_Manager::instance();
	}

	/**
	 * Set validator instance.
	 *
	 * @param GGM_Commerce_Google_Merchant_Validator $validator Validator instance.
	 */
	public function set_validator( $validator ) {
		$this->validator = $validator;
	}

	/**
	 * Generate a feed.
	 *
	 * @param string $feed_id Feed ID.
	 * @return array|WP_Error Result with file path or error.
	 */
	public function generate_feed( $feed_id ) {
		$feed = $this->feed_manager->get_feed( $feed_id );

		if ( ! $feed ) {
			return new WP_Error( 'feed_not_found', __( 'Feed not found.', 'ggm-commerce' ) );
		}

		// Get products for this feed.
		$products = $this->get_products_for_feed( $feed );

		if ( empty( $products ) ) {
			// Still create an empty feed.
			$products = array();
		}

		// Build XML.
		$xml = $this->build_feed_xml( $products, $feed );

		// Ensure directory exists.
		$this->ensure_feed_directory();

		// Write file.
		$file_path = $this->feed_manager->get_feed_file_path( $feed_id );
		$result    = file_put_contents( $file_path, $xml ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents

		if ( false === $result ) {
			return new WP_Error( 'write_error', __( 'Failed to write feed file.', 'ggm-commerce' ) );
		}

		// Update feed stats.
		$error_count = $this->validator ? $this->validator->get_error_count() : 0;
		$this->feed_manager->update_feed_stats( $feed_id, count( $products ), $error_count );

		return array(
			'success'       => true,
			'file_path'     => $file_path,
			'product_count' => count( $products ),
			'error_count'   => $error_count,
		);
	}

	/**
	 * Get products for a feed based on its filters.
	 *
	 * @param array $feed Feed configuration.
	 * @return array Products.
	 */
	public function get_products_for_feed( $feed ) {
		global $wpdb;

		$where = array( "p.status = 'active'" );
		$join  = '';

		// Category filter.
		if ( ! empty( $feed['categories'] ) ) {
			$category_ids = array_map( 'intval', $feed['categories'] );
			$placeholders = implode( ',', array_fill( 0, count( $category_ids ), '%d' ) );
			$join        .= " INNER JOIN {$wpdb->prefix}ggmc_product_categories pc ON p.id = pc.product_id";
			$where[]      = $wpdb->prepare( "pc.category_id IN ($placeholders)", ...$category_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}

		// Exclude categories.
		if ( ! empty( $feed['exclude_categories'] ) ) {
			$exclude_ids  = array_map( 'intval', $feed['exclude_categories'] );
			$placeholders = implode( ',', array_fill( 0, count( $exclude_ids ), '%d' ) );
			$where[]      = $wpdb->prepare(
				"p.id NOT IN (
					SELECT product_id FROM {$wpdb->prefix}ggmc_product_categories
					WHERE category_id IN ($placeholders)
				)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				...$exclude_ids
			);
		}

		// Stock status filter.
		if ( 'instock' === $feed['stock_status'] ) {
			$where[] = "p.stock_status = 'instock'";
		} elseif ( 'outofstock' === $feed['stock_status'] ) {
			$where[] = "p.stock_status = 'outofstock'";
		}

		// Sale only filter.
		if ( ! empty( $feed['sale_only'] ) ) {
			$where[] = 'p.sale_price IS NOT NULL AND p.sale_price > 0';
		}

		// Featured only filter.
		if ( ! empty( $feed['featured_only'] ) ) {
			$where[] = 'p.featured = 1';
		}

		$sql = "SELECT DISTINCT p.* FROM {$wpdb->prefix}ggmc_products p {$join} WHERE " . implode( ' AND ', $where ) . ' ORDER BY p.id ASC';

		return $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Build the feed XML.
	 *
	 * @param array $products Products to include.
	 * @param array $feed     Feed configuration.
	 * @return string XML content.
	 */
	private function build_feed_xml( $products, $feed ) {
		$site_name = get_bloginfo( 'name' );
		$site_url  = home_url();

		$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( $site_name . ' - ' . $feed['name'] ) . '</title>' . "\n";
		$xml .= '<link>' . $this->xml_escape( $site_url ) . '</link>' . "\n";
		$xml .= '<description>' . $this->xml_escape( sprintf( 'Product feed for %s', $site_name ) ) . '</description>' . "\n";

		foreach ( $products as $product ) {
			$xml .= $this->build_product_item( $product );
		}

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

		return $xml;
	}

	/**
	 * Build a single product item for the feed.
	 *
	 * @param object $product Product object.
	 * @return string XML item.
	 */
	private function build_product_item( $product ) {
		$item_data = $this->map_product_data( $product );

		// Validate if validator is set.
		if ( $this->validator ) {
			$this->validator->validate_product( $product->id, $item_data );
		}

		$xml = '<item>' . "\n";

		// Required fields.
		$xml .= '<g:id>' . $this->xml_escape( $item_data['id'] ) . '</g:id>' . "\n";
		$xml .= '<g:title>' . $this->xml_escape( $item_data['title'] ) . '</g:title>' . "\n";
		$xml .= '<g:description>' . $this->xml_escape( $item_data['description'] ) . '</g:description>' . "\n";
		$xml .= '<g:link>' . $this->xml_escape( $item_data['link'] ) . '</g:link>' . "\n";

		if ( ! empty( $item_data['image_link'] ) ) {
			$xml .= '<g:image_link>' . $this->xml_escape( $item_data['image_link'] ) . '</g:image_link>' . "\n";
		}

		$xml .= '<g:price>' . $this->xml_escape( $item_data['price'] ) . '</g:price>' . "\n";
		$xml .= '<g:availability>' . $this->xml_escape( $item_data['availability'] ) . '</g:availability>' . "\n";

		// Optional fields.
		if ( ! empty( $item_data['sale_price'] ) ) {
			$xml .= '<g:sale_price>' . $this->xml_escape( $item_data['sale_price'] ) . '</g:sale_price>' . "\n";
		}

		if ( ! empty( $item_data['brand'] ) ) {
			$xml .= '<g:brand>' . $this->xml_escape( $item_data['brand'] ) . '</g:brand>' . "\n";
		}

		if ( ! empty( $item_data['gtin'] ) ) {
			$xml .= '<g:gtin>' . $this->xml_escape( $item_data['gtin'] ) . '</g:gtin>' . "\n";
		}

		if ( ! empty( $item_data['mpn'] ) ) {
			$xml .= '<g:mpn>' . $this->xml_escape( $item_data['mpn'] ) . '</g:mpn>' . "\n";
		}

		$xml .= '<g:condition>' . $this->xml_escape( $item_data['condition'] ) . '</g:condition>' . "\n";

		if ( ! empty( $item_data['product_type'] ) ) {
			$xml .= '<g:product_type>' . $this->xml_escape( $item_data['product_type'] ) . '</g:product_type>' . "\n";
		}

		if ( ! empty( $item_data['google_product_category'] ) ) {
			$xml .= '<g:google_product_category>' . $this->xml_escape( $item_data['google_product_category'] ) . '</g:google_product_category>' . "\n";
		}

		// Additional images.
		if ( ! empty( $item_data['additional_image_links'] ) ) {
			foreach ( $item_data['additional_image_links'] as $additional_image ) {
				$xml .= '<g:additional_image_link>' . $this->xml_escape( $additional_image ) . '</g:additional_image_link>' . "\n";
			}
		}

		// Shipping.
		if ( ! empty( $item_data['shipping'] ) ) {
			$xml .= '<g:shipping>' . "\n";
			$xml .= '<g:country>' . $this->xml_escape( $item_data['shipping']['country'] ) . '</g:country>' . "\n";
			$xml .= '<g:price>' . $this->xml_escape( $item_data['shipping']['price'] ) . '</g:price>' . "\n";
			$xml .= '</g:shipping>' . "\n";
		}

		// Identifier exists (required if no GTIN/MPN/brand).
		if ( empty( $item_data['gtin'] ) && empty( $item_data['mpn'] ) ) {
			$xml .= '<g:identifier_exists>false</g:identifier_exists>' . "\n";
		}

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

		return $xml;
	}

	/**
	 * Map product data to Google Merchant fields.
	 *
	 * @param object $product Product object.
	 * @return array Mapped data.
	 */
	private function map_product_data( $product ) {
		$settings = $this->settings->get_settings();
		$currency = $settings['currency'] ?: 'USD';

		$data = array(
			'id'                      => $this->get_product_id( $product ),
			'title'                   => $this->clean_text( $product->name ),
			'description'             => $this->get_description( $product ),
			'link'                    => $this->get_product_url( $product ),
			'image_link'              => $this->get_image_url( $product->image_id ),
			'additional_image_links'  => $this->get_gallery_urls( $product->gallery_ids ),
			'price'                   => $this->format_price( $product->price, $currency ),
			'sale_price'              => ! empty( $product->sale_price ) ? $this->format_price( $product->sale_price, $currency ) : '',
			'availability'            => $this->map_availability( $product->stock_status ),
			'brand'                   => $this->get_brand( $product ),
			'gtin'                    => $this->get_gtin( $product->id ),
			'mpn'                     => $this->get_mpn( $product ),
			'condition'               => $settings['default_condition'] ?: 'new',
			'product_type'            => $this->get_product_type( $product->id ),
			'google_product_category' => $this->get_google_category( $product->id ),
			'shipping'                => $this->get_shipping_info(),
		);

		return $data;
	}

	/**
	 * Get product ID based on settings.
	 *
	 * @param object $product Product object.
	 * @return string
	 */
	private function get_product_id( $product ) {
		$id_format = $this->settings->get( 'id_format', 'sku' );

		if ( 'sku' === $id_format && ! empty( $product->sku ) ) {
			return $product->sku;
		}

		return (string) $product->id;
	}

	/**
	 * Get product description.
	 *
	 * @param object $product Product object.
	 * @return string
	 */
	private function get_description( $product ) {
		$field = $this->settings->get( 'description_field', 'description' );

		$description = '';
		if ( 'short_description' === $field && ! empty( $product->short_description ) ) {
			$description = $product->short_description;
		} else {
			$description = $product->description ?? '';
		}

		return $this->clean_text( $description );
	}

	/**
	 * Get product URL.
	 *
	 * @param object $product Product object.
	 * @return string
	 */
	private function get_product_url( $product ) {
		// Use ggm-commerce product URL structure.
		return home_url( '/product/' . $product->slug );
	}

	/**
	 * Get image URL from attachment ID.
	 *
	 * @param int $image_id Attachment ID.
	 * @return string
	 */
	private function get_image_url( $image_id ) {
		if ( empty( $image_id ) ) {
			return '';
		}

		$image_url = wp_get_attachment_url( $image_id );
		return $image_url ?: '';
	}

	/**
	 * Get gallery image URLs.
	 *
	 * @param string|array $gallery_ids Gallery IDs (comma-separated or array).
	 * @return array
	 */
	private function get_gallery_urls( $gallery_ids ) {
		if ( empty( $gallery_ids ) ) {
			return array();
		}

		if ( is_string( $gallery_ids ) ) {
			$gallery_ids = explode( ',', $gallery_ids );
		}

		$urls = array();
		foreach ( $gallery_ids as $id ) {
			$url = wp_get_attachment_url( (int) trim( $id ) );
			if ( $url ) {
				$urls[] = $url;
			}
		}

		// Limit to 10 additional images (Google's limit).
		return array_slice( $urls, 0, 10 );
	}

	/**
	 * Format price with currency.
	 *
	 * @param float  $price    Price value.
	 * @param string $currency Currency code.
	 * @return string
	 */
	private function format_price( $price, $currency ) {
		if ( empty( $price ) ) {
			return '';
		}
		return number_format( (float) $price, 2, '.', '' ) . ' ' . $currency;
	}

	/**
	 * Map stock status to Google availability.
	 *
	 * @param string $stock_status Stock status.
	 * @return string
	 */
	private function map_availability( $stock_status ) {
		switch ( $stock_status ) {
			case 'instock':
				return 'in_stock';
			case 'outofstock':
				return 'out_of_stock';
			case 'onbackorder':
				return 'backorder';
			default:
				return 'in_stock';
		}
	}

	/**
	 * Get brand for product.
	 *
	 * @param object $product Product object.
	 * @return string
	 */
	private function get_brand( $product ) {
		global $wpdb;

		$brand_attr_id = $this->settings->get( 'brand_attribute_id' );

		// Try to get from product attribute first.
		if ( $brand_attr_id ) {
			$brand = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT pav.value
					FROM {$wpdb->prefix}ggmc_product_attribute_values pav
					WHERE pav.product_id = %d AND pav.attribute_id = %d",
					$product->id,
					$brand_attr_id
				)
			);

			if ( $brand ) {
				return $brand;
			}
		}

		// Fallback to default brand.
		return $this->settings->get( 'default_brand', '' );
	}

	/**
	 * Get GTIN from product barcodes.
	 *
	 * @param int $product_id Product ID.
	 * @return string
	 */
	private function get_gtin( $product_id ) {
		global $wpdb;

		$barcode_type = $this->settings->get( 'gtin_barcode_type', 'EAN-13' );

		// Get primary barcode of specified type.
		$gtin = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT barcode_value
				FROM {$wpdb->prefix}ggmc_product_barcodes
				WHERE product_id = %d AND barcode_type = %s AND is_primary = 1
				LIMIT 1",
				$product_id,
				$barcode_type
			)
		);

		if ( $gtin ) {
			return $gtin;
		}

		// Try any barcode of GTIN-compatible types.
		$gtin_types = array( 'EAN-13', 'EAN-8', 'UPC-A', 'UPC-E', 'GTIN-14' );
		$gtin       = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT barcode_value
				FROM {$wpdb->prefix}ggmc_product_barcodes
				WHERE product_id = %d AND barcode_type IN ('" . implode( "','", $gtin_types ) . "')
				ORDER BY is_primary DESC
				LIMIT 1",
				$product_id
			)
		);

		return $gtin ?: '';
	}

	/**
	 * Get MPN for product.
	 *
	 * @param object $product Product object.
	 * @return string
	 */
	private function get_mpn( $product ) {
		$mpn_source = $this->settings->get( 'mpn_source', 'sku' );

		if ( 'sku' === $mpn_source ) {
			return $product->sku ?: '';
		}

		// If numeric, treat as attribute ID.
		if ( is_numeric( $mpn_source ) ) {
			global $wpdb;
			$mpn = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT pav.value
					FROM {$wpdb->prefix}ggmc_product_attribute_values pav
					WHERE pav.product_id = %d AND pav.attribute_id = %d",
					$product->id,
					(int) $mpn_source
				)
			);
			return $mpn ?: '';
		}

		return '';
	}

	/**
	 * Get product type (category path) for product.
	 *
	 * @param int $product_id Product ID.
	 * @return string
	 */
	private function get_product_type( $product_id ) {
		global $wpdb;

		$categories = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT c.name
				FROM {$wpdb->prefix}ggmc_categories c
				INNER JOIN {$wpdb->prefix}ggmc_product_categories pc ON c.id = pc.category_id
				WHERE pc.product_id = %d
				ORDER BY c.name",
				$product_id
			)
		);

		return implode( ' > ', $categories );
	}

	/**
	 * Get Google product category for product.
	 *
	 * @param int $product_id Product ID.
	 * @return string
	 */
	private function get_google_category( $product_id ) {
		global $wpdb;

		// First check for product-level override (if implemented).
		// Then check category-level mapping.
		$google_category = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT c.google_category_id
				FROM {$wpdb->prefix}ggmc_categories c
				INNER JOIN {$wpdb->prefix}ggmc_product_categories pc ON c.id = pc.category_id
				WHERE pc.product_id = %d AND c.google_category_id IS NOT NULL
				LIMIT 1",
				$product_id
			)
		);

		return $google_category ?: '';
	}

	/**
	 * Get shipping info from settings.
	 *
	 * @return array|null
	 */
	private function get_shipping_info() {
		$country = $this->settings->get( 'shipping_country' );
		$price   = $this->settings->get( 'shipping_price' );

		if ( empty( $country ) || empty( $price ) ) {
			return null;
		}

		$currency = $this->settings->get( 'currency', 'USD' );

		return array(
			'country' => $country,
			'price'   => $this->format_price( $price, $currency ),
		);
	}

	/**
	 * Clean text for XML output.
	 *
	 * @param string $text Text to clean.
	 * @return string
	 */
	private function clean_text( $text ) {
		// Strip HTML tags.
		$text = wp_strip_all_tags( $text );

		// Decode HTML entities.
		$text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' );

		// Remove excess whitespace.
		$text = preg_replace( '/\s+/', ' ', $text );

		// Trim.
		$text = trim( $text );

		// Limit length (Google has limits).
		if ( strlen( $text ) > 5000 ) {
			$text = substr( $text, 0, 4997 ) . '...';
		}

		return $text;
	}

	/**
	 * Escape text for XML.
	 *
	 * @param string $text Text to escape.
	 * @return string
	 */
	private function xml_escape( $text ) {
		return htmlspecialchars( (string) $text, ENT_XML1 | ENT_QUOTES, 'UTF-8' );
	}

	/**
	 * Ensure feed directory exists.
	 *
	 * @return bool
	 */
	private function ensure_feed_directory() {
		$upload_dir = wp_upload_dir();
		$feed_dir   = $upload_dir['basedir'] . '/' . self::FEED_DIR;

		if ( ! file_exists( $feed_dir ) ) {
			wp_mkdir_p( $feed_dir );

			// Add index.php for security.
			file_put_contents( $feed_dir . '/index.php', '<?php // Silence is golden' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents
		}

		return is_dir( $feed_dir );
	}

	/**
	 * Get the feed directory path.
	 *
	 * @return string
	 */
	public function get_feed_directory() {
		$upload_dir = wp_upload_dir();
		return $upload_dir['basedir'] . '/' . self::FEED_DIR;
	}

	/**
	 * Check if the feed feature is activated via license.
	 *
	 * @return bool
	 */
	public static function is_activated() {
		$license = ggm_commerce_license();
		return $license->is_widget_activated( 'google-merchant-feed' );
	}
}
