<?php
/**
 * Native WordPress post and page provider.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\WordPressContent;

use ScheduledChanges\Capabilities\WordPressPermissionService;
use ScheduledChanges\Field\FieldCollection;
use ScheduledChanges\Field\FieldDefinition;
use ScheduledChanges\Provider\AbstractFieldProvider;
use ScheduledChanges\Provider\ExecutionContext;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\OperationResult;
use ScheduledChanges\Provider\ProviderCategory;
use ScheduledChanges\Provider\ProviderSnapshot;
use ScheduledChanges\Provider\RollbackContext;
use ScheduledChanges\Provider\ValidationResult;
use ScheduledChanges\Taxonomy\TaxonomyTermService;

/**
 * Reads and writes certified WordPress post/page fields through public APIs.
 */
final class NativeWordPressProvider extends AbstractFieldProvider {
	private const PROVIDER_ID = 'wordpress-core';
	private const VERSION     = '1.0.0';

	private NativeWordPressFieldSchema $schema;
	private WordPressMetaFieldRegistry $metadata;
	private WordPressPermissionService $permissions;
	private TaxonomyTermService $taxonomy_terms;
	private RegisteredMetaService $registered_meta;

	/**
	 * Builds the provider with injectable discovery services.
	 */
	public function __construct(
		?NativeWordPressFieldSchema $schema = null,
		?WordPressMetaFieldRegistry $metadata = null,
		?WordPressPermissionService $permissions = null,
		?TaxonomyTermService $taxonomy_terms = null,
		?RegisteredMetaService $registered_meta = null
	) {
		$this->metadata        = $metadata ?? new WordPressMetaFieldRegistry();
		$this->schema          = $schema ?? new NativeWordPressFieldSchema( $this->metadata );
		$this->permissions     = $permissions ?? new WordPressPermissionService();
		$this->taxonomy_terms  = $taxonomy_terms ?? new TaxonomyTermService();
		$this->registered_meta = $registered_meta ?? new RegisteredMetaService( $this->metadata );
		parent::__construct();
	}

	/**
	 * Returns the stable provider ID.
	 */
	public function id(): string {
		return self::PROVIDER_ID;
	}

	/**
	 * Returns the provider contract version.
	 */
	public function version(): string {
		return self::VERSION;
	}

	/**
	 * Returns the ownership category.
	 */
	public function category(): string {
		return ProviderCategory::WORDPRESS_CORE;
	}

	/**
	 * Determines whether an existing post or page is supported.
	 */
	public function supports( ObjectContext $context ): bool {
		if ( ! in_array( $context->targetType(), array( 'post', 'page' ), true ) || null === $context->targetId() ) {
			return false;
		}

		$post = get_post( $context->targetId() );

		return $post instanceof \WP_Post
			&& $post->post_type === $context->targetType()
			&& ( '' === $context->subtype() || $post->post_type === $context->subtype() );
	}

	/**
	 * Discovers fields for a supported target.
	 */
	public function discover( ObjectContext $context ): FieldCollection {
		return $this->supports( $context ) ? $this->schema->definitions( $context ) : new FieldCollection();
	}

	/**
	 * @return mixed
	 */
	public function read( ObjectContext $context, FieldDefinition $field ) {
		$this->assertField( $context, $field );

		return $this->readValue( $this->post( $context ), $field );
	}

	/**
	 * @param mixed $value Value to normalize.
	 * @return mixed
	 */
	public function normalize( FieldDefinition $field, $value ) {
		$normalized = parent::normalize( $field, $value );
		$key        = $this->storageKey( $field );

		if ( 'post_name' === $key ) {
			return sanitize_title( (string) $normalized );
		}

		if ( in_array( $key, array( 'post_content', 'post_excerpt' ), true ) ) {
			return $this->normalizeBlockContent( (string) $normalized );
		}

		if ( 'post_status' === $key ) {
			return sanitize_key( (string) $normalized );
		}

		if ( 'post_meta' === $key ) {
			return $this->registered_meta->normalize( $field, $normalized );
		}

		return $normalized;
	}

	/**
	 * @param mixed $value Candidate value.
	 */
	public function validate(
		ObjectContext $context,
		FieldDefinition $field,
		$value
	): ValidationResult {
		try {
			$this->assertField( $context, $field );
		} catch ( \Throwable $throwable ) {
			return ValidationResult::invalid( 'unsupported_field', $throwable->getMessage(), $field->id() );
		}

		$post = $this->post( $context );

		if ( 'trash' === $post->post_status ) {
			return ValidationResult::invalid( 'target_trashed', 'The native WordPress target is in the Trash.', $field->id() );
		}

		$base = parent::validate( $context, $field, $value );

		if ( ! $base->isValid() ) {
			return $base;
		}

		$normalized = $this->normalize( $field, $value );
		$key        = $this->storageKey( $field );
		$error      = $this->valueValidationError( $post, $field, $key, $normalized );

		return null === $error
			? ValidationResult::valid()
			: ValidationResult::invalid( $error['code'], $error['message'], $field->id() );
	}

	/**
	 * @param mixed $value Value to write.
	 */
	public function write(
		ObjectContext $context,
		FieldDefinition $field,
		$value,
		ExecutionContext $execution
	): OperationResult {
		if ( ! $field->writable() ) {
			return OperationResult::failure( 'read_only_field', 'The native WordPress field is read-only.' );
		}

		$validation = $this->validate( $context, $field, $value );

		if ( ! $validation->isValid() ) {
			return OperationResult::failure(
				'validation_failed',
				$validation->errors()[0]['message'],
				array( 'errors' => $validation->errors() )
			);
		}

		$post       = $this->post( $context );
		$normalized = $this->normalize( $field, $value );
		$before     = $this->normalize( $field, $this->readValue( $post, $field ) );
		$permission = $this->permissionFailure( $post, $field, $normalized, $execution );

		if ( null !== $permission ) {
			return $permission;
		}

		if ( $before === $normalized ) {
			return OperationResult::success( 'The canonical WordPress value is already applied.', false );
		}

		if ( $execution->dryRun() ) {
			return OperationResult::success( 'Dry run validated the native WordPress write.', true );
		}

		$result = $this->setValue( $post, $field, $normalized );

		if ( is_wp_error( $result ) ) {
			return OperationResult::failure( 'native_write_failed', $result->get_error_message() );
		}

		$this->invalidateCaches( $post );

		$reloaded = get_post( $post->ID );

		if ( ! $reloaded instanceof \WP_Post ) {
			$this->restoreValue( $post, $field, $before );

			return OperationResult::failure( 'verification_failed', 'WordPress did not reload the updated target.' );
		}

		$after = $this->normalize( $field, $this->readValue( $reloaded, $field ) );

		if ( $after !== $normalized ) {
			$this->restoreValue( $reloaded, $field, $before );

			return OperationResult::failure(
				'verification_failed',
				'WordPress readback did not match the requested canonical value.',
				array(
					'expected' => $field->sensitive() ? '[redacted]' : $normalized,
					'actual'   => $field->sensitive() ? '[redacted]' : $after,
				)
			);
		}

		return OperationResult::success(
			'WordPress field saved and verified.',
			true,
			array( 'post_id' => $post->ID )
		);
	}

	/**
	 * Captures all currently discoverable values.
	 */
	public function snapshot( ObjectContext $context ): ProviderSnapshot {
		$values = array();

		foreach ( $this->discover( $context ) as $field ) {
			$values[ $field->id() ] = $this->normalize( $field, $this->read( $context, $field ) );
		}

		return new ProviderSnapshot( $this->id(), $this->version(), $values );
	}

	/**
	 * Restores reversible values and verifies readback.
	 */
	public function rollback(
		ObjectContext $context,
		ProviderSnapshot $snapshot,
		RollbackContext $rollback
	): OperationResult {
		if ( $snapshot->providerId() !== $this->id() ) {
			return OperationResult::failure( 'provider_mismatch', 'Snapshot belongs to another provider.' );
		}

		$post = $this->post( $context );

		$actor_id = (int) $rollback->actorUserId();

		if ( $actor_id <= 0 ) {
			return OperationResult::failure( 'actor_unavailable', 'A valid rollback actor is required.' );
		}

		$fields = $this->discover( $context );
		$values = $snapshot->values();
		$status = null;

		foreach ( $values as $field_id => $value ) {
			$field = $fields->get( $field_id );

			if ( null === $field || ! $field->reversible() ) {
				continue;
			}

			$permission = $this->permissions->fieldPermissionFailure( $actor_id, $post, $field, $value );

			if ( null !== $permission ) {
				return OperationResult::failure( 'rollback_' . $permission['code'], $permission['message'] );
			}

			if ( 'post_status' === $this->storageKey( $field ) ) {
				$status = array( $field, $value );
				continue;
			}

			$result = $this->setValue( $post, $field, $value );

			if ( is_wp_error( $result ) ) {
				return OperationResult::failure( 'rollback_failed', $result->get_error_message() );
			}
		}

		if ( is_array( $status ) ) {
			$result = $this->setValue( $post, $status[0], $status[1] );

			if ( is_wp_error( $result ) ) {
				return OperationResult::failure( 'rollback_failed', $result->get_error_message() );
			}
		}

		$this->invalidateCaches( $post );

		$reloaded = get_post( $post->ID );

		if ( ! $reloaded instanceof \WP_Post ) {
			return OperationResult::failure( 'rollback_verification_failed', 'WordPress did not reload the restored target.' );
		}

		foreach ( $values as $field_id => $value ) {
			$field = $fields->get( $field_id );

			if ( null === $field || ! $field->reversible() ) {
				continue;
			}

			$current = $this->normalize( $field, $this->readValue( $reloaded, $field ) );

			if ( $current !== $value ) {
				return OperationResult::failure( 'rollback_verification_failed', 'Rollback readback failed for ' . $field_id . '.' );
			}
		}

		return OperationResult::success( 'WordPress snapshot restored and verified.' );
	}

	/**
	 * Resolves the supported post object.
	 */
	private function post( ObjectContext $context ): \WP_Post {
		$post = null !== $context->targetId() ? get_post( $context->targetId() ) : null;

		if ( ! $post instanceof \WP_Post || ! $this->supports( $context ) ) {
			throw new \RuntimeException( 'The native WordPress target is unavailable or unsupported.' );
		}

		return $post;
	}

	/**
	 * Verifies that the provider owns a discovered field.
	 */
	private function assertField( ObjectContext $context, FieldDefinition $field ): void {
		if ( ! $this->supports( $context ) || self::PROVIDER_ID !== $field->provider() || null === $this->discover( $context )->get( $field->id() ) ) {
			throw new \InvalidArgumentException( 'The native WordPress provider does not own this target field.' );
		}
	}

	/**
	 * Returns the field's declared storage key.
	 */
	private function storageKey( FieldDefinition $field ): string {
		$key = (string) ( $field->validationRules()['storage_key'] ?? '' );

		if ( '' === $key ) {
			throw new \InvalidArgumentException( 'Native WordPress field storage metadata is missing.' );
		}

		return $key;
	}

	/**
	 * @return mixed
	 */
	private function readValue( \WP_Post $post, FieldDefinition $field ) {
		$key = $this->storageKey( $field );

		switch ( $key ) {
			case 'post_title':
			case 'post_name':
			case 'post_content':
			case 'post_excerpt':
			case 'post_status':
				return $post->{$key};
			case 'post_author':
			case 'post_parent':
			case 'menu_order':
				return (int) $post->{$key};
			case 'post_date_gmt':
				$date = get_post_datetime( $post, 'date', 'local' );

				return false !== $date
					? $date->setTimezone( new \DateTimeZone( 'UTC' ) )->format( 'Y-m-d\TH:i:s\Z' )
					: gmdate( 'Y-m-d\TH:i:s\Z' );
			case 'thumbnail_id':
				return (int) get_post_thumbnail_id( $post->ID );
			case 'page_template':
				$template = get_page_template_slug( $post );

				return is_string( $template ) ? $template : '';
			case 'taxonomy':
				return $this->taxonomy_terms->termIds( $post->ID, (string) ( $field->validationRules()['taxonomy'] ?? '' ) );
			case 'post_meta':
				return $this->registered_meta->read( $post->ID, $field );
			default:
				throw new \InvalidArgumentException( 'Unsupported native WordPress storage key.' );
		}
	}

	/**
	 * @param mixed $value Canonical value.
	 * @return true|\WP_Error
	 */
	private function setValue( \WP_Post $post, FieldDefinition $field, $value ) {
		$key = $this->storageKey( $field );

		if ( in_array( $key, array( 'post_title', 'post_name', 'post_content', 'post_excerpt', 'post_status', 'post_author', 'post_parent', 'menu_order' ), true ) ) {
			return $this->updatePost( $post->ID, array( $key => $value ) );
		}

		if ( 'post_date_gmt' === $key ) {
			$utc   = new \DateTimeImmutable( (string) $value, new \DateTimeZone( 'UTC' ) );
			$local = $utc->setTimezone( wp_timezone() );

			return $this->updatePost(
				$post->ID,
				array(
					'post_date'     => $local->format( 'Y-m-d H:i:s' ),
					'post_date_gmt' => $utc->format( 'Y-m-d H:i:s' ),
					'edit_date'     => true,
				)
			);
		}

		if ( 'thumbnail_id' === $key ) {
			$thumbnail_id = (int) $value;
			$updated      = $thumbnail_id > 0
				? set_post_thumbnail( $post->ID, $thumbnail_id )
				: delete_post_thumbnail( $post->ID );

			return false === $updated && (int) get_post_thumbnail_id( $post->ID ) !== $thumbnail_id
				? new \WP_Error( 'thumbnail_write_failed', 'WordPress could not update the featured image.' )
				: true;
		}

		if ( 'page_template' === $key ) {
			$updated = update_post_meta( $post->ID, '_wp_page_template', '' !== (string) $value ? (string) $value : 'default' );

			return false === $updated && $this->readValue( $post, $field ) !== $value
				? new \WP_Error( 'template_write_failed', 'WordPress could not update the page template.' )
				: true;
		}

		if ( 'taxonomy' === $key ) {
			return $this->taxonomy_terms->assign(
				$post->ID,
				(string) ( $field->validationRules()['taxonomy'] ?? '' ),
				array_map( 'intval', (array) $value )
			);
		}

		if ( 'post_meta' === $key ) {
			return $this->registered_meta->write( $post->ID, $field, $value );
		}

		return new \WP_Error( 'unsupported_field', 'The native WordPress field is unsupported.' );
	}

	/**
	 * @param array<string,mixed> $data Post columns to update.
	 * @return true|\WP_Error
	 */
	private function updatePost( int $post_id, array $data ) {
		$data['ID'] = $post_id;
		$result     = wp_update_post( wp_slash( $data ), true );

		return is_wp_error( $result ) ? $result : true;
	}

	/**
	 * @param mixed $value Canonical value.
	 * @return array{code:string,message:string}|null
	 */
	private function valueValidationError( \WP_Post $post, FieldDefinition $field, string $key, $value ): ?array {
		if ( 'post_status' === $key && ! in_array( $value, array( 'draft', 'pending', 'private', 'publish', 'future' ), true ) ) {
			return array(
				'code'    => 'unsupported_status',
				'message' => 'Only draft, pending, private, publish, and future statuses are supported.',
			);
		}

		if ( 'post_author' === $key && ! get_userdata( (int) $value ) ) {
			return array(
				'code'    => 'invalid_author',
				'message' => 'The selected WordPress author does not exist.',
			);
		}

		if ( 'thumbnail_id' === $key && (int) $value > 0 && ( 'attachment' !== get_post_type( (int) $value ) || ! wp_attachment_is_image( (int) $value ) ) ) {
			return array(
				'code'    => 'invalid_featured_image',
				'message' => 'The featured image must be an image attachment ID or zero.',
			);
		}

		if ( 'post_parent' === $key ) {
			$parent_id = (int) $value;

			if ( $parent_id === $post->ID || ( $parent_id > 0 && 'page' !== get_post_type( $parent_id ) ) ) {
				return array(
					'code'    => 'invalid_parent',
					'message' => 'The parent must be another page or zero.',
				);
			}

			if ( $parent_id > 0 && in_array( $post->ID, get_post_ancestors( $parent_id ), true ) ) {
				return array(
					'code'    => 'parent_cycle',
					'message' => 'The parent page selection would create a hierarchy cycle.',
				);
			}
		}

		if ( 'page_template' === $key && '' !== (string) $value ) {
			$templates = get_page_templates( $post );

			if ( ! in_array( (string) $value, array_values( $templates ), true ) ) {
				return array(
					'code'    => 'unsupported_page_template',
					'message' => 'The selected page template is not registered by the active theme.',
				);
			}
		}

		if ( 'taxonomy' === $key ) {
			return $this->taxonomy_terms->validationError(
				(string) ( $field->validationRules()['taxonomy'] ?? '' ),
				array_map( 'intval', (array) $value )
			);
		}

		if ( 'post_meta' === $key ) {
			return $this->registered_meta->validationError( $post->post_type, $field );
		}

		return null;
	}

	/**
	 * @param mixed $value Canonical value.
	 */
	private function permissionFailure(
		\WP_Post $post,
		FieldDefinition $field,
		$value,
		ExecutionContext $execution
	): ?OperationResult {
		$permission = $this->permissions->fieldPermissionFailure(
			(int) $execution->actorUserId(),
			$post,
			$field,
			$value
		);

		return null === $permission
			? null
			: OperationResult::failure( $permission['code'], $permission['message'] );
	}

	/**
	 * @param mixed $value Previous canonical value.
	 */
	private function restoreValue( \WP_Post $post, FieldDefinition $field, $value ): void {
		$result = $this->setValue( $post, $field, $value );
		$this->invalidateCaches( $post );
		unset( $result );
	}

	/**
	 * Clears native object caches after a provider-owned write.
	 */
	private function invalidateCaches( \WP_Post $post ): void {
		clean_post_cache( $post->ID );
		clean_object_term_cache( $post->ID, $post->post_type );
		wp_cache_delete( $post->ID, 'post_meta' );
	}

	/**
	 * Canonicalizes block serialization while preserving classic content.
	 */
	private function normalizeBlockContent( string $content ): string {
		if (
			function_exists( 'has_blocks' )
			&& function_exists( 'parse_blocks' )
			&& function_exists( 'serialize_blocks' )
			&& has_blocks( $content )
		) {
			return trim( serialize_blocks( parse_blocks( $content ) ) );
		}

		return $content;
	}
}
