<?php
/**
 * Saved-product bulk update 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\ProductRecord;
use Affilun\Infrastructure\Persistence\ProductRepository;
use DateTimeImmutable;
use DateTimeZone;
use InvalidArgumentException;

/**
 * Previews and applies filtered bulk edits to saved products.
 */
final class BulkUpdateProductsUseCase {

	/**
	 * Maximum rows included in preview samples.
	 */
	private const MAX_PREVIEW_RECORDS = 20;

	/**
	 * Product repository.
	 */
	private ProductRepository $products;

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

	/**
	 * @param ProductRepository     $products Product repository.
	 * @param ProductManualTaxonomy $taxonomy Manual taxonomy helper.
	 */
	public function __construct( ProductRepository $products, ProductManualTaxonomy $taxonomy ) {
		$this->products = $products;
		$this->taxonomy = $taxonomy;
	}

	/**
	 * Preview matching products without mutating them.
	 *
	 * @param array<string,mixed> $filters Filter inputs.
	 * @param array<string,mixed> $changes Change inputs.
	 */
	public function preview( array $filters, array $changes ): BulkUpdateProductsResult {
		return $this->run( $filters, $changes, false );
	}

	/**
	 * Apply changes after the caller has shown a preview.
	 *
	 * @param array<string,mixed> $filters Filter inputs.
	 * @param array<string,mixed> $changes Change inputs.
	 */
	public function apply( array $filters, array $changes ): BulkUpdateProductsResult {
		return $this->run( $filters, $changes, true );
	}

	/**
	 * Run preview or mutation.
	 *
	 * @param array<string,mixed> $filters Filter inputs.
	 * @param array<string,mixed> $changes Change inputs.
	 * @param bool                $mutate  Whether to apply changes.
	 *
	 * @throws InvalidArgumentException When the change set is empty or invalid.
	 */
	private function run( array $filters, array $changes, bool $mutate ): BulkUpdateProductsResult {
		$normalizedFilters = self::normalizeFilters( $filters );
		$normalizedChanges = self::normalizeChanges( $changes );
		if ( $normalizedChanges === array() ) {
			throw new InvalidArgumentException( 'At least one bulk product change is required.' );
		}

		$matched = 0;
		$updated = 0;
		$sample  = array();
		$page    = 1;

		do {
			$result = $this->products->findPaginated(
				new ProductListQuery(
					$page,
					50,
					$normalizedFilters['provider'],
					null,
					null,
					null,
					$normalizedFilters['lifecycle_state'],
					ProductListQuery::SORT_KEY_UPDATED_AT,
					ProductListQuery::SORT_ORDER_ASC
				)
			);

			foreach ( $result->items() as $record ) {
				if ( ! $this->matchesCategoryFilter( $record, $normalizedFilters['category'] ) ) {
					continue;
				}

				++$matched;
				if ( count( $sample ) < self::MAX_PREVIEW_RECORDS ) {
					$sample[] = $record;
				}

				if ( $mutate && $this->applyChanges( $record, $normalizedChanges ) ) {
					++$updated;
				}
			}

			++$page;
		} while ( $page <= $result->totalPages() );

		return new BulkUpdateProductsResult( $matched, $updated, $sample );
	}

	/**
	 * Check category filter.
	 *
	 * @param ProductRecord $record   Product record.
	 * @param string|null   $category Category filter.
	 */
	private function matchesCategoryFilter( ProductRecord $record, ?string $category ): bool {
		if ( $category === null ) {
			return true;
		}

		$id = $record->id();
		return $id !== null && $this->taxonomy->productHasCategory( $id, $category );
	}

	/**
	 * Apply normalized changes to one record.
	 *
	 * @param ProductRecord       $record  Product record.
	 * @param array<string,mixed> $changes Normalized changes.
	 */
	private function applyChanges( ProductRecord $record, array $changes ): bool {
		$id = $record->id();
		if ( $id === null ) {
			return false;
		}

		if ( array_key_exists( 'lifecycle_state', $changes ) && is_string( $changes['lifecycle_state'] ) ) {
			$lifecycleState         = $changes['lifecycle_state'];
			$now                    = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
			$row                    = $record->toArray();
			$row['lifecycle_state'] = $lifecycleState;
			$row['archived_at']     = $lifecycleState === ProductListQuery::LIFECYCLE_STATE_ACTIVE
				? null
				: $now;
			$row['updated_at']      = $now;
			$this->products->save( ProductRecord::fromArray( $row ) );
		}

		if ( array_key_exists( 'categories', $changes ) ) {
			$this->taxonomy->saveCategoriesForProduct(
				$id,
				ProductManualTaxonomy::termsFromInput( $changes['categories'] )
			);
		}

		if ( array_key_exists( 'tags', $changes ) ) {
			$this->taxonomy->saveTagsForProduct(
				$id,
				ProductManualTaxonomy::termsFromInput( $changes['tags'] )
			);
		}

		return true;
	}

	/**
	 * Normalize filters.
	 *
	 * @param array<string,mixed> $filters Filter inputs.
	 *
	 * @return array{provider:?string,lifecycle_state:?string,category:?string}
	 */
	private static function normalizeFilters( array $filters ): array {
		return array(
			'provider'        => self::optionalString( $filters['provider'] ?? null ),
			'lifecycle_state' => self::optionalLifecycleState( $filters['lifecycle_state'] ?? null ),
			'category'        => self::firstTermOrNull( $filters['category'] ?? null ),
		);
	}

	/**
	 * Normalize changes.
	 *
	 * @param array<string,mixed> $changes Change inputs.
	 *
	 * @return array<string,mixed>
	 */
	private static function normalizeChanges( array $changes ): array {
		$normalized = array();
		$lifecycle  = self::optionalLifecycleState( $changes['lifecycle_state'] ?? null );
		if ( $lifecycle !== null ) {
			$normalized['lifecycle_state'] = $lifecycle;
		}

		if ( array_key_exists( 'categories', $changes ) ) {
			$normalized['categories'] = ProductManualTaxonomy::termsFromInput( $changes['categories'] );
		}

		if ( array_key_exists( 'tags', $changes ) ) {
			$normalized['tags'] = ProductManualTaxonomy::termsFromInput( $changes['tags'] );
		}

		return $normalized;
	}

	/**
	 * Optional trimmed string.
	 *
	 * @param mixed $value Raw value.
	 */
	private static function optionalString( mixed $value ): ?string {
		if ( ! is_scalar( $value ) ) {
			return null;
		}
		$trimmed = trim( (string) $value );
		return $trimmed === '' ? null : $trimmed;
	}

	/**
	 * Optional lifecycle state.
	 *
	 * @param mixed $value Raw value.
	 *
	 * @throws InvalidArgumentException When lifecycle state is invalid.
	 */
	private static function optionalLifecycleState( mixed $value ): ?string {
		$state = self::optionalString( $value );
		if ( $state === null ) {
			return null;
		}

		$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;
	}

	/**
	 * First normalized term or null.
	 *
	 * @param mixed $value Raw value.
	 */
	private static function firstTermOrNull( mixed $value ): ?string {
		$terms = ProductManualTaxonomy::termsFromInput( $value );
		return $terms[0] ?? null;
	}
}
