<?php
/**
 * Batch Processor.
 *
 * Consumes one batch of approved suggestions (size BATCH_SIZE) and
 * applies each: backup → insert link → save. Skips a row if the
 * insertion fails (phrase no longer present, all occurrences blocked,
 * per-source link limit hit).
 *
 * @package Platform\Helm\Modules\Link_Architect
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\Link_Architect;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

final class BatchProcessor {

	public function __construct(
		private BackupManager $backups,
		private LinkInserter $inserter
	) {}

	/**
	 * Process one approval row.
	 *
	 * @param array{source_post_id:int,orphan_url:string,phrase:string} $approval
	 * @return array{status:'success'|'skipped'|'failed',message:string,post_id:int}
	 */
	public function process_one( array $approval ): array {
		$post_id    = (int) ( $approval['source_post_id'] ?? 0 );
		$orphan_url = (string) ( $approval['orphan_url'] ?? '' );
		$phrase     = (string) ( $approval['phrase'] ?? '' );

		if ( $post_id <= 0 || '' === $orphan_url || '' === $phrase ) {
			return [ 'status' => 'failed', 'message' => __( 'Invalid approval payload.', 'platform-helm' ), 'post_id' => $post_id ];
		}

		$post = get_post( $post_id );
		if ( ! $post instanceof \WP_Post ) {
			return [ 'status' => 'failed', 'message' => __( 'Source post no longer exists.', 'platform-helm' ), 'post_id' => $post_id ];
		}
		if ( 'publish' !== $post->post_status ) {
			return [ 'status' => 'skipped', 'message' => __( 'Source post no longer published.', 'platform-helm' ), 'post_id' => $post_id ];
		}

		// Per-source link cap — count how many anchors we've already injected
		// into this post in past runs (via the data-phelm-injected attribute).
		$cap = $this->max_links_per_post();
		$already = $this->count_injected_anchors( $post );
		if ( $already >= $cap ) {
			return [
				'status'  => 'skipped',
				'message' => sprintf(
					/* translators: %d: cap */
					__( 'Source already has %d injected links — at cap.', 'platform-helm' ),
					$cap
				),
				'post_id' => $post_id,
			];
		}

		$original_content = (string) $post->post_content;
		$original_em      = (string) get_post_meta( $post_id, '_elementor_data', true );

		// Try HTML insertion first.
		$content_result = $this->inserter->insert_in_html( $original_content, $phrase, $orphan_url );
		$em_result      = [ 'json' => $original_em, 'modified' => false ];

		// If HTML didn't catch it, try Elementor.
		if ( ! $content_result['modified'] && '' !== $original_em ) {
			$em_result = $this->inserter->insert_in_elementor( $original_em, $phrase, $orphan_url );
		}

		if ( ! $content_result['modified'] && ! $em_result['modified'] ) {
			return [
				'status'  => 'skipped',
				'message' => __( 'Phrase not found in a valid context (skipped headings / existing anchors / scripts).', 'platform-helm' ),
				'post_id' => $post_id,
			];
		}

		// Backup BEFORE writing anything.
		$this->backups->create_backup( $post_id, $original_content, $original_em, $phrase, $orphan_url );

		// Write the content if modified.
		if ( $content_result['modified'] ) {
			$updated = wp_update_post( [
				'ID'           => $post_id,
				'post_content' => $content_result['html'],
			], true );
			if ( is_wp_error( $updated ) ) {
				return [ 'status' => 'failed', 'message' => $updated->get_error_message(), 'post_id' => $post_id ];
			}
		}

		// Write Elementor data if modified.
		if ( $em_result['modified'] ) {
			update_post_meta( $post_id, '_elementor_data', wp_slash( $em_result['json'] ) );
			if ( class_exists( '\Elementor\Plugin' ) ) {
				try {
					\Elementor\Plugin::$instance->files_manager->clear_cache();
				} catch ( \Throwable $e ) {
					// Non-fatal.
				}
			}
		}

		// Record this built link in the shared LinkMemory so the Suggester
		// won't re-suggest the same source→target pair later. Resolve the
		// orphan URL to a post ID; if it doesn't resolve (external/edge case)
		// we simply skip recording — the action below still fires.
		$target_id = url_to_postid( $orphan_url );
		if ( $target_id > 0 ) {
			( new \Platform\Helm\Core\LinkMemory() )->record( $post_id, $target_id );
		}

		/**
		 * Fires after a successful link insertion. Activity Log listens for
		 * this; other modules can hook in too.
		 *
		 * @param int    $post_id     Source post the link was inserted into.
		 * @param string $phrase      Anchor phrase that was wrapped.
		 * @param string $orphan_url  Destination URL (the previously-orphan page).
		 */
		do_action( 'platform_helm_link_inserted', $post_id, $phrase, $orphan_url );

		return [
			'status'  => 'success',
			'message' => sprintf(
				/* translators: %s: phrase */
				__( 'Linked "%s" to orphan URL.', 'platform-helm' ),
				$phrase
			),
			'post_id' => $post_id,
		];
	}

	/**
	 * Count how many `data-phelm-injected="1"` anchors exist in this post.
	 * Cheap heuristic — just substring count.
	 */
	private function count_injected_anchors( \WP_Post $post ): int {
		$hay = (string) $post->post_content;
		$hay .= (string) get_post_meta( $post->ID, '_elementor_data', true );
		return substr_count( $hay, 'data-phelm-injected="1"' );
	}

	private function max_links_per_post(): int {
		/**
		 * Filter the cap on new outbound internal links injected per source
		 * post per Link Architect execution. Default is 3 — SEO sanity.
		 *
		 * @param int $cap
		 */
		$cap = apply_filters(
			'platform_helm_link_architect_max_per_source',
			LinkArchitectModule::DEFAULT_MAX_LINKS_PER_POST
		);
		return max( 1, (int) $cap );
	}
}
