<?php
/**
 * Update saved product manual fields use case.
 *
 * @package Affilun\Application\Product\UseCase
 */

declare(strict_types=1);

namespace Affilun\Application\Product\UseCase;

use Affilun\Application\Product\ProductManualTaxonomy;
use Affilun\Infrastructure\Persistence\ProductListQuery;
use Affilun\Infrastructure\Persistence\ProductMetaRepository;
use Affilun\Infrastructure\Persistence\ProductRecord;
use Affilun\Infrastructure\Persistence\ProductRepository;
use Affilun\Infrastructure\Persistence\ProductUrlRecord;
use Affilun\Infrastructure\Persistence\ProductUrlRepository;
use DateTimeImmutable;
use DateTimeZone;
use InvalidArgumentException;
use RuntimeException;

/**
 * Applies validated manual edits to a saved product snapshot.
 */
final class UpdateProductManualFieldsUseCase {

	/**
	 * Repository.
	 */
	private ProductRepository $repository;

	/**
	 * Manual taxonomy helper.
	 */
	private ?ProductManualTaxonomy $taxonomy;

	/**
	 * Optional provider URL repository.
	 */
	private ?ProductUrlRepository $urlRepository;

	/**
	 * @param ProductRepository          $repository Product repository.
	 * @param ProductMetaRepository|null $metaRepository Optional meta repository.
	 * @param ProductUrlRepository|null  $urlRepository Optional URL repository.
	 */
	public function __construct(
		ProductRepository $repository,
		?ProductMetaRepository $metaRepository = null,
		?ProductUrlRepository $urlRepository = null
	) {
		$this->repository    = $repository;
		$this->taxonomy      = $metaRepository === null ? null : new ProductManualTaxonomy( $metaRepository );
		$this->urlRepository = $urlRepository;
	}

	/**
	 * Execute update.
	 *
	 * @param int                 $productId Product id.
	 * @param array<string,mixed> $fields    Fields.
	 *
	 * @throws InvalidArgumentException When input is invalid.
	 * @throws RuntimeException         When product does not exist.
	 */
	public function execute( int $productId, array $fields ): ProductRecord {
		if ( $productId <= 0 ) {
			throw new InvalidArgumentException( 'Product id must be positive.' );
		}

		$record = $this->repository->findById( $productId );
		if ( $record === null ) {
			throw new RuntimeException( 'Product not found.' );
		}

		$row                      = $record->toArray();
		$row['title']             = self::requiredString( $fields, 'title' );
		$row['image_primary_url'] = self::optionalHttpUrl( $fields, 'image_primary_url' );
		$row['detail_url']        = self::requiredHttpUrl( $fields, 'detail_url' );
		$row['affiliate_url']     = self::optionalHttpUrl( $fields, 'affiliate_url' );
		$row['price_label']       = self::optionalString( $fields, 'price_label' );
		$row['lifecycle_state']   = self::lifecycleState( $fields['lifecycle_state'] ?? null );
		$now                      = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
		$row['updated_at']        = $now;
		$row['archived_at']       = $row['lifecycle_state'] === ProductListQuery::LIFECYCLE_STATE_ARCHIVED
			? $row['updated_at']
			: null;
		if ( array_key_exists( 'brand', $fields ) ) {
			$row['brand'] = self::optionalString( $fields, 'brand' );
		}

		$updated   = $this->repository->save( ProductRecord::fromArray( $row ) );
		$updatedId = $updated->id();
		if ( $updatedId !== null ) {
			$this->syncProviderUrls( $updated, $fields, $now );
		}

		if ( $this->taxonomy !== null && $updatedId !== null ) {
			if ( array_key_exists( 'categories', $fields ) ) {
				$this->taxonomy->saveCategoriesForProduct(
					$updatedId,
					ProductManualTaxonomy::termsFromInput( $fields['categories'] )
				);
			}
			if ( array_key_exists( 'tags', $fields ) ) {
				$this->taxonomy->saveTagsForProduct(
					$updatedId,
					ProductManualTaxonomy::termsFromInput( $fields['tags'] )
				);
			}
		}

		return $updated;
	}

	/**
	 * Synchronize provider URL rows from edit fields.
	 *
	 * @param ProductRecord       $record Saved product.
	 * @param array<string,mixed> $fields Fields.
	 * @param DateTimeImmutable   $now    Update timestamp.
	 */
	private function syncProviderUrls( ProductRecord $record, array $fields, DateTimeImmutable $now ): void {
		if ( $this->urlRepository === null || ! array_key_exists( 'provider_urls', $fields ) ) {
			return;
		}

		$productId = $record->id();
		if ( $productId === null ) {
			return;
		}

		$providerUrls = $fields['provider_urls'];
		if ( ! is_array( $providerUrls ) ) {
			return;
		}

		foreach ( $providerUrls as $provider => $entry ) {
			if ( ! is_string( $provider ) || ! in_array( $provider, self::supportedProviders(), true ) ) {
				continue;
			}
			if ( ! is_array( $entry ) ) {
				continue;
			}

			$url = self::optionalHttpUrl( $entry, 'url' );
			$this->urlRepository->deleteByProductAndProvider( $productId, $provider );
			if ( $url === null ) {
				continue;
			}

			$providerItemId = self::optionalString( $entry, 'provider_item_id' );
			if ( $provider === $record->provider() && $providerItemId === null ) {
				$providerItemId = $record->providerItemId();
			}

			$this->urlRepository->save(
				ProductUrlRecord::createNew(
					$productId,
					$provider,
					$providerItemId,
					$url,
					$now,
					array( 'source' => 'user' )
				)
			);
		}
	}

	/**
	 * Supported provider keys.
	 *
	 * @return list<string>
	 */
	private static function supportedProviders(): array {
		return array( 'amazon', 'rakuten', 'yahoo_shopping', 'mercari' );
	}

	/**
	 * Read required string.
	 *
	 * @param array<string,mixed> $fields Fields.
	 * @param string              $key    Key.
	 *
	 * @throws InvalidArgumentException When the value is absent.
	 */
	private static function requiredString( array $fields, string $key ): string {
		$value = self::optionalString( $fields, $key );
		if ( $value === null ) {
			throw new InvalidArgumentException( 'Required product field is missing.' );
		}
		return $value;
	}

	/**
	 * Read optional string.
	 *
	 * @param array<string,mixed> $fields Fields.
	 * @param string              $key    Key.
	 */
	private static function optionalString( array $fields, string $key ): ?string {
		$value = $fields[ $key ] ?? null;
		if ( ! is_scalar( $value ) ) {
			return null;
		}
		$trimmed = trim( (string) $value );
		return $trimmed === '' ? null : $trimmed;
	}

	/**
	 * Read required HTTP(S) URL.
	 *
	 * @param array<string,mixed> $fields Fields.
	 * @param string              $key    Key.
	 *
	 * @throws InvalidArgumentException When the URL is absent or invalid.
	 */
	private static function requiredHttpUrl( array $fields, string $key ): string {
		$value = self::optionalHttpUrl( $fields, $key );
		if ( $value === null ) {
			throw new InvalidArgumentException( 'Required product URL field must be an HTTP(S) URL.' );
		}
		return $value;
	}

	/**
	 * Read optional HTTP(S) URL.
	 *
	 * @param array<string,mixed> $fields Fields.
	 * @param string              $key    Key.
	 *
	 * @throws InvalidArgumentException When the URL is invalid.
	 */
	private static function optionalHttpUrl( array $fields, string $key ): ?string {
		$value = self::optionalString( $fields, $key );
		if ( $value === null ) {
			return null;
		}
		if ( preg_match( '#\Ahttps?://#i', $value ) !== 1 ) {
			throw new InvalidArgumentException( 'Product URL field must be an HTTP(S) URL.' );
		}
		return $value;
	}

	/**
	 * Validate lifecycle state.
	 *
	 * @param mixed $value Value.
	 *
	 * @throws InvalidArgumentException When the state is invalid.
	 */
	private static function lifecycleState( mixed $value ): string {
		$state = is_scalar( $value ) ? trim( (string) $value ) : ProductListQuery::LIFECYCLE_STATE_ACTIVE;
		$valid = array(
			ProductListQuery::LIFECYCLE_STATE_ACTIVE,
			ProductListQuery::LIFECYCLE_STATE_ARCHIVED,
			ProductListQuery::LIFECYCLE_STATE_DELETED_PENDING,
		);
		if ( ! in_array( $state, $valid, true ) ) {
			throw new InvalidArgumentException( 'Invalid lifecycle state.' );
		}
		return $state;
	}
}
