<?php
/**
 * Elementor image alt integration.
 *
 * @package AIAltTextGenerator
 * @since 1.6.0
 */

namespace AIAltTextGenerator;

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

/**
 * @since 1.6.0
 */
class ElementorIntegration {

	public function register(): void {
		if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
			return;
		}

		add_action( 'wp_ajax_wpps_ai_alt_elementor_scan', [ $this, 'ajaxScan' ] );
		add_action( 'wp_ajax_wpps_ai_alt_elementor_generate', [ $this, 'ajaxGenerate' ] );
		add_action( 'elementor/element/after_section_end', [ $this, 'addEditorControls' ], 10, 3 );
		add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueueEditorAssets' ] );
	}

	public function ajaxScan(): void {
		if ( ! check_ajax_referer( Module::nonceAction(), 'nonce', false ) || ! Capabilities::canView() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}

		wp_send_json_success(
			[
				'missing' => self::countMissingInElementor(),
				'items'   => array_slice( self::findMissingItems(), 0, 50 ),
			]
		);
	}

	public function ajaxGenerate(): void {
		if ( ! check_ajax_referer( Module::nonceAction(), 'nonce', false ) || ! Capabilities::canGenerate() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}

		$attachment_id = isset( $_POST['attachment_id'] ) ? (int) $_POST['attachment_id'] : 0;
		if ( $attachment_id <= 0 || ! current_user_can( 'edit_post', $attachment_id ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid image.', 'wp-powersuite' ) ] );
		}

		if ( ! Settings::hasApiKey() ) {
			wp_send_json_error( [ 'message' => __( 'API key is not configured.', 'wp-powersuite' ) ] );
		}

		if ( ! Module::checkRateLimit() ) {
			wp_send_json_error( [ 'message' => __( 'Rate limit reached. Please wait a moment.', 'wp-powersuite' ) ] );
		}

		$result = GenerationService::generateForAttachment( $attachment_id, ! empty( $_POST['force'] ) );
		if ( $result['status'] !== 'success' ) {
			wp_send_json_error( [ 'message' => $result['message'] ?? __( 'Generation failed.', 'wp-powersuite' ) ] );
		}

		wp_send_json_success(
			[
				'alt_text' => $result['alt_text'] ?? '',
				'message'  => __( 'Alt text generated.', 'wp-powersuite' ),
			]
		);
	}

	/**
	 * @param \Elementor\Controls_Stack $element
	 */
	public function addEditorControls( $element, string $section_id, array $args ): void {
		$name = $element->get_name();
		if ( ! in_array( $name, [ 'image', 'image-box' ], true ) || $section_id !== 'section_image' ) {
			return;
		}

		if ( ! Capabilities::canGenerate() ) {
			return;
		}

		$element->start_controls_section(
			'wpps_ai_alt_section',
			[
				'label' => __( 'AI Alt Text', 'wp-powersuite' ),
				'tab'   => \Elementor\Controls_Manager::TAB_CONTENT,
			]
		);

		$element->add_control(
			'wpps_ai_alt_generate',
			[
				'type'            => \Elementor\Controls_Manager::RAW_HTML,
				'raw'             => '<button type="button" class="elementor-button elementor-button-success wpps-ai-alt-elementor-generate" style="width:100%;">' . esc_html__( 'Generate alt text with AI', 'wp-powersuite' ) . '</button><p class="description" style="margin-top:8px;">' . esc_html__( 'Uses the image from this widget. Saves to the media library when auto-approve is enabled.', 'wp-powersuite' ) . '</p>',
				'content_classes' => 'wpps-ai-alt-elementor-wrap',
			]
		);

		$element->end_controls_section();
	}

	public function enqueueEditorAssets(): void {
		$base = WP_POWERSUITE_PLUGIN_URL . 'modules/ai-alt-text-generator/assets/';
		wp_enqueue_script(
			'wpps-ai-alt-elementor',
			$base . 'ai-alt-text-elementor.js',
			[ 'jquery', 'elementor-editor' ],
			'1.8.0',
			true
		);

		wp_localize_script(
			'wpps-ai-alt-elementor',
			'wppsAiAltElementor',
			[
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( Module::nonceAction() ),
				'strings' => [
					'generating' => __( 'Generating...', 'wp-powersuite' ),
					'success'    => __( 'Alt text applied to widget.', 'wp-powersuite' ),
					'error'      => __( 'Generation failed.', 'wp-powersuite' ),
					'noImage'    => __( 'Select an image first.', 'wp-powersuite' ),
				],
			]
		);
	}

	public static function isActive(): bool {
		return defined( 'ELEMENTOR_VERSION' );
	}

	public static function countMissingInElementor(): int {
		return count( self::findMissingItems() );
	}

	/**
	 * @return list<array<string, mixed>>
	 */
	public static function findMissingItems(): array {
		if ( ! self::isActive() ) {
			return [];
		}

		global $wpdb;
		$rows = $wpdb->get_results(
			"SELECT post_id, meta_value FROM {$wpdb->postmeta}
			WHERE meta_key = '_elementor_data' AND meta_value != ''
			LIMIT 300",
			ARRAY_A
		);

		$items = [];
		if ( ! is_array( $rows ) ) {
			return [];
		}

		foreach ( $rows as $row ) {
			$post_id = (int) ( $row['post_id'] ?? 0 );
			$data    = json_decode( (string) ( $row['meta_value'] ?? '' ), true );
			if ( ! is_array( $data ) ) {
				continue;
			}

			self::walkElementorNodes( $data, $post_id, $items );
		}

		return $items;
	}

	/**
	 * @param array<string, mixed>|list<mixed> $nodes
	 * @param list<array<string, mixed>>     $items
	 */
	private static function walkElementorNodes( array $nodes, int $post_id, array &$items ): void {
		foreach ( $nodes as $node ) {
			if ( ! is_array( $node ) ) {
				continue;
			}

			$widget = (string) ( $node['widgetType'] ?? '' );
			if ( in_array( $widget, [ 'image', 'image-box', 'gallery', 'image-carousel', 'media-carousel' ], true ) ) {
				$settings = is_array( $node['settings'] ?? null ) ? $node['settings'] : [];
				self::collectFromSettings( $settings, $post_id, $widget, $items );
			}

			if ( ! empty( $node['elements'] ) && is_array( $node['elements'] ) ) {
				self::walkElementorNodes( $node['elements'], $post_id, $items );
			}
		}
	}

	/**
	 * @param array<string, mixed>       $settings
	 * @param list<array<string, mixed>> $items
	 */
	private static function collectFromSettings( array $settings, int $post_id, string $widget, array &$items ): void {
		$candidates = [];

		if ( isset( $settings['image'] ) && is_array( $settings['image'] ) ) {
			$candidates[] = $settings['image'];
		}
		if ( isset( $settings['gallery'] ) && is_array( $settings['gallery'] ) ) {
			foreach ( $settings['gallery'] as $image ) {
				if ( is_array( $image ) ) {
					$candidates[] = $image;
				}
			}
		}
		if ( isset( $settings['carousel'] ) && is_array( $settings['carousel'] ) ) {
			foreach ( $settings['carousel'] as $image ) {
				if ( is_array( $image ) ) {
					$candidates[] = $image;
				}
			}
		}
		if ( ! empty( $settings['id'] ) ) {
			$candidates[] = [
				'id'  => $settings['id'],
				'alt' => $settings['alt'] ?? ( $settings['image_alt'] ?? '' ),
			];
		}

		foreach ( $candidates as $image ) {
			$id = (int) ( $image['id'] ?? 0 );
			if ( $id <= 0 ) {
				continue;
			}

			$alt = trim( (string) ( $image['alt'] ?? '' ) );
			$library_alt = trim( (string) get_post_meta( $id, '_wp_attachment_image_alt', true ) );

			if ( $alt === '' && $library_alt === '' && ! AttachmentMeta::isDecorative( $id ) ) {
				$items[] = [
					'post_id'       => $post_id,
					'attachment_id' => $id,
					'widget'        => $widget,
					'edit_url'      => get_edit_post_link( $post_id, 'raw' ),
				];
			}
		}
	}

	/**
	 * Update Elementor JSON alt fields for an attachment.
	 *
	 * Authorization: When called with $privileged_context = false (default),
	 * only posts the current user can edit will be updated.
	 *
	 * @param int    $attachment_id     The attachment ID.
	 * @param string $alt_text          The alt text to sync.
	 * @param bool   $privileged_context Whether running in a privileged context (skip per-post auth).
	 * @return int Number of posts updated.
	 */
	public static function syncAltForAttachment( int $attachment_id, string $alt_text, bool $privileged_context = false ): int {
		if ( ! self::isActive() || empty( Settings::get()['sync_content_alt'] ) ) {
			return 0;
		}

		return self::syncAltForAttachmentForced( $attachment_id, $alt_text, $privileged_context );
	}

	/**
	 * Update Elementor JSON alt fields regardless of sync_content_alt setting.
	 *
	 * Authorization: When called with $privileged_context = false (default),
	 * only posts the current user can edit will be updated. Use $privileged_context = true
	 * only from explicitly authorized background jobs (e.g., admin-initiated cron tasks).
	 *
	 * @param int    $attachment_id     The attachment ID.
	 * @param string $alt_text          The alt text to sync.
	 * @param bool   $privileged_context Whether running in a privileged context (skip per-post auth).
	 * @return int Number of posts updated.
	 */
	public static function syncAltForAttachmentForced( int $attachment_id, string $alt_text, bool $privileged_context = false ): int {
		if ( ! self::isActive() ) {
			return 0;
		}

		global $wpdb;
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT post_id, meta_id, meta_value FROM {$wpdb->postmeta}
				WHERE meta_key = '_elementor_data'
				AND (meta_value LIKE %s OR meta_value LIKE %s)",
				'%"id":' . $attachment_id . '%',
				'%"id":"' . $attachment_id . '"%'
			),
			ARRAY_A
		);

		$updated = 0;
		if ( ! is_array( $rows ) ) {
			return 0;
		}

		foreach ( $rows as $row ) {
			$post_id = (int) ( $row['post_id'] ?? 0 );

			// Authorization check: ensure the current user can edit the destination post.
			// This prevents privilege escalation where an author modifies editor/admin content.
			if ( ! $privileged_context && ! current_user_can( 'edit_post', $post_id ) ) {
				continue;
			}

			$data = json_decode( (string) ( $row['meta_value'] ?? '' ), true );
			if ( ! is_array( $data ) ) {
				continue;
			}

			$changed = false;
			$data    = self::applyAltToNodes( $data, $attachment_id, $alt_text, $changed );
			if ( ! $changed ) {
				continue;
			}

			$encoded = wp_json_encode( $data );
			if ( ! is_string( $encoded ) ) {
				continue;
			}

			update_metadata( 'post', $post_id, '_elementor_data', wp_slash( $encoded ) );
			++$updated;
		}

		return $updated;
	}

	/**
	 * @param array<string, mixed>|list<mixed> $nodes
	 * @return array<string, mixed>|list<mixed>
	 */
	private static function applyAltToNodes( array $nodes, int $attachment_id, string $alt_text, bool &$changed ): array {
		foreach ( $nodes as &$node ) {
			if ( ! is_array( $node ) ) {
				continue;
			}

			if ( isset( $node['settings'] ) && is_array( $node['settings'] ) ) {
				$node['settings'] = self::applyAltToSettings( $node['settings'], $attachment_id, $alt_text, $changed );
			}

			if ( ! empty( $node['elements'] ) && is_array( $node['elements'] ) ) {
				$node['elements'] = self::applyAltToNodes( $node['elements'], $attachment_id, $alt_text, $changed );
			}
		}
		unset( $node );

		return $nodes;
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	private static function applyAltToSettings( array $settings, int $attachment_id, string $alt_text, bool &$changed ): array {
		$overwrite = ! empty( Settings::get()['sync_overwrite_content_alt'] );

		if ( isset( $settings['image'] ) && is_array( $settings['image'] ) && (int) ( $settings['image']['id'] ?? 0 ) === $attachment_id ) {
			$current = trim( (string) ( $settings['image']['alt'] ?? '' ) );
			if ( $overwrite || $current === '' ) {
				$settings['image']['alt'] = $alt_text;
				$changed = true;
			}
		}

		foreach ( [ 'gallery', 'carousel' ] as $key ) {
			if ( empty( $settings[ $key ] ) || ! is_array( $settings[ $key ] ) ) {
				continue;
			}
			foreach ( $settings[ $key ] as $idx => $image ) {
				if ( ! is_array( $image ) || (int) ( $image['id'] ?? 0 ) !== $attachment_id ) {
					continue;
				}
				$current = trim( (string) ( $image['alt'] ?? '' ) );
				if ( $overwrite || $current === '' ) {
					$settings[ $key ][ $idx ]['alt'] = $alt_text;
					$changed = true;
				}
			}
		}

		if ( (int) ( $settings['id'] ?? 0 ) === $attachment_id ) {
			$current = trim( (string) ( $settings['alt'] ?? $settings['image_alt'] ?? '' ) );
			if ( $overwrite || $current === '' ) {
				$settings['alt'] = $alt_text;
				$changed = true;
			}
		}

		return $settings;
	}
}
