<?php
/**
 * Native WordPress editable-copy recording workspace.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\WordPressContent\Recording;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception details are escaped by application boundaries.

use ScheduledChanges\Capabilities\CapabilityManager;
use ScheduledChanges\Capabilities\WordPressPermissionService;
use ScheduledChanges\Domain\Recording\RecordingSession;
use ScheduledChanges\Domain\Shared\Uuid;
use ScheduledChanges\Recording\RecordingWorkspaceAdapter;
use ScheduledChanges\Recording\RecordingWorkspaceMetadata;
use ScheduledChanges\Recording\RecordingWorkspaceRequest;
use ScheduledChanges\Recording\RecordingWorkspaceResult;
use ScheduledChanges\WordPressContent\WordPressMetaFieldRegistry;

/**
 * Creates and owns non-public post/page copies for Record Changes.
 */
final class NativeWordPressRecordingWorkspaceAdapter implements RecordingWorkspaceAdapter {
	private NativeWordPressRecordingSnapshotter $snapshotter;
	private NativeWordPressRecordingDiffBuilder $diffs;
	private WordPressMetaFieldRegistry $metadata;
	private WordPressPermissionService $permissions;

	/**
	 * Builds the adapter from native snapshot and diff services.
	 */
	public function __construct(
		NativeWordPressRecordingSnapshotter $snapshotter,
		NativeWordPressRecordingDiffBuilder $diffs,
		?WordPressMetaFieldRegistry $metadata = null,
		?WordPressPermissionService $permissions = null
	) {
		$this->snapshotter = $snapshotter;
		$this->diffs       = $diffs;
		$this->metadata    = $metadata ?? new WordPressMetaFieldRegistry();
		$this->permissions = $permissions ?? new WordPressPermissionService();
	}

	/**
	 * Returns the primary routed source type.
	 */
	public function sourceType(): string {
		return 'post';
	}

	/**
	 * Returns the generic native workspace type.
	 */
	public function workspaceType(): string {
		return 'recording_post';
	}

	/**
	 * Validates source existence and start permissions.
	 */
	public function assertCanStart( int $source_id ): void {
		$source = get_post( $source_id );

		if ( ! $source instanceof \WP_Post || ! in_array( $source->post_type, array( 'post', 'page' ), true ) ) {
			throw new \RuntimeException( 'Record Changes requires an existing WordPress post or page.' );
		}

		if ( NativeWordPressWorkspaceAccessGuard::isWorkspace( $source_id ) ) {
			throw new \RuntimeException( 'A recording workspace cannot be used as a recording source.' );
		}

		if (
			! current_user_can( CapabilityManager::CREATE_SCHEDULES )
			|| ! current_user_can( 'edit_post', $source_id )
			|| ! $this->permissions->canInspectPost( get_current_user_id(), $source_id )
		) {
			throw new \RuntimeException( 'Current user cannot start a JJ\'s Post Scheduler recording session.' );
		}
	}

	/**
	 * Validates workspace identity and use permissions.
	 */
	public function assertCanUse( RecordingSession $session ): void {
		$workspace_id = $session->workspaceId();

		if ( null === $workspace_id ) {
			throw new \RuntimeException( 'Recording session has no native workspace.' );
		}

		$this->assertWorkspaceMapping( $workspace_id, $session->sourceId(), $session->uuid() );

		if (
			! current_user_can( 'edit_post', $session->sourceId() )
			|| ! current_user_can( 'edit_post', $workspace_id )
			|| ! $this->permissions->canInspectPost( get_current_user_id(), $session->sourceId() )
			|| ! $this->permissions->canInspectPost( get_current_user_id(), $workspace_id )
		) {
			throw new \RuntimeException( 'Current user cannot use this recording session.' );
		}
	}

	/**
	 * Captures the immutable source baseline.
	 *
	 * @return array<string,mixed>
	 */
	public function captureSourceBaseline( int $source_id ): array {
		$source = $this->source( $source_id );

		return $this->snapshotter->capture( $source_id, $source->post_type . ':' . $source_id );
	}

	/**
	 * Creates an isolated native editable copy.
	 */
	public function create( RecordingWorkspaceRequest $request ): RecordingWorkspaceResult {
		$source       = $this->source( $request->sourceId() );
		$workspace_id = 0;

		try {
			$workspace_id = wp_insert_post(
				wp_slash(
					array(
						'post_type'      => $source->post_type,
						'post_status'    => 'draft',
						'post_title'     => $source->post_title,
						'post_name'      => $source->post_name,
						'post_content'   => $source->post_content,
						'post_excerpt'   => $source->post_excerpt,
						'post_author'    => (int) $source->post_author,
						'post_date'      => $source->post_date,
						'post_date_gmt'  => $source->post_date_gmt,
						'post_parent'    => (int) $source->post_parent,
						'menu_order'     => (int) $source->menu_order,
						'comment_status' => $source->comment_status,
						'ping_status'    => $source->ping_status,
						'edit_date'      => true,
					)
				),
				true
			);

			if ( is_wp_error( $workspace_id ) ) {
				throw new \RuntimeException( $workspace_id->get_error_message() );
			}

			$workspace_id = (int) $workspace_id;
			$this->markWorkspace( $workspace_id, $request, $source );
			$this->copyTaxonomies( $source, $workspace_id );
			$this->copySupportedMetadata( $source, $workspace_id );
		} catch ( \Throwable $throwable ) {
			if ( $workspace_id > 0 ) {
				wp_delete_post( $workspace_id, true );
			}

			throw $throwable;
		}

		$source_reference    = $source->post_type . ':' . $source->ID;
		$workspace_reference = 'recording-' . $source->post_type . ':' . $workspace_id;
		$baseline            = $this->snapshotter->capture( $source->ID, $source_reference );
		$baseline_object     = is_array( $baseline['objects'][0] ?? null ) ? $baseline['objects'][0] : array();

		return new RecordingWorkspaceResult(
			$workspace_id,
			array(
				'source_to_workspace' => array( (string) $source->ID => $workspace_id ),
				'workspace_to_source' => array( (string) $workspace_id => $source->ID ),
			),
			array(
				'source_provider_id'    => 'wordpress-core',
				'source_type'           => $source->post_type,
				'source_reference'      => $source_reference,
				'workspace_provider_id' => 'wordpress-core',
				'workspace_type'        => 'recording_' . $source->post_type,
				'workspace_reference'   => $workspace_reference,
				'baseline_fingerprint'  => (string) ( $baseline_object['fingerprint'] ?? '' ),
				'edit_url'              => (string) get_edit_post_link( $workspace_id, 'raw' ),
			)
		);
	}

	/**
	 * Captures current workspace state.
	 *
	 * @return array<string,mixed>
	 */
	public function captureWorkspace( RecordingSession $session ): array {
		$this->assertCanUse( $session );

		return $this->snapshotter->capture(
			(int) $session->workspaceId(),
			(string) $session->workspaceReference(),
			true
		);
	}

	/**
	 * Captures current source state for mutation detection.
	 *
	 * @return array<string,mixed>
	 */
	public function captureSource( RecordingSession $session ): array {
		$this->assertCanUse( $session );

		return $this->snapshotter->capture(
			$session->sourceId(),
			$session->sourceReference()
		);
	}

	/**
	 * Builds a structured native field diff.
	 *
	 * @param array<string,mixed> $workspace_snapshot Workspace snapshot.
	 * @return array<string,mixed>
	 */
	public function buildDiff( RecordingSession $session, array $workspace_snapshot ): array {
		return $this->diffs->build( $session, $workspace_snapshot );
	}

	/**
	 * Permanently removes a persisted workspace idempotently.
	 */
	public function cleanup( RecordingSession $session, string $reason ): void {
		unset( $reason );
		$workspace_id = $session->workspaceId();

		if ( null === $workspace_id || ! get_post( $workspace_id ) ) {
			return;
		}

		$this->assertWorkspaceMapping( $workspace_id, $session->sourceId(), $session->uuid(), false );
		wp_delete_post( $workspace_id, true );
	}

	/**
	 * Removes a workspace created before session persistence completed.
	 */
	public function cleanupCreated( RecordingWorkspaceRequest $request, RecordingWorkspaceResult $workspace, string $reason ): void {
		unset( $reason );
		$workspace_id = $workspace->workspaceId();

		if ( ! get_post( $workspace_id ) ) {
			return;
		}

		$this->assertWorkspaceMapping( $workspace_id, $request->sourceId(), $request->sessionUuid() );
		wp_delete_post( $workspace_id, true );
	}

	/**
	 * Returns the source-scoped recording lock target.
	 */
	public function lockTarget( int $source_id ): string {
		$source = $this->source( $source_id );

		return 'recording:' . $source->post_type . ':' . $source_id;
	}

	/**
	 * Builds the default native launch name.
	 */
	public function defaultLaunchName( RecordingSession $session ): string {
		$title = trim( get_the_title( $session->sourceId() ) );

		return 'Recorded changes for ' . ( '' !== $title ? $title : $session->sourceReference() );
	}

	/**
	 * Returns neutral recording hook context.
	 *
	 * @return array<string,mixed>
	 */
	public function context( Uuid $uuid, int $source_id, ?int $workspace_id, int $owner_id, string $phase ): array {
		$source         = get_post( $source_id );
		$workspace_post = null === $workspace_id ? null : get_post( $workspace_id );
		$source_type    = $source instanceof \WP_Post
			? $source->post_type
			: ( $workspace_post instanceof \WP_Post ? $workspace_post->post_type : '' );

		if ( ! in_array( $source_type, array( 'post', 'page' ), true ) ) {
			throw new \RuntimeException( 'Native recording context has no supported source or workspace.' );
		}

		return array(
			'session_uuid'       => $uuid->value(),
			'source_provider_id' => 'wordpress-core',
			'source_type'        => $source_type,
			'source_id'          => $source_id,
			'source_reference'   => $source_type . ':' . $source_id,
			'workspace_type'     => 'recording_' . $source_type,
			'workspace_id'       => $workspace_id,
			'owner_id'           => $owner_id,
			'phase'              => $phase,
		);
	}

	/**
	 * Resolves a supported source post.
	 */
	private function source( int $source_id ): \WP_Post {
		$source = get_post( $source_id );

		if ( ! $source instanceof \WP_Post || ! in_array( $source->post_type, array( 'post', 'page' ), true ) ) {
			throw new \RuntimeException( 'Native recording source is unavailable or unsupported.' );
		}

		return $source;
	}

	/**
	 * Persists the minimum neutral workspace identity metadata.
	 */
	private function markWorkspace( int $workspace_id, RecordingWorkspaceRequest $request, \WP_Post $source ): void {
		update_post_meta( $workspace_id, RecordingWorkspaceMetadata::NATIVE_MARKER, '1' );
		update_post_meta( $workspace_id, RecordingWorkspaceMetadata::SESSION_UUID, $request->sessionUuid()->value() );
		update_post_meta( $workspace_id, RecordingWorkspaceMetadata::SOURCE_ID, $source->ID );
		update_post_meta( $workspace_id, RecordingWorkspaceMetadata::OWNER_ID, $request->ownerId() );
		update_post_meta( $workspace_id, RecordingWorkspaceMetadata::DESIRED_STATUS, $source->post_status );
	}

	/**
	 * Copies registered taxonomy assignments through WordPress APIs.
	 */
	private function copyTaxonomies( \WP_Post $source, int $workspace_id ): void {
		foreach ( get_object_taxonomies( $source->post_type, 'names' ) as $taxonomy ) {
			$term_ids = wp_get_object_terms( $source->ID, $taxonomy, array( 'fields' => 'ids' ) );

			if ( is_wp_error( $term_ids ) ) {
				throw new \RuntimeException( $term_ids->get_error_message() );
			}

			$result = wp_set_object_terms( $workspace_id, array_map( 'intval', $term_ids ), $taxonomy, false );

			if ( is_wp_error( $result ) ) {
				throw new \RuntimeException( $result->get_error_message() );
			}
		}
	}

	/**
	 * Copies only provider-certified metadata fields.
	 */
	private function copySupportedMetadata( \WP_Post $source, int $workspace_id ): void {
		$thumbnail_id = (int) get_post_thumbnail_id( $source->ID );

		if ( $thumbnail_id > 0 ) {
			set_post_thumbnail( $workspace_id, $thumbnail_id );
		}

		if ( 'page' === $source->post_type ) {
			$template = get_page_template_slug( $source );
			update_post_meta( $workspace_id, '_wp_page_template', is_string( $template ) && '' !== $template ? $template : 'default' );
		}

		foreach ( $this->metadata->allowedFor( $source->post_type ) as $key => $metadata ) {
			$single = (bool) ( $metadata['registration']['single'] ?? false );

			if ( $single ) {
				update_post_meta( $workspace_id, $key, get_post_meta( $source->ID, $key, true ) );
				continue;
			}

			foreach ( get_post_meta( $source->ID, $key, false ) as $value ) {
				add_post_meta( $workspace_id, $key, $value, false );
			}
		}
	}

	/**
	 * Fails closed when session and workspace identity do not match.
	 */
	private function assertWorkspaceMapping( int $workspace_id, int $source_id, Uuid $uuid, bool $require_source = true ): void {
		$workspace        = get_post( $workspace_id );
		$source           = get_post( $source_id );
		$mapped_source_id = (int) get_post_meta( $workspace_id, RecordingWorkspaceMetadata::SOURCE_ID, true );
		$mapped_uuid      = (string) get_post_meta( $workspace_id, RecordingWorkspaceMetadata::SESSION_UUID, true );

		if (
			! $workspace instanceof \WP_Post
			|| ( $require_source && ! $source instanceof \WP_Post )
			|| ( $source instanceof \WP_Post && 0 !== strcmp( $workspace->post_type, $source->post_type ) )
			|| ! NativeWordPressWorkspaceAccessGuard::isWorkspace( $workspace_id )
			|| $source_id !== $mapped_source_id
			|| $uuid->value() !== $mapped_uuid
		) {
			throw new \RuntimeException( 'Recording source and workspace mapping does not match the session.' );
		}
	}
}
