<?php
/**
 * Legacy product-link scanner.
 *
 * @package Affilun\Application\Migration
 */

declare(strict_types=1);

namespace Affilun\Application\Migration;

/**
 * Detects neutral legacy product-link patterns without mutating content.
 */
final class LegacyProductLinkScanner {

	/**
	 * Shortcode tags treated as product-link-like legacy sources.
	 *
	 * @var list<string>
	 */
	private const SHORTCODE_TAGS = array(
		'product_link',
		'product-card',
		'affiliate_product',
		'affiliate-card',
		'legacy_product_link',
	);

	/**
	 * Scan one post.
	 *
	 * @param PostScanItem $post Post.
	 *
	 * @return list<array{post_id:int,post_title:string,kind:string,source:string,offset:int,confidence:int,preview:string}>
	 */
	public function scanPost( PostScanItem $post ): array {
		$matches = array();
		foreach ( self::SHORTCODE_TAGS as $tag ) {
			$matches = array_merge( $matches, $this->scanShortcodeTag( $post, $tag ) );
		}

		$matches = array_merge( $matches, $this->scanMarketplaceLinks( $post ) );
		$matches = array_merge( $matches, $this->scanCardLikeHtml( $post ) );

		usort(
			$matches,
			static fn ( array $a, array $b ): int => $a['offset'] <=> $b['offset']
		);

		return $matches;
	}

	/**
	 * Scan several posts and return a dry-run report.
	 *
	 * @param list<PostScanItem> $posts Posts.
	 *
	 * @return array{posts_scanned:int,matches:list<array{post_id:int,post_title:string,kind:string,source:string,offset:int,confidence:int,preview:string}>}
	 */
	public function dryRun( array $posts ): array {
		$matches = array();
		foreach ( $posts as $post ) {
			$matches = array_merge( $matches, $this->scanPost( $post ) );
		}

		return array(
			'posts_scanned' => count( $posts ),
			'matches'       => $matches,
		);
	}

	/**
	 * Scan one shortcode tag.
	 *
	 * @param PostScanItem $post Post.
	 * @param string       $tag  Shortcode tag.
	 *
	 * @return list<array{post_id:int,post_title:string,kind:string,source:string,offset:int,confidence:int,preview:string}>
	 */
	private function scanShortcodeTag( PostScanItem $post, string $tag ): array {
		$matches = array();
		$pattern = '/\[\s*' . preg_quote( $tag, '/' ) . '\b[^\]]*\]/i';
		if ( preg_match_all( $pattern, $post->content(), $found, PREG_OFFSET_CAPTURE ) !== false ) {
			foreach ( $found[0] as $match ) {
				$matches[] = $this->buildMatch(
					$post,
					'shortcode',
					$tag,
					(int) $match[1],
					95,
					(string) $match[0]
				);
			}
		}
		return $matches;
	}

	/**
	 * Scan product-like marketplace links.
	 *
	 * @param PostScanItem $post Post.
	 *
	 * @return list<array{post_id:int,post_title:string,kind:string,source:string,offset:int,confidence:int,preview:string}>
	 */
	private function scanMarketplaceLinks( PostScanItem $post ): array {
		$matches = array();
		$pattern = '~https?://[^\s"\']*(?:/dp/[A-Z0-9]{8,}|item\.rakuten\.co\.jp|shopping\.yahoo\.co\.jp)[^\s"\']*~i';
		if ( preg_match_all( $pattern, $post->content(), $found, PREG_OFFSET_CAPTURE ) !== false ) {
			foreach ( $found[0] as $match ) {
				$matches[] = $this->buildMatch(
					$post,
					'marketplace_link',
					'marketplace_url',
					(int) $match[1],
					80,
					(string) $match[0]
				);
			}
		}
		return $matches;
	}

	/**
	 * Scan generic product-card-like HTML classes.
	 *
	 * @param PostScanItem $post Post.
	 *
	 * @return list<array{post_id:int,post_title:string,kind:string,source:string,offset:int,confidence:int,preview:string}>
	 */
	private function scanCardLikeHtml( PostScanItem $post ): array {
		$matches = array();
		$pattern = '/<(?:div|section|article)\b[^>]*class=["\'][^"\']*'
			. '(?:product-link|affiliate-card|product-card)[^"\']*["\'][^>]*>/i';
		if ( preg_match_all( $pattern, $post->content(), $found, PREG_OFFSET_CAPTURE ) !== false ) {
			foreach ( $found[0] as $match ) {
				$matches[] = $this->buildMatch(
					$post,
					'html_card',
					'class_name',
					(int) $match[1],
					70,
					(string) $match[0]
				);
			}
		}
		return $matches;
	}

	/**
	 * Build a report match.
	 *
	 * @param PostScanItem $post       Post.
	 * @param string       $kind       Match kind.
	 * @param string       $source     Source token.
	 * @param int          $offset     Offset.
	 * @param int          $confidence Confidence.
	 * @param string       $preview    Preview.
	 *
	 * @return array{post_id:int,post_title:string,kind:string,source:string,offset:int,confidence:int,preview:string}
	 */
	private function buildMatch(
		PostScanItem $post,
		string $kind,
		string $source,
		int $offset,
		int $confidence,
		string $preview
	): array {
		return array(
			'post_id'    => $post->id(),
			'post_title' => $post->title(),
			'kind'       => $kind,
			'source'     => $source,
			'offset'     => max( 0, $offset ),
			'confidence' => max( 0, min( 100, $confidence ) ),
			'preview'    => $this->preview( $preview ),
		);
	}

	/**
	 * Normalize a preview string.
	 *
	 * @param string $value Preview candidate.
	 */
	private function preview( string $value ): string {
		$plain = trim( preg_replace( '/\s+/', ' ', $value ) ?? $value );
		if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) {
			return mb_strlen( $plain ) > 140 ? mb_substr( $plain, 0, 137 ) . '...' : $plain;
		}
		return strlen( $plain ) > 140 ? substr( $plain, 0, 137 ) . '...' : $plain;
	}
}
