<?php
/**
 * affilun Cloud product-keyword normalization client.
 *
 * @package Affilun\Application\Cloud
 */

declare(strict_types=1);

namespace Affilun\Application\Cloud;

use Affilun\Application\Settings\CloudSettings;
use InvalidArgumentException;
use RuntimeException;

/**
 * Normalizes saved-product search keywords through affilun Cloud managed AI.
 */
final class CloudProductKeywordsClient {

	/**
	 * Transport callback.
	 *
	 * @var callable
	 */
	private $transport;

	/**
	 * @param CloudRequestSigner $signer    Request signer.
	 * @param callable|null      $transport Transport override.
	 *
	 * @phpstan-param (callable(string,array<string,string>,string):array<string,mixed>)|null $transport
	 */
	public function __construct(
		private CloudRequestSigner $signer = new CloudRequestSigner(),
		?callable $transport = null
	) {
		$this->transport = $transport ?? self::defaultTransport();
	}

	/**
	 * Normalize product search keywords.
	 *
	 * @param CloudSettings       $settings  Cloud settings.
	 * @param array<string,mixed> $product   Product payload.
	 * @param int                 $timestamp Timestamp.
	 * @param string              $nonce     Nonce.
	 *
	 * @throws InvalidArgumentException When Cloud settings are incomplete.
	 * @throws RuntimeException When Cloud response fails.
	 */
	public function normalize( CloudSettings $settings, array $product, int $timestamp, string $nonce ): string {
		if ( ! $settings->isConfigured() ) {
			throw new InvalidArgumentException( 'affilun Cloud settings are not configured.' );
		}

		$path                    = '/ai/product-keywords';
		$body                    = self::jsonEncode( $product );
		$headers                 = $this->signer->sign(
			$settings->siteId(),
			$settings->apiKey(),
			'POST',
			$path,
			$body,
			$timestamp,
			$nonce
		);
		$headers['Content-Type'] = 'application/json';

		$response = ( $this->transport )( $settings->endpoint() . $path, $headers, $body );
		$status   = isset( $response['status'] ) ? (int) $response['status'] : 0;
		$data     = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : null;
		$keywords = is_array( $data ) && is_scalar( $data['search_keywords'] ?? null )
			? trim( (string) $data['search_keywords'] )
			: '';
		if ( $status < 200 || $status >= 300 || $keywords === '' ) {
			throw new RuntimeException( 'affilun Cloud product keyword normalization failed.' );
		}

		return self::truncate( $keywords, 512 );
	}

	/**
	 * JSON encode request payload.
	 *
	 * @param array<string,mixed> $payload Payload.
	 */
	private static function jsonEncode( array $payload ): string {
		if ( function_exists( 'wp_json_encode' ) ) {
			$body = wp_json_encode( $payload );
		} else {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Pure-unit fallback when WordPress is unavailable.
			$body = json_encode( $payload );
		}
		if ( ! is_string( $body ) ) {
			throw new RuntimeException( 'Failed to encode affilun Cloud request.' );
		}
		return $body;
	}

	/**
	 * Default WordPress HTTP transport.
	 *
	 * @return callable
	 *
	 * @phpstan-return callable(string,array<string,string>,string):array<string,mixed>
	 */
	private static function defaultTransport(): callable {
		return static function ( string $url, array $headers, string $body ): array {
			$response = CloudHttpOptions::safePost(
				$url,
				array(
					'timeout' => 5,
					'headers' => $headers,
					'body'    => $body,
				)
			);
			if ( function_exists( 'is_wp_error' ) && is_wp_error( $response ) ) {
				return array(
					'status' => 503,
					'data'   => null,
				);
			}
			$raw     = is_array( $response ) ? (string) wp_remote_retrieve_body( $response ) : '';
			$decoded = json_decode( $raw, true );
			return array(
				'status' => is_array( $response ) ? (int) wp_remote_retrieve_response_code( $response ) : 0,
				'data'   => is_array( $decoded ) ? $decoded : null,
			);
		};
	}

	/**
	 * Truncate text.
	 *
	 * @param string $value Text.
	 * @param int    $max   Max length.
	 */
	private static function truncate( string $value, int $max ): string {
		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $value, 0, $max, 'UTF-8' );
		}
		return substr( $value, 0, $max );
	}
}
