<?php
/**
 * Installed JJ\'s Post Scheduler module manifest.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Extension;

/**
 * Public manifest that a separate compatibility plugin registers with core.
 */
final class ModuleDefinition {
	private string $slug;
	private string $name;
	private string $version;
	private string $adapter_slug;
	private string $provider_id;
	private string $plugin_file = '';
	/**
	 * @var string[]
	 */
	private array $verified_lifecycle         = array();
	private string $declared_lifecycle_status = '';
	private string $lifecycle_message         = '';
	private bool $certification_required      = false;
	private string $certification_id          = '';
	/**
	 * @var array<string,string[]>
	 */
	private array $certification_evidence       = array();
	private string $certification_checksum      = '';
	private bool $certification_valid           = false;
	private string $certification_result_status = '';
	/**
	 * @var string[]
	 */
	private array $certification_commands = array();
	/**
	 * @var array<string,string>
	 */
	private array $certification_command_checksums = array();
	/**
	 * @var string[]
	 */
	private array $certification_passed_commands = array();
	/**
	 * @var string[]
	 */
	private array $dependencies = array();
	/**
	 * @var string[]
	 */
	private array $capabilities        = array();
	private string $package_form       = '';
	private string $visibility         = '';
	private string $source_package     = '';
	private string $group              = '';
	private string $documentation_url  = '';
	private string $description        = '';
	private string $registration_phase = '';
	/**
	 * @var array<string,string[]>
	 */
	private array $resource_ownership         = array();
	private bool $capabilities_declared       = false;
	private bool $package_form_declared       = false;
	private bool $visibility_declared         = false;
	private bool $source_package_declared     = false;
	private bool $description_declared        = false;
	private bool $resource_ownership_declared = false;

	public const LIFECYCLE_COMPLETE     = 'complete';
	public const LIFECYCLE_PARTIAL      = 'partial';
	public const LIFECYCLE_MISSING      = 'missing';
	public const LIFECYCLE_INCOMPATIBLE = 'incompatible';
	public const LIFECYCLE_UNKNOWN      = 'unknown';

	public const PACKAGE_BUNDLED_CORE      = 'bundled-core';
	public const PACKAGE_BUNDLED_EXTENSION = 'bundled-extension';
	public const PACKAGE_SEPARATE_PLUGIN   = 'separate-plugin';
	public const VISIBILITY_PUBLIC         = 'public';
	public const VISIBILITY_DEVELOPMENT    = 'development';
	public const VISIBILITY_HIDDEN         = 'hidden';
	public const VISIBILITY_GROUPED_PUBLIC = 'grouped-public';
	public const PHASE_FOUNDATION          = 'foundation';
	public const PHASE_PERSISTENCE         = 'persistence';
	public const PHASE_PROVIDERS           = 'providers';
	public const PHASE_APPLICATION         = 'application';
	public const PHASE_DELIVERY            = 'delivery';
	public const RESOURCE_ACTION_HOOK      = 'action-scheduler-hook';
	public const RESOURCE_ASSET            = 'asset';
	public const RESOURCE_CLI_COMMAND      = 'cli-command';
	public const RESOURCE_FIELD            = 'field';
	public const RESOURCE_META             = 'meta';
	public const RESOURCE_MIGRATION        = 'migration';
	public const RESOURCE_OPTION           = 'option';
	public const RESOURCE_REST_ROUTE       = 'rest-route';
	public const RESOURCE_TABLE            = 'table';
	public const RESOURCE_UNINSTALL        = 'uninstall-cleanup';
	public const RESOURCE_WORDPRESS_HOOK   = 'wordpress-hook';

	private const LIFECYCLE_STATUSES = array(
		self::LIFECYCLE_COMPLETE,
		self::LIFECYCLE_PARTIAL,
		self::LIFECYCLE_MISSING,
		self::LIFECYCLE_INCOMPATIBLE,
		self::LIFECYCLE_UNKNOWN,
	);

	private const PACKAGE_FORMS = array(
		self::PACKAGE_BUNDLED_CORE,
		self::PACKAGE_BUNDLED_EXTENSION,
		self::PACKAGE_SEPARATE_PLUGIN,
	);

	private const VISIBILITIES = array(
		self::VISIBILITY_PUBLIC,
		self::VISIBILITY_DEVELOPMENT,
		self::VISIBILITY_HIDDEN,
		self::VISIBILITY_GROUPED_PUBLIC,
	);

	private const REGISTRATION_PHASES = array(
		self::PHASE_FOUNDATION,
		self::PHASE_PERSISTENCE,
		self::PHASE_PROVIDERS,
		self::PHASE_APPLICATION,
		self::PHASE_DELIVERY,
	);

	private const RESOURCE_KINDS = array(
		self::RESOURCE_ACTION_HOOK,
		self::RESOURCE_ASSET,
		self::RESOURCE_CLI_COMMAND,
		self::RESOURCE_FIELD,
		self::RESOURCE_META,
		self::RESOURCE_MIGRATION,
		self::RESOURCE_OPTION,
		self::RESOURCE_REST_ROUTE,
		self::RESOURCE_TABLE,
		self::RESOURCE_UNINSTALL,
		self::RESOURCE_WORDPRESS_HOOK,
	);

	/**
	 * Creates a module definition.
	 *
	 * @param string              $slug Module plugin slug.
	 * @param string              $name Human-readable module name.
	 * @param string              $version Module version.
	 * @param string              $adapter_slug Adapter target slug this module satisfies.
	 * @param string              $provider_id Field provider ID registered by the module.
	 * @param string              $plugin_file Main module plugin file path.
	 * @param string[]            $verified_lifecycle Certified lifecycle steps.
	 * @param string              $declared_lifecycle_status Optional lifecycle status declared by the module.
	 * @param string              $lifecycle_message Optional lifecycle diagnostic message.
	 * @param array<string,mixed> $certification Optional certification evidence contract.
	 * @param array<string,mixed> $metadata Optional bundled module catalog metadata.
	 */
	public function __construct(
		string $slug,
		string $name,
		string $version,
		string $adapter_slug,
		string $provider_id,
		string $plugin_file = '',
		array $verified_lifecycle = array(),
		string $declared_lifecycle_status = '',
		string $lifecycle_message = '',
		array $certification = array(),
		array $metadata = array()
	) {
		$this->slug                      = $slug;
		$this->name                      = $name;
		$this->version                   = $version;
		$this->adapter_slug              = $adapter_slug;
		$this->provider_id               = $provider_id;
		$this->plugin_file               = $plugin_file;
		$this->verified_lifecycle        = array_values(
			array_filter(
				array_map( static fn ( $step ): string => self::slugPart( (string) $step ), $verified_lifecycle ),
				static fn ( string $step ): bool => '' !== $step
			)
		);
		$this->declared_lifecycle_status = self::normalizeLifecycleStatus( $declared_lifecycle_status );
		$this->lifecycle_message         = trim( $lifecycle_message );
		$this->initializeCertification( $certification );
		$this->initializeMetadata( $metadata );
		foreach ( array( $this->slug, $this->adapter_slug, $this->provider_id ) as $value ) {
			if ( ! preg_match( '/^[a-z][a-z0-9-]*$/', $value ) ) {
				throw new \InvalidArgumentException( 'Module manifest slugs must be lowercase slugs.' );
			}
		}

		if ( '' === trim( $this->name ) || '' === trim( $this->version ) ) {
			throw new \InvalidArgumentException( 'Module name and version are required.' );
		}

		if ( 1 !== preg_match( '/^\d+\.\d+\.\d+(?:[-+._a-z0-9]+)?$/i', $this->version ) ) {
			throw new \InvalidArgumentException( 'Module version must be a stable machine-readable version.' );
		}
	}

	/**
	 * Builds a manifest from the array shape convenient for WordPress filters.
	 *
	 * @param array<string,mixed> $data Manifest data.
	 */
	public static function fromArray( array $data ): self {
		$lifecycle = is_array( $data['lifecycle'] ?? null ) ? $data['lifecycle'] : array();
		$verified  = $lifecycle['verified'] ?? $data['verified_lifecycle'] ?? array();

		return new self(
			(string) ( $data['slug'] ?? '' ),
			(string) ( $data['name'] ?? '' ),
			(string) ( $data['version'] ?? '' ),
			(string) ( $data['adapter_slug'] ?? '' ),
			(string) ( $data['provider_id'] ?? '' ),
			(string) ( $data['plugin_file'] ?? '' ),
			is_array( $verified ) ? $verified : array(),
			(string) ( $lifecycle['status'] ?? $data['lifecycle_status'] ?? '' ),
			(string) ( $lifecycle['message'] ?? $data['lifecycle_message'] ?? '' ),
			is_array( $data['certification'] ?? null ) ? $data['certification'] : array(),
			is_array( $data['metadata'] ?? null ) ? $data['metadata'] : $data
		);
	}

	public function slug(): string {
		return $this->slug;
	}

	public function name(): string {
		return $this->name;
	}

	public function version(): string {
		return $this->version;
	}

	public function adapterSlug(): string {
		return $this->adapter_slug;
	}

	public function providerId(): string {
		return $this->provider_id;
	}

	public function pluginFile(): string {
		return $this->plugin_file;
	}

	/**
	 * @return string[]
	 */
	public function dependencies(): array {
		return $this->dependencies;
	}

	/**
	 * @return string[]
	 */
	public function capabilities(): array {
		return $this->capabilities;
	}

	public function packageForm(): string {
		return $this->package_form;
	}

	public function visibility(): string {
		return $this->visibility;
	}

	public function sourcePackage(): string {
		return $this->source_package;
	}

	public function group(): string {
		return $this->group;
	}

	public function documentationUrl(): string {
		return $this->documentation_url;
	}

	public function description(): string {
		return $this->description;
	}

	public function registrationPhase(): string {
		return $this->registration_phase;
	}

	/**
	 * Returns resources this module owns, grouped by resource kind.
	 *
	 * @return array<string,string[]>
	 */
	public function resourceOwnership(): array {
		return $this->resource_ownership;
	}

	public function capabilitiesDeclared(): bool {
		return $this->capabilities_declared;
	}

	public function packageFormDeclared(): bool {
		return $this->package_form_declared;
	}

	public function visibilityDeclared(): bool {
		return $this->visibility_declared;
	}

	public function sourcePackageDeclared(): bool {
		return $this->source_package_declared;
	}

	public function descriptionDeclared(): bool {
		return $this->description_declared;
	}

	public function resourceOwnershipDeclared(): bool {
		return $this->resource_ownership_declared;
	}

	public function declaredLifecycleStatus(): string {
		return $this->declared_lifecycle_status;
	}

	public function lifecycleMessage(): string {
		return $this->lifecycle_message;
	}

	/**
	 * @return string[]
	 */
	public function verifiedLifecycle(): array {
		$steps = array_values( array_unique( $this->verified_lifecycle ) );
		sort( $steps );

		if ( $this->certification_required ) {
			if ( ! $this->certification_valid ) {
				return array();
			}

			$steps = array_values( array_intersect( $steps, array_keys( $this->certification_evidence ) ) );
			sort( $steps );
		}

		return $steps;
	}

	public function certificationRequired(): bool {
		return $this->certification_required;
	}

	public function certificationId(): string {
		return $this->certification_id;
	}

	public function certificationValid(): bool {
		return $this->certification_valid;
	}

	public function certificationChecksum(): string {
		return $this->certification_checksum;
	}

	public function certificationResultStatus(): string {
		return $this->certification_result_status;
	}

	/**
	 * @return string[]
	 */
	public function certificationCommands(): array {
		return $this->certification_commands;
	}

	/**
	 * @return string[]
	 */
	public function certificationPassedCommands(): array {
		return $this->certification_passed_commands;
	}

	/**
	 * @return array<string,string[]>
	 */
	public function certificationEvidence(): array {
		return $this->certification_evidence;
	}

	/**
	 * @param string[] $required Required lifecycle steps.
	 */
	public function satisfiesLifecycle( array $required ): bool {
		$verified = array_fill_keys( $this->verifiedLifecycle(), true );

		foreach ( $required as $step ) {
			if ( ! isset( $verified[ $step ] ) ) {
				return false;
			}
		}

		return true;
	}

	private static function slugPart( string $value ): string {
		return trim( (string) preg_replace( '/[^a-z0-9_-]+/', '', strtolower( $value ) ), '_-' );
	}

	private static function normalizeLifecycleStatus( string $status ): string {
		$status = self::slugPart( $status );

		return in_array( $status, self::LIFECYCLE_STATUSES, true ) ? $status : '';
	}

	/**
	 * @param array<string,mixed> $metadata Module catalog metadata.
	 */
	private function initializeMetadata( array $metadata ): void {
		$this->capabilities_declared       = array_key_exists( 'capabilities', $metadata );
		$this->package_form_declared       = array_key_exists( 'package_form', $metadata ) || array_key_exists( 'packageForm', $metadata );
		$this->visibility_declared         = array_key_exists( 'visibility', $metadata );
		$this->source_package_declared     = array_key_exists( 'source_package', $metadata ) || array_key_exists( 'sourcePackage', $metadata );
		$this->description_declared        = array_key_exists( 'description', $metadata );
		$this->resource_ownership_declared = array_key_exists( 'resources', $metadata )
			|| array_key_exists( 'resource_ownership', $metadata )
			|| array_key_exists( 'resourceOwnership', $metadata );
		$this->dependencies                = self::normalizeSlugList( $metadata['dependencies'] ?? array() );
		$this->capabilities                = self::normalizeCapabilityList( $metadata['capabilities'] ?? array() );
		$this->resource_ownership          = self::normalizeResourceOwnership( $metadata['resources'] ?? $metadata['resource_ownership'] ?? $metadata['resourceOwnership'] ?? array() );
		$this->registration_phase          = self::normalizeEnum(
			(string) ( $metadata['registration_phase'] ?? $metadata['registrationPhase'] ?? '' ),
			self::REGISTRATION_PHASES,
			self::PHASE_APPLICATION
		);
		$this->package_form                = self::normalizeEnum(
			(string) ( $metadata['package_form'] ?? $metadata['packageForm'] ?? '' ),
			self::PACKAGE_FORMS,
			'' === $this->plugin_file ? self::PACKAGE_BUNDLED_CORE : self::PACKAGE_SEPARATE_PLUGIN
		);
		$this->visibility                  = self::normalizeEnum(
			(string) ( $metadata['visibility'] ?? '' ),
			self::VISIBILITIES,
			self::VISIBILITY_PUBLIC
		);
		$this->source_package              = self::slugPart( (string) ( $metadata['source_package'] ?? $metadata['sourcePackage'] ?? '' ) );
		$this->group                       = self::slugPart( (string) ( $metadata['group'] ?? '' ) );
		$this->documentation_url           = trim( (string) ( $metadata['documentation_url'] ?? $metadata['documentationUrl'] ?? '' ) );
		$this->description                 = trim( (string) ( $metadata['description'] ?? '' ) );

		if ( '' === $this->source_package ) {
			$this->source_package = self::PACKAGE_SEPARATE_PLUGIN === $this->package_form
				? $this->slug
				: 'scheduled-changes-core';
		}
	}

	/**
	 * @param mixed $values Candidate slug list.
	 * @return string[]
	 */
	private static function normalizeSlugList( $values ): array {
		if ( ! is_array( $values ) ) {
			return array();
		}

		$values = array_values(
			array_unique(
				array_filter(
					array_map( static fn ( $value ): string => self::slugPart( (string) $value ), $values ),
					static fn ( string $value ): bool => '' !== $value
				)
			)
		);
		sort( $values );

		return $values;
	}

	/**
	 * @param mixed $values Candidate capability IDs.
	 * @return string[]
	 */
	private static function normalizeCapabilityList( $values ): array {
		if ( ! is_array( $values ) ) {
			return array();
		}

		$values = array_values(
			array_unique(
				array_filter(
					array_map(
						static fn ( $value ): string => trim(
							(string) preg_replace( '/[^a-z0-9_.:-]+/', '', strtolower( (string) $value ) ),
							'_.:-'
						),
						$values
					),
					static fn ( string $value ): bool => '' !== $value
				)
			)
		);
		sort( $values );

		return $values;
	}

	/**
	 * @param mixed $values Candidate resource ownership map.
	 * @return array<string,string[]>
	 */
	private static function normalizeResourceOwnership( $values ): array {
		if ( ! is_array( $values ) ) {
			return array();
		}

		$resources = array();

		foreach ( $values as $kind => $names ) {
			if ( is_array( $names ) && isset( $names['type'], $names['name'] ) ) {
				$kind  = $names['type'];
				$names = array( $names['name'] );
			}

			$kind = self::normalizeEnum( (string) $kind, self::RESOURCE_KINDS, '' );
			if ( '' === $kind || ! is_array( $names ) ) {
				continue;
			}

			foreach ( $names as $name ) {
				$name = self::normalizeResourceName( (string) $name );
				if ( '' !== $name ) {
					$resources[ $kind ][] = $name;
				}
			}
		}

		foreach ( $resources as $kind => $names ) {
			$resources[ $kind ] = array_values( array_unique( $names ) );
			sort( $resources[ $kind ] );

			if ( array() === $resources[ $kind ] ) {
				unset( $resources[ $kind ] );
			}
		}
		ksort( $resources );

		return $resources;
	}

	private static function normalizeResourceName( string $value ): string {
		return trim( (string) preg_replace( '/[^a-z0-9_.:\/?*{}()|<>= -]+/', '', strtolower( $value ) ) );
	}

	/**
	 * @param string[] $allowed Allowed enum values.
	 */
	private static function normalizeEnum( string $value, array $allowed, string $fallback ): string {
		$value = self::slugPart( $value );

		return in_array( $value, $allowed, true ) ? $value : $fallback;
	}

	/**
	 * @param array<string,mixed> $certification Certification evidence contract.
	 */
	private function initializeCertification( array $certification ): void {
		$this->certification_required = true === ( $certification['required'] ?? false );
		$this->certification_id       = self::slugPart( (string) ( $certification['id'] ?? '' ) );
		$this->certification_checksum = strtolower( trim( (string) ( $certification['checksum'] ?? '' ) ) );

		$raw_evidence = is_array( $certification['evidence'] ?? null ) ? $certification['evidence'] : array();
		$evidence     = array();

		foreach ( $raw_evidence as $step => $ids ) {
			$step = self::slugPart( (string) $step );
			if ( '' === $step || ! is_array( $ids ) ) {
				continue;
			}

			$normalized = array_values(
				array_unique(
					array_filter(
						array_map( static fn ( $id ): string => trim( (string) $id ), $ids ),
						static fn ( string $id ): bool => '' !== $id
					)
				)
			);
			sort( $normalized );

			if ( array() !== $normalized ) {
				$evidence[ $step ] = $normalized;
			}
		}

		ksort( $evidence );
		$this->certification_evidence          = $evidence;
		$this->certification_command_checksums = self::normalizeCommandChecksums( $certification['commands'] ?? array() );
		$this->certification_commands          = array_keys( $this->certification_command_checksums );

		if ( ! $this->certification_required && array() === $certification ) {
			return;
		}

		$payload = array(
			'schema_version' => (int) ( $certification['schema_version'] ?? 0 ),
			'id'             => $this->certification_id,
			'evidence'       => $this->certification_evidence,
			'commands'       => $this->certification_command_checksums,
		);
		$json    = function_exists( 'wp_json_encode' )
			? wp_json_encode( $payload, JSON_UNESCAPED_SLASHES )
			// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Supports isolated SDK consumers.
			: json_encode( $payload, JSON_UNESCAPED_SLASHES );
		$result = is_array( $certification['result'] ?? null ) ? $certification['result'] : array();

		$this->certification_result_status   = self::slugPart( (string) ( $result['status'] ?? '' ) );
		$this->certification_passed_commands = self::normalizeStringList( $result['commands'] ?? array() );

		$this->certification_valid = 1 === $payload['schema_version']
			&& '' !== $this->certification_id
			&& array() !== $this->certification_commands
			&& is_string( $json )
			&& 1 === preg_match( '/^[a-f0-9]{64}$/', $this->certification_checksum )
			&& hash_equals( hash( 'sha256', $json ), $this->certification_checksum )
			&& 'passed' === $this->certification_result_status
			&& self::slugPart( (string) ( $result['certification_id'] ?? '' ) ) === $this->certification_id
			&& hash_equals(
				$this->certification_checksum,
				strtolower( trim( (string) ( $result['manifest_checksum'] ?? '' ) ) )
			)
			&& array() === array_diff( $this->certification_commands, $this->certification_passed_commands );
	}

	/**
	 * @param mixed $values Candidate string list.
	 * @return string[]
	 */
	private static function normalizeStringList( $values ): array {
		if ( ! is_array( $values ) ) {
			return array();
		}

		$values = array_values(
			array_unique(
				array_filter(
					array_map( static fn ( $value ): string => trim( (string) $value ), $values ),
					static fn ( string $value ): bool => '' !== $value
				)
			)
		);
		sort( $values );

		return $values;
	}

	/**
	 * @param mixed $commands Candidate command checksum map.
	 * @return array<string,string>
	 */
	private static function normalizeCommandChecksums( $commands ): array {
		if ( ! is_array( $commands ) ) {
			return array();
		}

		$normalized = array();
		foreach ( $commands as $id => $checksum ) {
			$id       = self::slugPart( (string) $id );
			$checksum = strtolower( trim( (string) $checksum ) );

			if ( '' !== $id && 1 === preg_match( '/^[a-f0-9]{64}$/', $checksum ) ) {
				$normalized[ $id ] = $checksum;
			}
		}
		ksort( $normalized );

		return $normalized;
	}
}
