<?php
/**
 * Screen label formatter manager.
 *
 * @package Ealab\Deployer
 */

namespace Ealab\Deployer\Tracking;

use Dakwamine\Component\ComponentBasedObject;
use Dakwamine\Component\Exception\UnmetDependencyException;
use Dakwamine\Component\RootDependencyDefinition;
use Ealab\Deployer\Exception\LabelFormatterException;
use Ealab\Deployer\Exception\WpDeployException;
use Ealab\Deployer\Tracking\ScreenLabelFormatter\ScreenLabelFormatterBase;

/**
 * Screen label formatter manager.
 */
class ScreenLabelFormatterManager extends ComponentBasedObject {
	/**
	 * The class names, keyed by their ID.
	 *
	 * @var array<string,string>
	 */
	private array $class_names = array();

	/**
	 * Whether the definitions have been loaded.
	 *
	 * @var bool
	 */
	private bool $has_loaded_class_names = false;

	/**
	 * Stack of item full UUIDs being processed by formatters.
	 *
	 * It helps prevent infinite recursions.
	 *
	 * @var array<string>
	 */
	private array $processing_items_stack = array();

	/**
	 * The tracking manager.
	 *
	 * @var TrackingManager|null
	 */
	private ?TrackingManager $tracking_manager = null;

	/**
	 * Formats the screen label from the given deployment item.
	 *
	 * This is the entrypoint for label formatting.
	 *
	 * @param string              $bundle_subdirectory The bundle subdirectory.
	 * @param array<string,mixed> $file_item The file item containing the deployment data.
	 *
	 * @return string|null The formatted screen label, or null if the formatter could not be found
	 *   or the formatting failed.
	 */
	public function formatFromFileItem( string $bundle_subdirectory, array $file_item = array() ): ?string {
		try {
			return $this->formatFromFileItemRecursive( $bundle_subdirectory, $file_item ) ?? null;
		} catch ( WpDeployException $e ) {
			// Make this error non-blocking.
			// TODO Attempt to return a default label?
			return null;
		} finally {
			// Clean up the stack.
			$this->processing_items_stack = array();
		}
	}

	/**
	 * Formats the screen label from the given deployment item.
	 *
	 * Call this method when recursion is involved (e.g. referenced item label formatters).
	 * Use formatFromFileItem() instead as the entrypoint for label formatting.
	 *
	 * @param string              $bundle_subdirectory The bundle subdirectory.
	 * @param array<string,mixed> $file_item The file item containing the deployment data.
	 *
	 * @return string|null The formatted screen label, or null if the formatter could not be found
	 *   or the formatting failed softly.
	 *
	 * @throws LabelFormatterException|WpDeployException Hard failure while formatting the screen label.
	 */
	public function formatFromFileItemRecursive( string $bundle_subdirectory, array $file_item = array() ): ?string {
		if ( empty( $bundle_subdirectory ) ) {
			return null;
		}

		$tracker = $file_item['tracker'] ?? null;

		if ( empty( $tracker ) ) {
			throw new LabelFormatterException( 'The tracker ID is not set in the item to get the label for.' );
		}

		$item_tracker = $this->tracking_manager->getItemTrackerByTrackerId( $tracker );

		if ( empty( $item_tracker ) ) {
			// The tracker is not available.
			// It could be a tracker from a plugin which is not (yet) active.
			throw new LabelFormatterException( 'The tracker managing the item to get the label for is not (yet) available.' );
		}

		// Get the formatter configuration from the item tracker.
		$config = $item_tracker->getScreenLabelFormatterConfig();

		return $this->getFormatterByConfig( $config )->formatFromFileItem( $bundle_subdirectory, $file_item, $config['config'] ?? null ) ?? null;
	}

	/**
	 * Formats the screen label from the given trackable item.
	 *
	 * This is the entrypoint for label formatting.
	 *
	 * @param TrackableItem $trackable_item The trackable item to format. The formatter will be deduced from.
	 *
	 * @return string|null The formatted screen label, or null if the formatter could not be found
	 *   or the formatting failed.
	 */
	public function formatFromTrackableItem( TrackableItem $trackable_item ): ?string {
		try {
			return $this->formatFromTrackableItemRecursive( $trackable_item ) ?? null;
		} catch ( WpDeployException $e ) {
			// Make this error non-blocking.
			// TODO Attempt to return a default label?
			return null;
		} finally {
			// Clean up the stack.
			$this->processing_items_stack = array();
		}
	}

	/**
	 * Formats the screen label from the given trackable item.
	 *
	 * Call this method when recursion is involved (e.g. referenced item label formatters).
	 * Use formatFromFileItem() instead as the entrypoint for label formatting.
	 *
	 * @param TrackableItem $trackable_item The trackable item to format. The formatter will be deduced from.
	 *
	 * @return string|null The formatted screen label, or null if the formatter could not be found
	 *   or the formatting failed softly.
	 *
	 * @throws LabelFormatterException|WpDeployException Hard failure while formatting the screen label.
	 */
	public function formatFromTrackableItemRecursive( TrackableItem $trackable_item ): ?string {
		// Get the formatter configuration from the item tracker.
		$config = $trackable_item->getRegistry()->getItemTracker()->getScreenLabelFormatterConfig();

		return $this->getFormatterByConfig( $config )->formatFromTrackableItem( $trackable_item, $config['config'] ?? array() ) ?? null;
	}

	/**
	 * Gets the class name for the given ID.
	 *
	 * @param string|null $id The ID. If empty, the default class name will be returned.
	 *
	 * @return class-string<ScreenLabelFormatterBase> The class name.
	 *   Defaults to ScreenLabelFormatterBase::class if the ID is empty
	 *   or the definition for the given ID is invalid or not found.
	 */
	private function getClassNameById( ?string $id ): string {
		if ( empty( $id ) ) {
			return ScreenLabelFormatterBase::class;
		}

		$class_name = $this->getClassNames()[ $id ] ?? null;

		return empty( $class_name ) ? ScreenLabelFormatterBase::class : $class_name;
	}

	/**
	 * Gets the class names.
	 *
	 * @return array<string,string> The class names.
	 */
	private function getClassNames(): array {
		if ( ! $this->has_loaded_class_names ) {
			$this->class_names            = apply_filters( 'deploy_sync_content__screen_label_formatter_manager__get_class_names', array() );
			$this->has_loaded_class_names = true;
		}

		return $this->class_names;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getDependencyDefinitions(): array {
		$definitions   = parent::getDependencyDefinitions();
		$definitions[] = new RootDependencyDefinition( TrackingManager::class, $this->tracking_manager );

		return $definitions;
	}

	/**
	 * Loads the item tracker for the given config.
	 *
	 * If the item tracker is already instantiated, this instance will be returned.
	 *
	 * @param array<string,mixed> $config The config of the instance.
	 *   See ScreenLabelFormatterBase::$config for the list of base keys, and the extending classes
	 *   for the list of additional keys.
	 *
	 * @return ScreenLabelFormatterBase|null The created instance, or null if the instance could not be created.
	 *
	 * @throws WpDeployException Error while instantiating the item tracker.
	 */
	private function getFormatterByConfig( array $config ): ?ScreenLabelFormatterBase {
		$class = $this->getClassNameById( $config['id'] ?? null );

		try {
			/** @var ScreenLabelFormatterBase $instance */
			$instance = $this->getComponentByClassName( $class, true );
		} catch ( UnmetDependencyException $e ) {
			// Ignore this manager if it is not ready.
			throw new WpDeployException( 'Error while initializing the screen label formatter instance.' );
		}

		return $instance;
	}

	/**
	 * Checks whether the UUID of the item which is being asked for its screen label is already stacked.
	 *
	 * @param string $uuid Full UUID of the item.
	 *
	 * @return bool True if stacked, false otherwise.
	 */
	public function isStacked( string $uuid ): bool {
		return in_array( $uuid, $this->processing_items_stack, true );
	}

	/**
	 * Stacks the UUID of the item which is being asked for its screen label.
	 *
	 * The script which is asking for the screen label of an item is responsible for unstacking it.
	 *
	 * @param string $uuid Full UUID of the item.
	 */
	public function stack( string $uuid ): void {
		$this->processing_items_stack[] = $uuid;
	}

	/**
	 * Unstacks the UUID of the item which is being asked for its screen label.
	 *
	 * @param string $uuid Full UUID of the item.
	 *
	 * @throws WpDeployException If the UUID of the item which is being asked for its screen label is not
	 *   on top of the stack.
	 */
	public function unstack( string $uuid ): void {
		$last_uuid = array_pop( $this->processing_items_stack );

		if ( $last_uuid !== $uuid ) {
			throw new WpDeployException( 'Screen label formatter stack is corrupted.' );
		}
	}
}
