<?php
/**
 * Mercari provider implementation.
 *
 * 0.4.0 introduces Mercari as a lightweight search-link provider. Mercari
 * does not expose a supported public item search API suitable for AFFILUN's
 * normalized SearchProviderInterface pipeline, so this provider advertises no
 * product-search or lookup capability. It is still registered so rendering
 * code can enumerate enabled providers and create Mercari search buttons.
 *
 * @package Affilun\Infrastructure\Provider\Mercari
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Provider\Mercari;

use Affilun\Domain\Affiliate\AffiliateContext;
use Affilun\Domain\Affiliate\AffiliateLinkResult;
use Affilun\Domain\Pricing\AvailabilityInfo;
use Affilun\Domain\Product\ProductSnapshot;
use Affilun\Domain\Provider\ProviderCapabilities;
use Affilun\Domain\Provider\ProviderHealthStatus;
use Affilun\Domain\Provider\ProviderProductId;
use Affilun\Domain\Provider\SearchProviderInterface;
use Affilun\Domain\Search\SearchQuery;
use Affilun\Domain\Search\SearchResult;
use DateTimeImmutable;
use DateTimeZone;
use InvalidArgumentException;

/**
 * Lightweight Mercari provider used for multi-provider search buttons.
 */
final class MercariProvider implements SearchProviderInterface {

	/**
	 * Stable provider key.
	 */
	public const PROVIDER_KEY = 'mercari';

	/**
	 * Human-readable provider label.
	 */
	public const LABEL = 'メルカリ';

	/**
	 * Mercari Ambassador image endpoint.
	 */
	private const AMBASSADOR_IMAGE_ENDPOINT = 'https://ambassador-system.mercari.com/v1/i';

	/**
	 * Optional Mercari Ambassador affiliate id (`afid`).
	 */
	private string $affiliateId;

	/**
	 * @param string $affiliateId Mercari Ambassador affiliate id (`afid`).
	 */
	public function __construct( string $affiliateId = '' ) {
		$this->affiliateId = trim( $affiliateId );
	}

	/**
	 * Configured Mercari Ambassador affiliate id.
	 */
	public function affiliateId(): string {
		return $this->affiliateId;
	}

	/**
	 * Stable §4 provider key.
	 */
	public function key(): string {
		return self::PROVIDER_KEY;
	}

	/**
	 * Human-readable display name.
	 */
	public function label(): string {
		return self::LABEL;
	}

	/**
	 * Mercari is used as a search-link target, not a normalized API provider.
	 */
	public function capabilities(): ProviderCapabilities {
		return new ProviderCapabilities(
			true,
			false,
			false,
			false,
			false,
			false,
			false,
			false,
			true,
			false,
			false
		);
	}

	/**
	 * Report whether the named feature is supported.
	 *
	 * @param string $feature Feature token.
	 *
	 * @throws InvalidArgumentException When `$feature` is out of set.
	 */
	public function supportsFeature( string $feature ): bool {
		return $this->capabilities()->supportsFeature( $feature );
	}

	/**
	 * Mercari link target is deterministic and does not need an API health probe.
	 */
	public function healthCheck(): ProviderHealthStatus {
		return ProviderHealthStatus::healthy( new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ) );
	}

	/**
	 * Return a synthetic Mercari search-link result.
	 *
	 * Mercari has no supported public item-search API for normalized product
	 * rows. The editor search still needs a saveable target, so one deterministic
	 * result points to the Mercari keyword search page.
	 *
	 * @param SearchQuery $query Normalized search query.
	 */
	public function searchProducts( SearchQuery $query ): SearchResult {
		$title = 'メルカリで「' . $query->keyword() . '」を検索';
		$url   = $this->buildSearchUrl( $query->keyword() );
		$now   = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
		$id    = 'search-' . substr( hash( 'sha256', $query->keyword() ), 0, 16 );

		$snapshot = new ProductSnapshot(
			self::PROVIDER_KEY,
			new ProviderProductId( self::PROVIDER_KEY, $id ),
			$title,
			self::LABEL,
			'Mercari の検索結果ページへ移動します。商品ごとの価格・在庫はリンク先で確認してください。',
			$url,
			$url,
			null,
			array(),
			null,
			AvailabilityInfo::unknown(),
			null,
			null,
			array( self::LABEL ),
			array( 'search_query' => $query->keyword() ),
			$now,
			array( 'synthetic_search_link' => true )
		);

		return new SearchResult(
			self::PROVIDER_KEY,
			array( $snapshot ),
			$query->page(),
			$query->perPage(),
			1,
			false,
			array( 'mode' => 'search_link' )
		);
	}

	/**
	 * Stamp a Mercari URL with the Ambassador `afid` when configured.
	 *
	 * @param ProductSnapshot  $snapshot Snapshot to link.
	 * @param AffiliateContext $context  Per-request context.
	 */
	public function buildAffiliateLink( ProductSnapshot $snapshot, AffiliateContext $context ): AffiliateLinkResult {
		unset( $context );
		$url = $this->buildAffiliateUrl( $snapshot->detailUrl() );
		if ( $url === $snapshot->detailUrl() ) {
			return AffiliateLinkResult::passthrough( $url );
		}

		return new AffiliateLinkResult(
			$url,
			array( 'nofollow', 'sponsored', 'noopener', 'noreferrer' ),
			'_blank',
			true,
			array()
		);
	}

	/**
	 * Build a Mercari search URL and stamp `afid` when configured.
	 *
	 * @param string $query Search query.
	 */
	public function buildSearchUrl( string $query ): string {
		$url = 'https://jp.mercari.com/search?keyword=' . rawurlencode( $query );
		return $this->buildAffiliateUrl( $url );
	}

	/**
	 * Stamp the supplied Mercari URL with `afid` when configured.
	 *
	 * @param string $url Mercari URL.
	 */
	public function buildAffiliateUrl( string $url ): string {
		if ( $this->affiliateId === '' ) {
			return $url;
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Provider helper is loadable in pure-unit tests without WordPress.
		$parts = parse_url( $url );
		if ( ! is_array( $parts ) ) {
			return $url;
		}

		$host = isset( $parts['host'] ) ? strtolower( (string) $parts['host'] ) : '';
		if ( $host !== 'jp.mercari.com' ) {
			return $url;
		}

		$query = array();
		if ( isset( $parts['query'] ) && is_string( $parts['query'] ) ) {
			parse_str( $parts['query'], $query );
		}
		$query         = self::orderAmbassadorQuery( $query );
		$query['afid'] = $this->affiliateId;

		$scheme = isset( $parts['scheme'] ) ? (string) $parts['scheme'] : 'https';
		$path   = isset( $parts['path'] ) ? (string) $parts['path'] : '';
		$built  = $scheme . '://' . $host . $path . '?' . http_build_query( $query, '', '&', PHP_QUERY_RFC3986 );
		if ( isset( $parts['fragment'] ) && is_string( $parts['fragment'] ) && $parts['fragment'] !== '' ) {
			$built .= '#' . $parts['fragment'];
		}

		return $built;
	}

	/**
	 * Reorder known Mercari ad parameters to match Ambassador-generated markup.
	 *
	 * @param array<string, mixed> $query Query map.
	 *
	 * @return array<string, mixed>
	 */
	private static function orderAmbassadorQuery( array $query ): array {
		$ordered = array();
		foreach ( array( 'ad_auction_id', 'ad_auction_unit_id', 'ad_id' ) as $key ) {
			if ( array_key_exists( $key, $query ) ) {
				$ordered[ $key ] = $query[ $key ];
				unset( $query[ $key ] );
			}
		}

		foreach ( $query as $key => $value ) {
			$ordered[ $key ] = $value;
		}

		return $ordered;
	}

	/**
	 * Build the Mercari Ambassador image URL for a product detail URL.
	 *
	 * @param string $url Mercari product URL.
	 */
	public static function ambassadorImageUrlFor( string $url ): ?string {
		// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Provider helper is loadable in pure-unit tests without WordPress.
		$parts = parse_url( $url );
		if ( ! is_array( $parts ) || ! isset( $parts['path'] ) || ! is_string( $parts['path'] ) ) {
			return null;
		}

		$path = $parts['path'];
		if ( preg_match( '~/shops/product/([^/?#]+)~', $path, $matches ) === 1 ) {
			return self::AMBASSADOR_IMAGE_ENDPOINT . '?id=' . rawurlencode( $matches[1] ) . '&svc=ms';
		}

		if ( preg_match( '~/item/([^/?#]+)~', $path, $matches ) === 1 ) {
			return self::AMBASSADOR_IMAGE_ENDPOINT . '?id=' . rawurlencode( $matches[1] ) . '&svc=m';
		}

		return null;
	}
}
