<?php
/**
 * REST API: get/save recommendations.
 *
 * @package PRECO
 */

namespace PRECO\Includes\Admin\Rest;

use PRECO\Includes\Frontend\Core\RecommendationsConfig;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

defined( 'ABSPATH' ) || exit;

/**
 * Recommendations REST controller.
 */
class RecommendationsRest {
	/**
	 * Legacy key map (from prior sanitize_key-based saving).
	 *
	 * Maps lowercase/mangled keys back to their expected camelCase equivalents.
	 *
	 * @var array<string,string>
	 */
	private $legacy_key_map = array(
		'recommendedlists' => 'recommendedLists',
		'triggertype'      => 'triggerType',
		'attrkey'          => 'attrKey',
		'attrvalue'        => 'attrValue',
	);
	/**
	 * REST namespace.
	 */
	const PRECO_REST_NAMESPACE = 'preco/v1';

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register routes.
	 *
	 * @return void
	 */
	public function register_routes() {
		register_rest_route(
			self::PRECO_REST_NAMESPACE,
			'/recommendations',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_recommendations' ),
					'permission_callback' => array( $this, 'permission_check' ),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'save_recommendations' ),
					'permission_callback' => array( $this, 'permission_check' ),
					'args'                => array(
						'recommendations' => array(
							'required' => true,
						),
					),
				),
			)
		);

		// POST /preco/v1/cache/flush — flushes all cached recommendation results.
		register_rest_route(
			self::PRECO_REST_NAMESPACE,
			'/cache/flush',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'flush_cache' ),
				'permission_callback' => array( $this, 'permission_check' ),
			)
		);
	}

	/**
	 * Flush all recommendation cache entries.
	 *
	 * Called via POST /preco/v1/cache/flush from the React admin editor.
	 * Delegates to RecommendationsCache::invalidate_all() which removes
	 * every transient row matching the preco_s_ / preco_u_ prefixes.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return \WP_REST_Response
	 */
	public function flush_cache( WP_REST_Request $request ): \WP_REST_Response {
		if ( ! wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		$deleted = \PRECO\Includes\Frontend\Core\RecommendationsCache::invalidate_all();

		return new WP_REST_Response(
			array(
				'success' => true,
				'deleted' => $deleted,
				'message' => sprintf(
					/* translators: %d = number of cache entries removed */
					__( 'Cache flushed. %1$d entr%2$s removed.', 'wow-ai-product-recommendations' ),
					$deleted,
					1 === $deleted ? 'y' : 'ies'
				),
			),
			200
		);
	}

	/**
	 * Permission check.
	 *
	 * @return bool
	 */
	public function permission_check() {
		return current_user_can( 'manage_woocommerce' ) || current_user_can( 'manage_options' );
	}

	/**
	 * Get recommendations in a frontend-friendly format.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response
	 */
	public function get_recommendations( WP_REST_Request $request ) {
		if ( ! wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		$stored = $this->read_all_pages();

		foreach ( RecommendationsConfig::PAGE_KEYS as $page_key ) {
			$page_changed = false;

			$restored = $this->restore_legacy_keys_recursive( $stored[ $page_key ] );
			if ( $restored['changed'] ) {
				$stored[ $page_key ] = $restored['data'];
				$page_changed        = true;
			}

			// Normalize any old comma-separated string taxonomy values to arrays so
			// the admin FormTokenField can display them and so the engine can filter
			// correctly without needing a re-save first.
			$normalized = $this->normalize_taxonomy_values_recursive( $stored[ $page_key ] );
			if ( $normalized['changed'] ) {
				$stored[ $page_key ] = $normalized['data'];
				$page_changed        = true;
			}

			if ( $page_changed ) {
				update_option( RecommendationsConfig::option_name_for_page( $page_key ), $stored[ $page_key ], false );
			}
		}

		return new WP_REST_Response(
			array(
				'recommendations' => $stored,
				'formatted'       => $this->format_for_frontend( $stored ),
			),
			200
		);
	}

	/**
	 * Save recommendations.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function save_recommendations( WP_REST_Request $request ) {
		if ( ! wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		$recommendations = $request->get_param( 'recommendations' );
		if ( ! is_array( $recommendations ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid recommendations payload.' ), 400 );
		}

		$sanitized = RestSanitizer::sanitize_recursive( $recommendations );
		$defaults  = $this->get_default_recommendations();

		foreach ( RecommendationsConfig::PAGE_KEYS as $page_key ) {
			$page_data = isset( $sanitized[ $page_key ] ) && is_array( $sanitized[ $page_key ] )
				? $sanitized[ $page_key ]
				: $defaults[ $page_key ];
			update_option( RecommendationsConfig::option_name_for_page( $page_key ), $page_data, false );
		}

		$stored = $this->read_all_pages();

		// Notify cache layer (and any other listeners) that the recommendation
		// configuration has been updated so stale cached results are flushed.
		// @see RecommendationsCacheAdmin::on_preco_settings_saved().
		do_action( 'preco_settings_saved' );

		return new WP_REST_Response(
			array(
				'updated'         => true,
				'recommendations' => $stored,
				'formatted'       => $this->format_for_frontend( $stored ),
			),
			200
		);
	}

	/**
	 * Read every page's config from its own option row, falling back to
	 * that page's default structure when the option is missing.
	 *
	 * @return array
	 */
	private function read_all_pages() {
		$defaults = $this->get_default_recommendations();
		$out      = array();

		foreach ( RecommendationsConfig::PAGE_KEYS as $page_key ) {
			$stored           = get_option( RecommendationsConfig::option_name_for_page( $page_key ), null );
			$out[ $page_key ] = is_array( $stored ) ? $stored : $defaults[ $page_key ];
		}

		return $out;
	}

	/**
	 * Restore legacy-sanitized keys back to expected camelCase.
	 *
	 * @param mixed $value Value.
	 * @param int   $depth Depth.
	 * @return array{data:mixed,changed:bool}
	 */
	private function restore_legacy_keys_recursive( $value, $depth = 0 ) {
		if ( $depth > 50 ) {
			return array(
				'data'    => $value,
				'changed' => false,
			);
		}

		if ( ! is_array( $value ) ) {
			return array(
				'data'    => $value,
				'changed' => false,
			);
		}

		$changed = false;
		$out     = array();
		foreach ( $value as $k => $v ) {
			$new_key = $k;
			if ( is_string( $k ) && isset( $this->legacy_key_map[ $k ] ) ) {
				$new_key = $this->legacy_key_map[ $k ];
				$changed = true;
			}

			$child = $this->restore_legacy_keys_recursive( $v, $depth + 1 );
			if ( $child['changed'] ) {
				$changed = true;
			}
			$out[ $new_key ] = $child['data'];
		}

		return array(
			'data'    => $out,
			'changed' => $changed,
		);
	}

	/**
	 * Default structure (based on plans.md).
	 *
	 * @return array
	 */
	private function get_default_recommendations() {
		$out = array();
		foreach ( RecommendationsConfig::PAGE_KEYS as $page_key ) {
			$locations = array();
			foreach ( RecommendationsConfig::get_location_keys( $page_key ) as $location_key ) {
				$locations[ $location_key ] = $this->default_location();
			}
			$out[ $page_key ] = array( 'locations' => $locations );
		}
		return $out;
	}

	/**
	 * Default location structure.
	 *
	 * @return array
	 */
	private function default_location() {
		return array(
			'settings'         => array(),
			'recommendedLists' => array(),
		);
	}

	/**
	 * Create an editor-friendly structure for the frontend (admin app).
	 *
	 * @param array $recommendations Raw recommendations.
	 * @return array
	 */
	private function format_for_frontend( $recommendations ) {
		// Kept as literal __() calls (rather than looping over
		// RecommendationsConfig::PAGE_LABELS) so these strings remain
		// statically extractable for translation.
		$page_labels = array(
			'product-page'    => __( 'Product Page', 'wow-ai-product-recommendations' ),
			'shop-page'       => __( 'Shop Page', 'wow-ai-product-recommendations' ),
			'product-archive' => __( 'Product Archive', 'wow-ai-product-recommendations' ),
			'thank-you'       => __( 'Thank You Page', 'wow-ai-product-recommendations' ),
			'cart'            => __( 'Cart Page', 'wow-ai-product-recommendations' ),
			'checkout'        => __( 'Checkout Page', 'wow-ai-product-recommendations' ),
		);

		$out = array();
		foreach ( RecommendationsConfig::PAGE_KEYS as $page_key ) {
			$page_label = isset( $page_labels[ $page_key ] ) ? $page_labels[ $page_key ] : $page_key;
			$page       = isset( $recommendations[ $page_key ] ) && is_array( $recommendations[ $page_key ] ) ? $recommendations[ $page_key ] : array();
			$locations  = isset( $page['locations'] ) && is_array( $page['locations'] ) ? $page['locations'] : array();

			$location_items = array();
			foreach ( $locations as $location_key => $location_value ) {
				$location_items[] = array(
					'id'    => (string) $location_key,
					'label' => RecommendationsConfig::humanize_key( $location_key ),
					'data'  => is_array( $location_value ) ? $location_value : array(),
				);
			}

			$out[] = array(
				'id'        => (string) $page_key,
				'label'     => (string) $page_label,
				'locations' => $location_items,
			);
		}

		return $out;
	}

	/**
	 * Walk the stored data and convert any rule `value` fields for taxonomy
	 * trigger types (category, tag, brand, current_category, current_tag) from
	 * legacy comma-separated strings into proper arrays.
	 *
	 * @param mixed $value Value.
	 * @param int   $depth Depth.
	 * @return array{data:mixed,changed:bool}
	 */
	private function normalize_taxonomy_values_recursive( $value, $depth = 0 ) {
		if ( $depth > 50 || ! is_array( $value ) ) {
			return array(
				'data'    => $value,
				'changed' => false,
			);
		}

		// Detect a rule node: has triggerType and a value key.
		$taxonomy_triggers = array( 'category', 'tag', 'brand', 'current_category', 'current_tag' );
		if (
			isset( $value['triggerType'], $value['value'] ) &&
			in_array( (string) $value['triggerType'], $taxonomy_triggers, true ) &&
			is_string( $value['value'] ) &&
			'' !== trim( $value['value'] )
		) {
			$raw_slugs      = array_values( array_filter( array_map( 'trim', explode( ',', $value['value'] ) ) ) );
			$value['value'] = $raw_slugs;
			return array(
				'data'    => $value,
				'changed' => true,
			);
		}

		$changed = false;
		$out     = array();
		foreach ( $value as $k => $v ) {
			$child = $this->normalize_taxonomy_values_recursive( $v, $depth + 1 );
			if ( $child['changed'] ) {
				$changed = true;
			}
			$out[ $k ] = $child['data'];
		}

		return array(
			'data'    => $out,
			'changed' => $changed,
		);
	}
}
