<?php
/**
 * affilun Cloud top affiliate candidates client.
 *
 * @package Affilun\Application\Cloud
 */

declare(strict_types=1);

namespace Affilun\Application\Cloud;

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

/**
 * Fetches top-selling affiliate candidates from affilun Cloud.
 */
final class CloudTopAffiliateCandidatesClient {

	/**
	 * @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();
	}

	/**
	 * Fetch product-shaped top affiliate candidate rows.
	 *
	 * @param CloudSettings $settings  Cloud settings.
	 * @param list<string>  $providers Enabled provider keys.
	 * @param int           $limit     Max candidate sets.
	 * @param int           $timestamp Timestamp.
	 * @param string        $nonce     Nonce.
	 *
	 * @return list<array<string,mixed>>
	 */
	public function fetch(
		CloudSettings $settings,
		array $providers,
		int $limit,
		int $timestamp,
		string $nonce
	): array {
		if ( ! $settings->isConfigured() ) {
			throw new InvalidArgumentException( 'affilun Cloud settings are not configured.' );
		}
		$providers = self::normalizeProviders( $providers );
		if ( $providers === array() ) {
			throw new InvalidArgumentException( 'At least one affiliate provider is required.' );
		}
		$limit   = max( 1, min( 100, $limit ) );
		$path    = '/top-affiliate-candidates?providers='
			. rawurlencode( implode( ',', $providers ) )
			. '&limit=' . rawurlencode( (string) $limit );
		$headers = $this->signer->sign(
			$settings->siteId(),
			$settings->apiKey(),
			'GET',
			$path,
			'',
			$timestamp,
			$nonce
		);

		$response = ( $this->transport )( $settings->endpoint() . $path, $headers, '' );
		$status   = isset( $response['status'] ) ? (int) $response['status'] : 0;
		$data     = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : null;
		if ( $status < 200 || $status >= 300 || ! is_array( $data ) ) {
			throw new RuntimeException( 'affilun Cloud top affiliate candidates failed.' );
		}

		return self::rowsFromPayload( $data );
	}

	/**
	 * @param list<string> $providers Providers.
	 *
	 * @return list<string>
	 */
	private static function normalizeProviders( array $providers ): array {
		$normalized = array();
		foreach ( $providers as $provider ) {
			$value = preg_replace( '/[^a-z0-9_]/', '', strtolower( trim( $provider ) ) ) ?? '';
			if ( $value !== '' && ! in_array( $value, $normalized, true ) ) {
				$normalized[] = $value;
			}
		}

		return $normalized;
	}

	/**
	 * @param array<string,mixed> $payload Payload.
	 *
	 * @return list<array<string,mixed>>
	 */
	private static function rowsFromPayload( array $payload ): array {
		$candidates = isset( $payload['candidates'] ) && is_array( $payload['candidates'] )
			? $payload['candidates']
			: array();
		$rows       = array();
		foreach ( $candidates as $candidate ) {
			if ( ! is_array( $candidate ) ) {
				continue;
			}
			$row = self::rowFromCandidate( $candidate );
			if ( $row !== null ) {
				$rows[] = $row;
			}
		}

		return $rows;
	}

	/**
	 * @param array<string,mixed> $candidate Candidate.
	 *
	 * @return array<string,mixed>|null
	 */
	private static function rowFromCandidate( array $candidate ): ?array {
		$provider       = self::stringField( $candidate, 'provider' );
		$providerItemId = self::stringField( $candidate, 'provider_item_id' );
		$title          = self::stringField( $candidate, 'title' );
		$detailUrl      = self::stringField( $candidate, 'detail_url' );
		if ( $provider === '' || $providerItemId === '' || $title === '' || $detailUrl === '' ) {
			return null;
		}

		$rawPayload = $candidate['raw_payload'] ?? $candidate;

		return array(
			'provider'               => $provider,
			'provider_item_id'       => $providerItemId,
			'canonical_hash'         => self::stringField( $candidate, 'canonical_hash' ) !== ''
				? self::stringField( $candidate, 'canonical_hash' )
				: hash( 'sha256', $provider . ':' . $providerItemId ),
			'title'                  => $title,
			'brand'                  => self::nullableStringField( $candidate, 'brand' ),
			'description'            => self::nullableStringField( $candidate, 'description' ),
			'image_primary_url'      => self::nullableStringField( $candidate, 'image_primary_url' ),
			'image_gallery_json'     => self::nullableJsonField( $candidate, 'image_gallery_json' ),
			'price_amount'           => self::nullableScalarField( $candidate, 'price_amount' ),
			'price_currency'         => self::nullableStringField( $candidate, 'price_currency' ),
			'price_label'            => self::nullableStringField( $candidate, 'price_label' ),
			'availability_status'    => self::nullableStringField( $candidate, 'availability_status' ),
			'detail_url'             => $detailUrl,
			'affiliate_url'          => self::nullableStringField( $candidate, 'affiliate_url' ),
			'raw_payload_json'       => self::jsonEncode( $rawPayload ),
			'search_keywords'        => self::nullableStringField( $candidate, 'search_keywords' ),
			'search_keywords_source' => self::nullableStringField( $candidate, 'search_keywords_source' ),
			'snapshot_version'       => 1,
			'sync_state'             => 'fresh',
			'lifecycle_state'        => 'active',
		);
	}

	/**
	 * @param array<string,mixed> $row Row.
	 * @param string              $key Field key.
	 */
	private static function stringField( array $row, string $key ): string {
		$value = $row[ $key ] ?? '';
		return is_scalar( $value ) ? trim( (string) $value ) : '';
	}

	/**
	 * @param array<string,mixed> $row Row.
	 * @param string              $key Field key.
	 */
	private static function nullableStringField( array $row, string $key ): ?string {
		$value = self::stringField( $row, $key );
		return $value !== '' ? $value : null;
	}

	/**
	 * @param array<string,mixed> $row Row.
	 * @param string              $key Field key.
	 */
	private static function nullableScalarField( array $row, string $key ): string|int|float|null {
		$value = $row[ $key ] ?? null;
		return is_scalar( $value ) && trim( (string) $value ) !== '' ? $value : null;
	}

	/**
	 * @param array<string,mixed> $row Row.
	 * @param string              $key Field key.
	 */
	private static function nullableJsonField( array $row, string $key ): ?string {
		$value = $row[ $key ] ?? null;
		if ( is_array( $value ) ) {
			return self::jsonEncode( $value );
		}
		if ( is_scalar( $value ) ) {
			$string = trim( (string) $value );
			return $string !== '' ? $string : null;
		}

		return null;
	}

	/**
	 * @param mixed $value Value.
	 */
	private static function jsonEncode( mixed $value ): string {
		if ( function_exists( 'wp_json_encode' ) ) {
			$json = wp_json_encode( $value );
		} else {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Pure-unit fallback when WordPress is unavailable.
			$json = json_encode( $value );
		}
		if ( ! is_string( $json ) ) {
			throw new RuntimeException( 'Failed to encode top affiliate candidate payload.' );
		}

		return $json;
	}

	/**
	 * @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 {
			unset( $body );
			$args     = array(
				'headers' => $headers,
				'timeout' => 10,
			);
			$response = wp_remote_get( $url, $args );
			if ( is_wp_error( $response ) ) {
				return array(
					'status' => 0,
					'data'   => null,
					'error'  => $response->get_error_message(),
				);
			}
			$status       = (int) wp_remote_retrieve_response_code( $response );
			$responseBody = (string) wp_remote_retrieve_body( $response );
			$data         = json_decode( $responseBody, true );

			return array(
				'status' => $status,
				'data'   => is_array( $data ) ? $data : null,
			);
		};
	}
}
