<?php
/**
 * Public extension hook gateway.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Extension;

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

use ScheduledChanges\Domain\Operation\Operation;
use ScheduledChanges\Domain\Operation\TargetDefinition;
use ScheduledChanges\Bootstrap\CoreModuleServices;
use ScheduledChanges\Execution\ExecutionState;
use ScheduledChanges\Execution\OperationExecutionResult;
use ScheduledChanges\Execution\ProviderOperationHandler;
use ScheduledChanges\Field\FieldRegistry;
use ScheduledChanges\Provider\ExecutionContext;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\ProviderManager;
use ScheduledChanges\Provider\ProviderRegistry;
use ScheduledChanges\Recording\RecordingLaunchCompilerContract;
use ScheduledChanges\Recording\RecordingWorkspaceAdapter;
use ScheduledChanges\Repository\LaunchRepository;
use ScheduledChanges\Repository\LaunchVersionRepository;
use ScheduledChanges\Repository\RecordingSessionRepository;
use ScheduledChanges\Repository\RunRepository;
use ScheduledChanges\Rollback\CreatedTargetRollbackHandler;
use ScheduledChanges\Taxonomy\TaxonomyRegistration;

/**
 * Centralizes stable public hooks so call sites and documentation stay aligned.
 */
final class PublicHooks {
	/**
	 * Filters distributable Extension manifests.
	 *
	 * @param array<int,mixed> $extensions Extension manifests.
	 * @return array<int,mixed>
	 */
	public static function extensionDefinitions( array $extensions ): array {
		return self::applyArrayFilter( 'scheduled_changes_extensions', $extensions );
	}

	/**
	 * Filters selected Extension IDs for a distribution profile.
	 *
	 * @param string[] $extension_ids Candidate Extension IDs.
	 * @param string   $profile Active distribution profile.
	 * @param array<int,mixed> $available Available Extension manifests.
	 * @return string[]
	 */
	public static function extensionProfileSelection( array $extension_ids, string $profile, array $available ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_extension_profile_selection', $extension_ids, $profile, $available );

		return array_values(
			array_unique(
				array_filter(
					array_map( 'strval', $result )
				)
			)
		);
	}

	/**
	 * Announces the core application services to extensions.
	 */
	public static function coreServicesReady( CoreModuleServices $services ): void {
		do_action( 'scheduled_changes_core_services_ready', $services );
	}

	/**
	 * Filters the module provider used to compose runtime services.
	 *
	 * @param object $provider Default module provider.
	 * @return object
	 */
	public static function coreModuleProvider( object $provider ): object {
		$result = apply_filters( 'scheduled_changes_core_module_provider', $provider );

		return is_object( $result ) ? $result : $provider;
	}

	/**
	 * @param array<int,mixed> $providers Provider services.
	 * @return array<int,mixed>
	 */
	public static function fieldProviders( array $providers ): array {
		return self::applyArrayFilter( 'scheduled_changes_field_providers', $providers );
	}

	/**
	 * @param array<int,mixed> $modules Module manifests.
	 * @return array<int,mixed>
	 */
	public static function moduleDefinitions( array $modules ): array {
		return self::applyArrayFilter( 'scheduled_changes_modules', $modules );
	}

	/**
	 * Filters selected module IDs for a distribution profile.
	 *
	 * @param string[] $module_ids Candidate module IDs.
	 * @param string   $profile Active distribution profile.
	 * @param array<int,mixed> $available Available module manifests.
	 * @return string[]
	 */
	public static function moduleProfileSelection( array $module_ids, string $profile, array $available ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_module_profile_selection', $module_ids, $profile, $available );

		return array_values(
			array_unique(
				array_filter(
					array_map( 'strval', $result )
				)
			)
		);
	}

	/**
	 * Filters the active module distribution profile.
	 */
	public static function moduleDistributionProfile( string $profile ): string {
		$result = apply_filters( 'scheduled_changes_module_distribution_profile', $profile );

		return is_scalar( $result ) ? (string) $result : $profile;
	}

	/**
	 * Filters bundled module manifests.
	 *
	 * @param ModuleDefinition[] $modules Bundled module manifests.
	 * @return ModuleDefinition[]
	 */
	public static function bundledModuleDefinitions( array $modules ): array {
		return self::applyArrayFilter( 'scheduled_changes_bundled_modules', $modules );
	}

	/**
	 * Filters official adapter target definitions.
	 *
	 * @param array<int,mixed> $definitions Adapter target definitions.
	 * @return array<int,mixed>
	 */
	public static function adapterDefinitions( array $definitions ): array {
		return self::applyArrayFilter( 'scheduled_changes_adapter_definitions', $definitions );
	}

	/**
	 * Filters admin data keys selected by active modules.
	 *
	 * @param string[] $keys Data keys.
	 * @param string[] $module_ids Active module IDs.
	 * @return string[]
	 */
	public static function adminDataKeys( array $keys, array $module_ids ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_admin_data_keys', $keys, $module_ids );

		return array_values(
			array_unique(
				array_filter(
					array_map(
						'strval',
						array_filter(
							$result,
							static fn ( $key ): bool => is_scalar( $key )
						)
					)
				)
			)
		);
	}

	/**
	 * Filters screen-specific admin data keys selected by active modules.
	 *
	 * @param array<string,string[]> $screen_keys Screen IDs mapped to data keys.
	 * @param string[]               $module_ids Active module IDs.
	 * @param string[]               $keys Selected data keys.
	 * @return array<string,string[]>
	 */
	public static function adminScreenDataKeys( array $screen_keys, array $module_ids, array $keys ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_admin_screen_data_keys', $screen_keys, $module_ids, $keys );

		return $result;
	}

	/**
	 * @param array<int,mixed> $operations Operation type services.
	 * @return array<int,mixed>
	 */
	public static function operationTypes( array $operations ): array {
		return self::applyArrayFilter( 'scheduled_changes_operation_types', $operations );
	}

	/**
	 * @param string[] $keys Metadata keys that may not be written generically.
	 * @return string[]
	 */
	public static function metadataDenylist( array $keys ): array {
		return self::applyArrayFilter( 'scheduled_changes_metadata_denylist', $keys );
	}

	/**
	 * Filters explicitly allowlisted registered post metadata for native scheduling.
	 *
	 * @param array<int|string,mixed> $fields Metadata keys or keyed field options.
	 * @return array<int|string,mixed>
	 */
	public static function wordpressMetaFields( array $fields, string $post_type ): array {
		return self::applyArrayFilter( 'scheduled_changes_wordpress_meta_fields', $fields, $post_type );
	}

	/**
	 * Filters canonical export target mappings.
	 *
	 * @param array<string,mixed> $mapping Canonical mapping.
	 * @return array<string,mixed>
	 */
	public static function exportTargetMapping( array $mapping, TargetDefinition $target, string $context ): array {
		return self::applyArrayFilter( 'scheduled_changes_export_target_mapping', $mapping, $target, $context );
	}

	/**
	 * Filters imported target IDs and clamps invalid values to zero.
	 *
	 * @param array<string,mixed> $mapping Imported target mapping.
	 */
	public static function importTargetId( int $target_id, array $mapping, string $key, TargetDefinition $target ): int {
		$result = apply_filters( 'scheduled_changes_import_target_id', $target_id, $mapping, $key, $target );
		$result = is_numeric( $result ) ? (int) $result : $target_id;

		return max( 0, $result );
	}

	/**
	 * Filters explicit taxonomy capability registrations.
	 *
	 * @param TaxonomyRegistration[] $registrations Taxonomy registrations.
	 * @return array<int,mixed> Validation is owned by TaxonomyModule.
	 */
	public static function taxonomyRegistrations( array $registrations ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_taxonomy_registrations', $registrations );

		return array_values( $result );
	}

	/**
	 * @param array<int,mixed> $strategies Recording clone strategies.
	 * @return array<int,mixed>
	 */
	public static function recordingCloneStrategies( array $strategies ): array {
		return self::applyArrayFilter( 'scheduled_changes_recording_clone_strategies', $strategies );
	}

	/**
	 * Filters the recording workspace adapter contributed by a provider module.
	 */
	public static function recordingWorkspaceAdapter(
		?RecordingWorkspaceAdapter $adapter,
		ProviderRegistry $providers,
		FieldRegistry $fields,
		RecordingSessionRepository $recordings
	): ?RecordingWorkspaceAdapter {
		$result = apply_filters( 'scheduled_changes_recording_workspace_adapter', $adapter, $providers, $fields, $recordings );

		return $result instanceof RecordingWorkspaceAdapter ? $result : null;
	}

	/**
	 * Filters the recording launch compiler contributed by a provider module.
	 */
	public static function recordingLaunchCompiler(
		?RecordingLaunchCompilerContract $compiler,
		ProviderRegistry $providers,
		FieldRegistry $fields,
		LaunchRepository $launches,
		LaunchVersionRepository $versions
	): ?RecordingLaunchCompilerContract {
		$result = apply_filters( 'scheduled_changes_recording_launch_compiler', $compiler, $providers, $fields, $launches, $versions );

		return $result instanceof RecordingLaunchCompilerContract ? $result : null;
	}

	/**
	 * Filters admin target presentation for provider-owned targets.
	 *
	 * @param array<string,mixed> $target Display row.
	 * @param array<string,mixed> $stored_target Stored target/source row.
	 * @return array<string,mixed>
	 */
	public static function adminTargetRow( array $target, array $stored_target, string $context ): array {
		return self::applyArrayFilter( 'scheduled_changes_admin_target_row', $target, $stored_target, $context );
	}

	/**
	 * Filters targets that can be selected from the admin recording picker.
	 *
	 * @param array<int,array<string,mixed>> $targets Target rows.
	 * @param array<string,mixed>            $args Additional selector context.
	 * @return array<int,array<string,mixed>>
	 */
	public static function recordableTargets( array $targets, int $limit, array $args = array() ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_recordable_targets', $targets, $limit, $args );

		return array_values(
			array_filter(
				$result,
				static fn ( $target ): bool => is_array( $target )
			)
		);
	}

	/**
	 * Filters system rows shown in the admin status screen and support bundle.
	 *
	 * @param array<int,array{label:string,value:string}> $rows System rows.
	 * @return array<int,array{label:string,value:string}>
	 */
	public static function adminSystemRows( array $rows, ?ProviderManager $providers = null ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_admin_system_rows', $rows, $providers );

		return array_values(
			array_filter(
				$result,
				static fn ( $row ): bool => is_array( $row )
					&& isset( $row['label'], $row['value'] )
					&& is_scalar( $row['label'] )
					&& is_scalar( $row['value'] )
			)
		);
	}

	/**
	 * Filters compatibility rows for REST and support diagnostics.
	 *
	 * @param array<string,array<string,mixed>> $compatibility Compatibility rows.
	 * @return array<string,array<string,mixed>>
	 */
	public static function systemCompatibility( array $compatibility, ?ProviderManager $providers = null ): array {
		return self::applyArrayFilter( 'scheduled_changes_system_compatibility', $compatibility, $providers );
	}

	/**
	 * Filters the full REST system-status payload before it is returned.
	 *
	 * @param array<string,mixed> $payload REST payload.
	 * @return array<string,mixed>
	 */
	public static function systemStatusPayload( array $payload, ?ProviderManager $providers = null ): array {
		return self::applyArrayFilter( 'scheduled_changes_system_status_payload', $payload, $providers );
	}

	/**
	 * Filters the REST system-status schema for provider compatibility fields.
	 *
	 * @param array<string,mixed> $schema Canonical status schema.
	 * @return array<string,mixed>
	 */
	public static function systemStatusSchema( array $schema ): array {
		return self::applyArrayFilter( 'scheduled_changes_system_status_schema', $schema );
	}

	/**
	 * Filters one public REST launch row for provider compatibility aliases.
	 *
	 * @param array<string,mixed> $row Canonical launch row.
	 * @return array<string,mixed>
	 */
	public static function restLaunchRow( array $row ): array {
		return self::applyArrayFilter( 'scheduled_changes_rest_launch_row', $row );
	}

	/**
	 * Filters the public REST launch schema for provider compatibility aliases.
	 *
	 * @param array<string,mixed> $schema Canonical launch schema.
	 * @return array<string,mixed>
	 */
	public static function restLaunchSchema( array $schema ): array {
		return self::applyArrayFilter( 'scheduled_changes_rest_launch_schema', $schema );
	}

	/**
	 * Filters the canonical WP-CLI doctor payload.
	 *
	 * @param array<string,mixed> $payload Doctor payload.
	 * @return array<string,mixed>
	 */
	public static function cliDoctorPayload( array $payload, ?ProviderManager $providers = null ): array {
		return self::applyArrayFilter( 'scheduled_changes_cli_doctor_payload', $payload, $providers );
	}

	/**
	 * Filters reusable provider-owned launch template blueprints.
	 *
	 * @param array<int,array<string,mixed>> $templates Canonical templates.
	 * @return array<int,array<string,mixed>>
	 */
	public static function launchTemplates( array $templates ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_launch_templates', $templates );

		return array_values(
			array_filter(
				$result,
				static fn ( $template ): bool => is_array( $template )
					&& '' !== (string) ( $template['id'] ?? '' )
			)
		);
	}

	/**
	 * Filters diagnostic field-discovery contexts.
	 *
	 * @param ObjectContext[] $contexts Diagnostic contexts.
	 * @return ObjectContext[]
	 */
	public static function providerDiagnosticContexts( array $contexts, ?ProviderManager $providers = null ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_provider_diagnostic_contexts', $contexts, $providers );

		return array_values(
			array_filter(
				$result,
				static fn ( $context ): bool => $context instanceof ObjectContext
			)
		);
	}

	/**
	 * Resolves a canonical target reference into a provider object context.
	 *
	 * @param array<string,mixed> $context_data Surface-specific resolver context.
	 */
	public static function targetReferenceContext( ?ObjectContext $context, string $target_reference, array $context_data = array() ): ?ObjectContext {
		$result = apply_filters( 'scheduled_changes_target_reference_context', $context, $target_reference, $context_data );

		return $result instanceof ObjectContext ? $result : null;
	}

	/**
	 * Resolves a CLI field-scan target reference into a provider object context.
	 *
	 * @param array<string,mixed> $assoc_args Associative CLI arguments.
	 */
	public static function cliFieldScanContext( ?ObjectContext $context, string $target_reference, array $assoc_args = array() ): ?ObjectContext {
		$context = self::targetReferenceContext( $context, $target_reference, array( 'surface' => 'wp-cli' ) + $assoc_args );
		$result  = apply_filters( 'scheduled_changes_cli_field_scan_context', $context, $target_reference, $assoc_args );

		return $result instanceof ObjectContext ? $result : null;
	}

	/**
	 * Lets a provider validate whether its target currently exists.
	 */
	public static function validationTargetExists( bool $exists, TargetDefinition $target ): bool {
		return (bool) apply_filters( 'scheduled_changes_validation_target_exists', $exists, $target );
	}

	/**
	 * Lets a provider report the current subtype for drift validation.
	 */
	public static function validationTargetSubtype( string $subtype, TargetDefinition $target ): string {
		$result = apply_filters( 'scheduled_changes_validation_target_subtype', $subtype, $target );

		return is_scalar( $result ) ? (string) $result : $subtype;
	}

	/**
	 * Lets providers report the current owner of a unique external value.
	 */
	public static function validationUniqueValueOwnerId( int $owner_id, string $field, string $value ): int {
		$result = apply_filters( 'scheduled_changes_validation_unique_value_owner_id', $owner_id, $field, $value );

		return is_numeric( $result ) ? max( 0, (int) $result ) : $owner_id;
	}

	/**
	 * Lets provider extensions authorize owned targets before scheduler mutations.
	 */
	public static function providerAuthorization( bool $authorized, string $action, int $actor_id, TargetDefinition $target ): bool {
		return (bool) apply_filters( 'scheduled_changes_provider_authorization', $authorized, $action, $actor_id, $target );
	}

	/**
	 * @param ProviderOperationHandler[] $handlers Provider-owned operation handlers.
	 * @param ProviderRegistry|null      $providers Provider registry context.
	 * @param FieldRegistry|null         $fields Field registry context.
	 * @param RunRepository|null         $runs Run repository context.
	 * @return ProviderOperationHandler[]
	 */
	public static function providerOperationHandlers( array $handlers, ?ProviderRegistry $providers = null, ?FieldRegistry $fields = null, ?RunRepository $runs = null ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_provider_operation_handlers', $handlers, $providers, $fields, $runs );

		return array_values(
			array_filter(
				$result,
				static fn ( $handler ): bool => $handler instanceof ProviderOperationHandler
			)
		);
	}

	/**
	 * @param CreatedTargetRollbackHandler[] $handlers Provider-owned created-target rollback handlers.
	 * @param ProviderRegistry|null          $providers Provider registry context.
	 * @param FieldRegistry|null             $fields Field registry context.
	 * @param RunRepository|null             $runs Run repository context.
	 * @return CreatedTargetRollbackHandler[]
	 */
	public static function createdTargetRollbackHandlers( array $handlers, ?ProviderRegistry $providers = null, ?FieldRegistry $fields = null, ?RunRepository $runs = null ): array {
		$result = self::applyArrayFilter( 'scheduled_changes_created_target_rollback_handlers', $handlers, $providers, $fields, $runs );

		return array_values(
			array_filter(
				$result,
				static fn ( $handler ): bool => $handler instanceof CreatedTargetRollbackHandler
			)
		);
	}

	/**
	 * Filters headless token operations.
	 *
	 * @param array<string,mixed> $result Default operation result.
	 * @param array<string,mixed> $context Operation context.
	 * @return array<string,mixed>
	 */
	public static function headlessTokenOperation( array $result, string $operation, array $context ): array {
		return self::applyArrayFilter( 'scheduled_changes_headless_token_operation', $result, $operation, $context );
	}

	/**
	 * Filters headless workflow operations.
	 *
	 * @param array<string,mixed> $result Default operation result.
	 * @param array<string,mixed> $context Operation context.
	 * @return array<string,mixed>
	 */
	public static function headlessWorkflowOperation( array $result, string $operation, array $context ): array {
		return self::applyArrayFilter( 'scheduled_changes_headless_workflow_operation', $result, $operation, $context );
	}

	/**
	 * Keeps the external-operation health hook behind the stable public gateway.
	 *
	 * The wrapper intentionally adds no policy; it prevents hook spelling and
	 * default-value drift across validation callers.
	 *
	 * @return mixed
	 */
	public static function externalOperationHealth( Operation $operation ) {
		return apply_filters( 'scheduled_changes_validate_external_operation', true, $operation );
	}

	public static function executeProviderOperation(
		Operation $operation,
		ExecutionContext $context,
		ExecutionState $state
	): ?OperationExecutionResult {
		$result = apply_filters( 'scheduled_changes_execute_provider_operation', null, $operation, $context, $state );

		return $result instanceof OperationExecutionResult ? $result : null;
	}

	/**
	 * @param mixed $launch
	 */
	public static function beforeValidateLaunch( $launch ): void {
		do_action( 'scheduled_changes_before_validate_launch', $launch );
	}

	public static function beforeExecuteLaunch( LaunchExecutionHookPayload $payload ): void {
		do_action( 'scheduled_changes_before_execute_launch', $payload->toArray() );
	}

	public static function afterExecuteLaunch( LaunchExecutionHookPayload $payload ): void {
		do_action( 'scheduled_changes_after_execute_launch', $payload->toArray() );
	}

	/**
	 * @param mixed $rollback
	 */
	public static function afterRollbackLaunch( $rollback ): void {
		do_action( 'scheduled_changes_after_rollback_launch', $rollback );
	}

	/**
	 * Applies an array-returning filter and falls back when a filter returns another type.
	 *
	 * @param array<mixed> $fallback Original value.
	 * @param mixed        ...$args Additional filter context.
	 * @return array<mixed>
	 */
	private static function applyArrayFilter( string $hook, array $fallback, ...$args ): array {
		$result = apply_filters( $hook, $fallback, ...$args );

		return is_array( $result ) ? $result : $fallback;
	}
}
