<?php
/**
 * Livada SEO — Internal Linking Suggestions
 *
 * Surfaces a small list of suggested internal links inside the SEO metabox,
 * based on the focus keyword of the current post. The query looks for
 * published posts and pages whose title OR focus keyword shares words with
 * the current focus keyword, and that are not already linked from the post.
 *
 * Data is exposed through an AJAX endpoint so the metabox JS can refresh the
 * list without reloading the editor.
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_Link_Suggestions {

	const MAX_SUGGESTIONS = 8;

	// ── Bootstrap ─────────────────────────────────────────────────────────────

	public static function init(): void {
		add_action( 'wp_ajax_livada_seo_link_suggestions', [ self::class, 'ajax_suggest' ] );
	}

	public static function ajax_suggest(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'edit_posts' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ], 403 );
		}

		$post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
		$keyword = isset( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';

		if ( '' === $keyword ) {
			wp_send_json_success( [ 'suggestions' => [] ] );
		}

		$suggestions = self::find_suggestions( $post_id, $keyword );

		wp_send_json_success( [ 'suggestions' => $suggestions ] );
	}

	/**
	 * Returns a list of suggestion arrays: [ id, title, url, why ].
	 *
	 * @param int    $current_post_id ID of the post being edited (excluded from results).
	 * @param string $keyword         Focus keyword to match against.
	 * @return array<int,array{id:int,title:string,url:string,why:string}>
	 */
	private static function find_suggestions( int $current_post_id, string $keyword ): array {
		// Tokenise the focus keyword into useful search terms (≥ 4 chars).
		$tokens = preg_split( '/\s+/u', mb_strtolower( $keyword, 'UTF-8' ), -1, PREG_SPLIT_NO_EMPTY );
		$tokens = array_values( array_filter( $tokens ?: [], static function ( $t ) {
			return mb_strlen( (string) $t, 'UTF-8' ) >= 4;
		} ) );

		if ( empty( $tokens ) ) {
			return [];
		}

		// Title LIKE for each token, OR'd together. Keeps the query simple
		// without depending on FULLTEXT indexes.
		global $wpdb;

		$conds  = [];
		$params = [];

		foreach ( $tokens as $tok ) {
			$conds[]  = "p.post_title LIKE %s";
			$params[] = '%' . $wpdb->esc_like( $tok ) . '%';
		}

		// Add a meta-key match against focus keyword for richer suggestions.
		$conds[]  = "EXISTS (
			SELECT 1 FROM {$wpdb->postmeta} pm
			WHERE pm.post_id = p.ID
			  AND pm.meta_key = '_livada_seo_focus_keyword'
			  AND pm.meta_value LIKE %s
		)";
		$params[] = '%' . $wpdb->esc_like( $tokens[0] ) . '%';

		$where_or = '(' . implode( ' OR ', $conds ) . ')';

		$sql = "SELECT p.ID, p.post_title
		        FROM {$wpdb->posts} p
		        WHERE p.post_status = 'publish'
		          AND p.post_type IN ('post','page')
		          AND p.ID != %d
		          AND {$where_or}
		        LIMIT 40";

		array_unshift( $params, $current_post_id );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$rows = $wpdb->get_results(
			// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
			$wpdb->prepare( $sql, ...$params )
		);

		if ( empty( $rows ) ) {
			return [];
		}

		// Filter out posts already linked from the current content.
		$current_content = '';
		if ( $current_post_id > 0 ) {
			$current = get_post( $current_post_id );
			if ( $current ) {
				$current_content = (string) $current->post_content;
			}
		}

		$suggestions = [];
		foreach ( $rows as $row ) {
			$url = (string) get_permalink( (int) $row->ID );
			if ( '' === $url ) {
				continue;
			}

			// Skip if the URL or post_id is already linked.
			if ( $current_content !== '' && (
				false !== strpos( $current_content, $url ) ||
				false !== strpos( $current_content, 'data-id="' . (int) $row->ID . '"' )
			) ) {
				continue;
			}

			// Score how many tokens appear in the title — used for ordering.
			$title_lower = mb_strtolower( (string) $row->post_title, 'UTF-8' );
			$score       = 0;
			$matched     = [];
			foreach ( $tokens as $tok ) {
				if ( false !== mb_stripos( $title_lower, $tok ) ) {
					$score++;
					$matched[] = $tok;
				}
			}

			// Cornerstone bonus — flagship articles get pushed to the top of suggestions.
			$is_cornerstone = (bool) get_post_meta( (int) $row->ID, '_livada_seo_cornerstone', true );
			if ( $is_cornerstone ) {
				$score += 5;
			}

			$why = empty( $matched )
				? __( 'Matched on focus keyword meta', 'livada-seo' )
				: sprintf(
					/* translators: %s: matched terms */
					__( 'Title matches: %s', 'livada-seo' ),
					implode( ', ', $matched )
				);

			if ( $is_cornerstone ) {
				$why .= ' · ' . __( 'cornerstone content', 'livada-seo' );
			}

			$suggestions[] = [
				'id'           => (int) $row->ID,
				'title'        => (string) $row->post_title,
				'url'          => $url,
				'score'        => $score,
				'why'          => $why,
				'is_cornerstone' => $is_cornerstone,
			];
		}

		// Sort by score desc, then title asc.
		usort( $suggestions, static function ( $a, $b ) {
			if ( $a['score'] === $b['score'] ) {
				return strcmp( $a['title'], $b['title'] );
			}
			return $b['score'] - $a['score'];
		} );

		return array_slice( $suggestions, 0, self::MAX_SUGGESTIONS );
	}
}
