<?php
/**
 * This file is part of the Wisewand WordPress plugin.
 *
 * API Payload Cleaner — single source of truth for Wisewand API field
 * validation, type casting, and payload sanitisation.
 *
 * @package Wisewand
 * @since 2.9.0
 * @license GPL-2.0-or-later
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

namespace Wisewand\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * API Payload Cleaner
 *
 * Provides a single, authoritative whitelist of fields accepted by the
 * Wisewand API and handles type-safe cleaning of raw payloads before
 * they are sent to the API.
 *
 * Replaces the duplicated whitelist/cleaning logic that previously lived in:
 * - Calendar_AJAX_Handler::build_clean_api_payload() + get_valid_api_fields()
 * - CRON_Article_Generator::clean_payload_for_api() + get_valid_fields_for_endpoint()
 * - Calendar_CRON::clean_payload_for_api() + get_valid_fields_for_endpoint()
 *
 * @package Wisewand
 * @since 2.9.0
 */
class API_Payload_Cleaner {

	/**
	 * Fields that must always be kept even when empty.
	 *
	 * @var array
	 */
	private static $required_fields = array(
		'subject',
		'title',
		'target_keyword',
		'lang',
		'country',
	);

	/**
	 * Boolean fields — jQuery may send "false" as a string, so we
	 * must cast with filter_var() rather than a simple (bool) cast.
	 *
	 * @var array
	 */
	private static $boolean_fields = array(
		'apply_project_brief_config',
		'keep_title',
		'use_cta',
		'use_boldkeywords',
		'use_faq',
		'use_toc',
		'use_image',
		'image_use_palette',
		'use_inlineimages',
		'inline_images_use_palette',
		'use_webhook',
		'use_internallinks',
		'use_indexed_pages_internal_links',
		'use_wp_internal_links',
		'use_app_internal_links',
		'use_list_internal_links',
		'use_publishwordpress',
		'use_publishshopify',
		'use_publishprestashop',
		'use_publishwoocommerce',
		'use_infotable',
		'use_bulletlist',
		'use_externalsources',
		'use_audio',
		'use_social',
		'information_search_only_from_subject_sources',
		'use_enhancedformatting',
		'auto_import_assets',
		'use_widget',
		'use_video',
		'use_inlinephotos',
		'use_schemaorg',
		'use_summary',
		'use_blockquotes',
	);

	/**
	 * Integer fields.
	 *
	 * @var array
	 */
	private static $integer_fields = array(
		'inlineimages_count',
		'publishwordpress_category',
		'publishwordpress_author',
	);

	/**
	 * Array fields that must be preserved even when empty.
	 *
	 * @var array
	 */
	private static $required_arrays = array(
		'social_networks',
		'internal_links_wp_status',
	);

	/**
	 * Fields managed by the project brief config.
	 *
	 * When apply_project_brief_config is true, these fields are stripped
	 * from the payload so the API applies the brief config values instead
	 * of using the explicit (often false) values from the stored payload.
	 *
	 * @var array
	 */
	private static $brief_managed_fields = array(
		// Content feature flags.
		'use_cta',
		'use_boldkeywords',
		'use_faq',
		'use_toc',
		'use_image',
		'use_inlineimages',
		'use_inlinephotos',
		'use_infotable',
		'use_bulletlist',
		'use_externalsources',
		'use_enhancedformatting',
		'use_widget',
		'use_schemaorg',
		'use_audio',
		'use_video',
		'use_social',
		'use_summary',
		'use_blockquotes',
		'use_internallinks',
		'use_indexed_pages_internal_links',
		'use_wp_internal_links',
		'use_app_internal_links',
		'use_list_internal_links',
		'information_search_only_from_subject_sources',
		// Related config fields.
		'image_ratio',
		'image_prompt',
		'image_use_palette',
		'image_primary_color',
		'image_secondary_color',
		'inlineimages_count',
		'inline_images_ratio',
		'inline_images_use_palette',
		'inline_images_primary_color',
		'inline_images_secondary_color',
		'audio_voice_id',
		'faq_questions',
		'ctas',
		'social_networks',
		'video_position',
		'internal_links_list',
		'internal_links_wp_status',
		'internal_links_wp_connection',
	);

	/**
	 * Clean a raw payload so it contains only valid, correctly-typed
	 * fields for the given Wisewand API endpoint.
	 *
	 * @since 2.9.0
	 *
	 * @param array  $raw_payload  Raw payload from the database or form submission.
	 * @param string $endpoint     API endpoint: articles, categorypages, productpages, discoverarticles.
	 * @param string $article_type Article type (blog, affiliation-product-review, etc.). Default 'blog'.
	 * @return array Cleaned payload with only valid, correctly-typed fields.
	 */
	public static function clean( array $raw_payload, $endpoint, $article_type = 'blog' ) {
		// Always enable brief config when a project is set — ensures old
		// calendar entries (pre-v2.9.0) also get the project brief applied.
		if ( ! empty( $raw_payload['project_id'] ) ) {
			$raw_payload['apply_project_brief_config'] = true;
		}

		$valid_fields  = self::get_valid_fields( $endpoint, $article_type );
		$clean_payload = array();

		foreach ( $valid_fields as $field ) {
			if ( ! isset( $raw_payload[ $field ] ) ) {
				continue;
			}

			$value = $raw_payload[ $field ];

			// Skip empty strings and nulls for optional fields.
			if ( ( '' === $value || null === $value ) && ! in_array( $field, self::$required_fields, true ) ) {
				continue;
			}

			// --- Type conversions ---

			// Boolean fields: filter_var handles "true"→true, "false"→false, "1"→true, "0"→false.
			if ( in_array( $field, self::$boolean_fields, true ) ) {
				$value = filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
				if ( null === $value ) {
					$value = false;
				}
			}

			// Length: integer OR string "auto".
			if ( 'length' === $field ) {
				if ( 'auto' !== $value && is_numeric( $value ) ) {
					$value = (int) $value;
				}
			}

			// Integer fields.
			if ( in_array( $field, self::$integer_fields, true ) && is_numeric( $value ) ) {
				$value = absint( $value );
			}

			// Country/language validation.
			if ( 'country' === $field ) {
				$value = self::validate_country( $value, $raw_payload['lang'] ?? 'en' );
			}

			// Array handling.
			if ( is_array( $value ) ) {
				$value = self::clean_array_value( $value );

				if ( empty( $value ) && ! in_array( $field, self::$required_arrays, true ) ) {
					continue;
				}

				$value = array_values( $value );
			}

			$clean_payload[ $field ] = $value;
		}

		// When brief config is active, strip all brief-managed fields so
		// the API applies the project brief config values instead of the
		// explicit (often false) values stored in the calendar payload.
		if ( ! empty( $clean_payload['apply_project_brief_config'] ) ) {
			$clean_payload = self::strip_brief_managed_fields( $clean_payload );
		}

		return $clean_payload;
	}

	/**
	 * Remove brief-config-managed fields from the payload.
	 *
	 * When apply_project_brief_config is true, the API should apply all
	 * feature settings from the project brief. Sending explicit values
	 * (especially false) for these fields would override the brief config.
	 *
	 * @since 2.9.0
	 *
	 * @param array $payload Cleaned payload.
	 * @return array Payload with brief-managed fields removed.
	 */
	private static function strip_brief_managed_fields( array $payload ) {
		foreach ( self::$brief_managed_fields as $field ) {
			unset( $payload[ $field ] );
		}

		return $payload;
	}

	/**
	 * Get the whitelist of valid fields for a Wisewand API endpoint.
	 *
	 * Based on the official API schema at api.wisewand.ai/docs/json.
	 *
	 * @since 2.9.0
	 *
	 * @param string $endpoint     API endpoint identifier.
	 * @param string $article_type Article type for affiliation-specific fields.
	 * @return array List of valid field names.
	 */
	public static function get_valid_fields( $endpoint, $article_type = 'blog' ) {
		// Common fields accepted by all content endpoints.
		$fields = array(
			'subject',
			'title',
			'length',
			'lang',
			'country',
			'target_keyword',
			'project_id',
			'persona_id',
			'writing_style_mode',
			'additional_information',
			'apply_project_brief_config',
			'use_webhook',
			'webhook_connection_id',
		);

		if ( 'articles' === $endpoint ) {
			$fields = array_merge(
				$fields,
				array(
					// Core article fields.
					'type',
					'keep_title',
					'keywords_secondary',
					'search_intention_answer',
					'avoid_information',
					// Content features.
					'use_cta',
					'ctas',
					'use_boldkeywords',
					'use_faq',
					'faq_questions',
					'use_toc',
					// Images.
					'use_image',
					'image_ratio',
					'image_prompt',
					'image_use_palette',
					'image_primary_color',
					'image_secondary_color',
					'use_inlineimages',
					'inlineimages_count',
					'inline_images_ratio',
					'inline_images_use_palette',
					'inline_images_primary_color',
					'inline_images_secondary_color',
					'use_inlinephotos',
					// Formatting & structure.
					'use_infotable',
					'use_bulletlist',
					'use_externalsources',
					'use_enhancedformatting',
					'auto_import_assets',
					'use_widget',
					'use_schemaorg',
					'information_search_only_from_subject_sources',
					// Audio & video.
					'use_audio',
					'audio_voice_id',
					'use_video',
					'video_position',
					// Social.
					'use_social',
					'social_networks',
					// Internal links.
					'use_internallinks',
					'use_indexed_pages_internal_links',
					'use_wp_internal_links',
					'use_app_internal_links',
					'use_list_internal_links',
					'internal_links_list',
					'internal_links_wp_status',
					'internal_links_wp_connection',
					// Publishing.
					'use_publishwordpress',
					'publishwordpress_status',
					'publishwordpress_category',
					'publishwordpress_author',
					'publishwordpress_connection',
					'publishwordpress_date',
					'use_publishshopify',
					'publishshopify_connection_id',
					'use_publishprestashop',
					'publishprestashop_connection_id',
					'use_publishwoocommerce',
					'publishwoocommerce_connection_id',
				)
			);

			// Affiliation-specific fields.
			if ( 'affiliation-product-review' === $article_type ) {
				$fields = array_merge(
					$fields,
					array(
						'affiliation_product_name',
						'affiliation_product_review_urls',
						'affiliation_links',
						'affiliation_promo_code',
						'affiliation_promo_percentage',
					)
				);
			} elseif ( 'affiliation-compare-products' === $article_type ) {
				$fields = array_merge(
					$fields,
					array(
						'affiliation_product_1_name',
						'affiliation_product_1_review_urls',
						'affiliation_links_product_1',
						'affiliation_promo_code_1',
						'affiliation_promo_percentage_1',
						'affiliation_product_2_name',
						'affiliation_product_2_review_urls',
						'affiliation_links_product_2',
						'affiliation_promo_code_2',
						'affiliation_promo_percentage_2',
					)
				);
			} elseif ( 'affiliation-top-product' === $article_type ) {
				$fields[] = 'affiliation_top_products';
			}
		} elseif ( in_array( $endpoint, array( 'categorypages', 'productpages', 'discoverarticles' ), true ) ) {
			$fields = array_merge(
				$fields,
				array(
					'website',
					'writing_style',
					'use_summary',
					'use_blockquotes',
				)
			);

			if ( 'discoverarticles' === $endpoint ) {
				$fields[] = 'keywords_secondary';
			}
		}

		return $fields;
	}

	/**
	 * Validate that a country code is valid for the given language.
	 *
	 * Falls back to the first valid country for that language if invalid.
	 *
	 * @since 2.9.0
	 *
	 * @param string $country Country code.
	 * @param string $lang    Language code.
	 * @return string Valid country code.
	 */
	private static function validate_country( $country, $lang ) {
		if ( ! class_exists( 'Wisewand\\Config\\Supported_Options' ) ) {
			require_once WISEWAND_PLUGIN_DIR . 'includes/Config/Supported_Options.php';
		}

		$valid_combos = \Wisewand\Config\Supported_Options::get_valid_language_country_combos();

		if ( isset( $valid_combos[ $lang ] ) && ! in_array( $country, $valid_combos[ $lang ], true ) ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( "WISEWAND: Invalid country '{$country}' for lang '{$lang}'. Defaulting to '{$valid_combos[ $lang ][0]}'" );
			}
			return $valid_combos[ $lang ][0];
		}

		return $country;
	}

	/**
	 * Clean an array value by removing empty entries and re-indexing.
	 *
	 * Handles both simple arrays and arrays of associative arrays (objects).
	 *
	 * @since 2.9.0
	 *
	 * @param array $value Array to clean.
	 * @return array Cleaned array.
	 */
	private static function clean_array_value( array $value ) {
		// Detect array of objects (associative arrays).
		$is_array_of_objects = ! empty( $value ) && isset( $value[0] ) && is_array( $value[0] );

		if ( $is_array_of_objects ) {
			return array_filter(
				$value,
				static function ( $item ) {
					if ( ! is_array( $item ) ) {
						return ! empty( $item ) && '' !== $item;
					}
					foreach ( $item as $prop_value ) {
						if ( ! empty( $prop_value ) && '' !== $prop_value ) {
							return true;
						}
					}
					return false;
				}
			);
		}

		// Simple array — filter out empty strings, nulls, and false values.
		return array_filter(
			$value,
			static function ( $v ) {
				return '' !== $v && null !== $v && false !== $v;
			}
		);
	}
}
