<?php
/**
 * Recording product-editor POST remapper.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\Recording;

// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals -- scheduled_changes_ is the product prefix.

use ScheduledChanges\Domain\Recording\RecordingSession;

/**
 * Rewrites safe source IDs in product-editor payloads to shadow IDs.
 */
final class RecordingPostRemapper {
	/**
	 * Remaps product-editor POST data in place.
	 *
	 * @param array<string,mixed> $post Product-editor POST payload.
	 * @param RecordingSession    $session Recording session.
	 * @return array<string,mixed>
	 */
	public function remap( array $post, RecordingSession $session ): array {
		$id_lookup = $this->sourceToShadowLookup( $session );

		if ( isset( $post['variable_post_id'] ) && is_array( $post['variable_post_id'] ) ) {
			foreach ( $post['variable_post_id'] as $index => $value ) {
				$post['variable_post_id'][ $index ] = $this->remapVariationId( $value, $id_lookup );
			}
		}

		if ( isset( $post['new_variation_menu_order_id'] ) ) {
			$post['new_variation_menu_order_id'] = $this->remapVariationId( $post['new_variation_menu_order_id'], $id_lookup );
		}

		foreach ( $this->remapDefinitions( $session, $id_lookup ) as $definition ) {
			$key  = (string) ( $definition['key'] ?? '' );
			$mode = (string) ( $definition['mode'] ?? '' );

			if ( '' === $key || '' === $mode || ! array_key_exists( $key, $post ) ) {
				continue;
			}

			$post[ $key ] = $this->remapValue( $post[ $key ], $mode, $id_lookup );
		}

		return $post;
	}

	/**
	 * @param RecordingSession $session Recording session.
	 * @return array<int,int>
	 */
	private function sourceToShadowLookup( RecordingSession $session ): array {
		$source_to_shadow = $session->variationMap()['source_to_shadow'] ?? array();

		if ( ! is_array( $source_to_shadow ) ) {
			return array();
		}

		$lookup = array();

		foreach ( $source_to_shadow as $source_id => $shadow_id ) {
			$source_id = $this->positiveInt( $source_id );
			$shadow_id = $this->positiveInt( $shadow_id );

			if ( $source_id > 0 && $shadow_id > 0 ) {
				$lookup[ $source_id ] = $shadow_id;
			}
		}

		return $lookup;
	}

	/**
	 * @param RecordingSession $session Recording session.
	 * @param array<int,int>   $id_lookup Source variation ID to shadow variation ID.
	 * @return array<int,array{key:string,mode:string}>
	 */
	private function remapDefinitions( RecordingSession $session, array $id_lookup ): array {
		$definitions = array(
			array(
				'key'  => '_wc_variation_file_names',
				'mode' => 'array_keys',
			),
			array(
				'key'  => '_wc_variation_file_urls',
				'mode' => 'array_keys',
			),
			array(
				'key'  => '_wc_variation_file_hashes',
				'mode' => 'array_keys',
			),
		);

		/**
		 * Filters source-to-shadow remap rules for recording shadow saves.
		 *
		 * Each rule must include a POST key and one of these modes:
		 * array_keys, array_values, single_value, or nested_array_keys.
		 *
		 * @param array<int,array{key:string,mode:string}> $definitions Remap definitions.
		 * @param RecordingSession                         $session Recording session.
		 * @param array<int,int>                            $id_lookup Source variation ID to shadow variation ID.
		 */
		$filtered = apply_filters(
			'scheduled_changes_recording_shadow_post_remap_keys',
			$definitions,
			$session,
			$id_lookup
		);

		if ( ! is_array( $filtered ) ) {
			return $definitions;
		}

		$valid = array();

		foreach ( $filtered as $definition ) {
			if ( ! is_array( $definition ) ) {
				continue;
			}

			$key  = (string) ( $definition['key'] ?? '' );
			$mode = (string) ( $definition['mode'] ?? '' );

			if (
				'' === $key
				|| ! in_array( $mode, array( 'array_keys', 'array_values', 'single_value', 'nested_array_keys' ), true )
			) {
				continue;
			}

			$valid[] = array(
				'key'  => $key,
				'mode' => $mode,
			);
		}

		return $valid;
	}

	/**
	 * @param mixed          $value Value to remap.
	 * @param string         $mode Remap mode.
	 * @param array<int,int> $id_lookup Source variation ID to shadow variation ID.
	 * @return mixed
	 */
	private function remapValue( $value, string $mode, array $id_lookup ) {
		switch ( $mode ) {
			case 'array_keys':
				return is_array( $value ) ? $this->remapArrayKeys( $value, $id_lookup ) : $value;
			case 'array_values':
				return is_array( $value ) ? $this->remapArrayValues( $value, $id_lookup ) : $value;
			case 'nested_array_keys':
				return is_array( $value ) ? $this->remapNestedArrayKeys( $value, $id_lookup ) : $value;
			case 'single_value':
				return $this->remapScalarId( $value, $id_lookup );
			default:
				return $value;
		}
	}

	/**
	 * @param array<mixed>   $value Value array.
	 * @param array<int,int> $id_lookup Source variation ID to shadow variation ID.
	 * @return array<mixed>
	 */
	private function remapArrayKeys( array $value, array $id_lookup ): array {
		$remapped = array();

		foreach ( $value as $key => $item ) {
			$numeric_key = is_int( $key ) || ctype_digit( (string) $key ) ? (int) $key : null;
			$new_key     = null === $numeric_key ? $key : $this->remapVariationId( $numeric_key, $id_lookup );

			$remapped[ $new_key ] = $item;
		}

		return $remapped;
	}

	/**
	 * @param array<mixed>   $value Value array.
	 * @param array<int,int> $id_lookup Source variation ID to shadow variation ID.
	 * @return array<mixed>
	 */
	private function remapArrayValues( array $value, array $id_lookup ): array {
		foreach ( $value as $key => $item ) {
			$value[ $key ] = is_array( $item )
				? $this->remapArrayValues( $item, $id_lookup )
				: $this->remapScalarId( $item, $id_lookup );
		}

		return $value;
	}

	/**
	 * @param array<mixed>   $value Value array.
	 * @param array<int,int> $id_lookup Source variation ID to shadow variation ID.
	 * @return array<mixed>
	 */
	private function remapNestedArrayKeys( array $value, array $id_lookup ): array {
		$value = $this->remapArrayKeys( $value, $id_lookup );

		foreach ( $value as $key => $item ) {
			if ( is_array( $item ) ) {
				$value[ $key ] = $this->remapNestedArrayKeys( $item, $id_lookup );
			}
		}

		return $value;
	}

	/**
	 * @param mixed          $value Scalar value.
	 * @param array<int,int> $id_lookup Source variation ID to shadow variation ID.
	 * @return mixed
	 */
	private function remapScalarId( $value, array $id_lookup ) {
		return $this->remapVariationId( $value, $id_lookup );
	}

	/**
	 * @param mixed          $value Value that may contain a source or shadow variation ID.
	 * @param array<int,int> $id_lookup Source variation ID to shadow variation ID.
	 * @return mixed
	 */
	private function remapVariationId( $value, array $id_lookup ) {
		$id = $this->positiveInt( $value );

		if ( $id <= 0 ) {
			return $value;
		}

		if ( isset( $id_lookup[ $id ] ) ) {
			return is_string( $value ) ? (string) $id_lookup[ $id ] : $id_lookup[ $id ];
		}

		if ( in_array( $id, array_values( $id_lookup ), true ) ) {
			return $value;
		}

		throw new \RuntimeException( 'Recording shadow save payload contained an unknown variation ID.' );
	}

	/**
	 * @param mixed $value
	 */
	private function positiveInt( $value ): int {
		if ( is_int( $value ) ) {
			return max( 0, $value );
		}

		if ( is_string( $value ) && ctype_digit( $value ) ) {
			return (int) $value;
		}

		return 0;
	}
}
