<?php
/**
 * IndexNow (Bing/Yandex/Seznam) submission for Newsroom OS.
 *
 * The IndexNow protocol requires the host to publish a key verification file at a
 * predictable URL. Instead of asking users to upload a file via FTP, we auto-generate
 * a 32-character key and serve it virtually from `/<key>.txt` via a rewrite rule that
 * survives permalink flushes (the public init hook below registers it on every load
 * and template_redirect prints the key body when the matching query var is detected).
 *
 * Gated behind newsai_is_pro(). The shared instant-indexing queue (Google + Bing) is
 * defined in newsroom-ai-assistant.php and only fires for Pro sites.
 *
 * @package Newsroom_AI_Assistant
 */

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

// ============================================================
// Key handling
// ============================================================

if ( ! function_exists( 'newsai_indexnow_get_key' ) ) {
	/**
	 * Returns the IndexNow key, creating one on first request. The key is 32 hex chars
	 * (compatible with the 8-128 char IndexNow spec) and stored as a non-autoloaded option.
	 *
	 * @return string
	 */
	function newsai_indexnow_get_key() {
		$key = (string) get_option( 'newsai_indexnow_key', '' );
		if ( $key !== '' && preg_match( '/^[a-z0-9]{8,128}$/', $key ) ) {
			return $key;
		}
		// 32 alphanumeric chars, lowercased. wp_generate_password( …, false, false )
		// returns [A-Za-z0-9]; lowercasing keeps it inside the IndexNow spec.
		$key = strtolower( wp_generate_password( 32, false, false ) );
		update_option( 'newsai_indexnow_key', $key, false );
		return $key;
	}
}

if ( ! function_exists( 'newsai_indexnow_key_url' ) ) {
	function newsai_indexnow_key_url() {
		return home_url( '/' . newsai_indexnow_get_key() . '.txt' );
	}
}

// ============================================================
// Virtual key file: rewrite + template_redirect handler
// ============================================================

if ( ! function_exists( 'newsai_indexnow_add_rewrite' ) ) {
	function newsai_indexnow_add_rewrite() {
		// Match any 8-128 char alphanumeric .txt at the site root and expose it as a query var.
		add_rewrite_rule( '^([a-zA-Z0-9]{8,128})\.txt$', 'index.php?newsai_indexnow_key=$matches[1]', 'top' );
	}
}
add_action( 'init', 'newsai_indexnow_add_rewrite' );

if ( ! function_exists( 'newsai_indexnow_query_vars' ) ) {
	function newsai_indexnow_query_vars( $vars ) {
		$vars[] = 'newsai_indexnow_key';
		return $vars;
	}
}
add_filter( 'query_vars', 'newsai_indexnow_query_vars' );

if ( ! function_exists( 'newsai_indexnow_serve_key_file' ) ) {
	function newsai_indexnow_serve_key_file() {
		$requested = (string) get_query_var( 'newsai_indexnow_key' );
		if ( $requested === '' ) {
			return;
		}
		// Only respond when the requested filename equals our stored key. Anything else falls
		// through to 404 (we deliberately don't disclose whether the file exists).
		$stored = (string) get_option( 'newsai_indexnow_key', '' );
		if ( $stored === '' || ! hash_equals( $stored, $requested ) ) {
			return;
		}
		nocache_headers();
		status_header( 200 );
		header( 'Content-Type: text/plain; charset=UTF-8' );
		echo esc_html( $stored ); // Body must equal the filename per IndexNow spec.
		exit;
	}
}
add_action( 'template_redirect', 'newsai_indexnow_serve_key_file', 1 );

if ( ! defined( 'NEWSAI_INDEXNOW_REWRITE_VER' ) ) {
	/** Bump when the IndexNow rewrite rule changes so existing sites flush once in admin. */
	define( 'NEWSAI_INDEXNOW_REWRITE_VER', 2 );
}

if ( ! function_exists( 'newsai_indexnow_flush_rewrite_rules' ) ) {
	/**
	 * Register the virtual key-file rule and flush permalinks (activation, settings save, version bump).
	 */
	function newsai_indexnow_flush_rewrite_rules() {
		newsai_indexnow_add_rewrite();
		flush_rewrite_rules( false );
		update_option( 'newsai_indexnow_rewrite_ver', (string) NEWSAI_INDEXNOW_REWRITE_VER, false );
		// Legacy flag from <= 1.8.1 — keep in sync so a cleared option cannot re-trigger a public flush.
		update_option( 'newsai_indexnow_flushed', '1', false );
	}
}

if ( ! function_exists( 'newsai_indexnow_maybe_flush_rules' ) ) {
	/**
	 * One-shot admin-only flush when the rewrite version is behind. Never runs on front-end requests.
	 */
	function newsai_indexnow_maybe_flush_rules() {
		if ( ! is_admin() ) {
			return;
		}
		$stored = (int) get_option( 'newsai_indexnow_rewrite_ver', 0 );
		if ( $stored >= (int) NEWSAI_INDEXNOW_REWRITE_VER ) {
			return;
		}
		// Sites that already flushed under the old `newsai_indexnow_flushed` option: migrate without re-flushing.
		if ( $stored < 1 && get_option( 'newsai_indexnow_flushed', '' ) === '1' ) {
			update_option( 'newsai_indexnow_rewrite_ver', (string) NEWSAI_INDEXNOW_REWRITE_VER, false );
			return;
		}
		newsai_indexnow_flush_rewrite_rules();
	}
}
add_action( 'admin_init', 'newsai_indexnow_maybe_flush_rules', 20 );

// ============================================================
// Submission queue
// ============================================================

if ( ! function_exists( 'newsai_indexnow_queue_submit' ) ) {
	function newsai_indexnow_queue_submit( $post_id, $is_update = false ) {
		if ( ! function_exists( 'newsai_is_pro' ) || ! newsai_is_pro() ) {
			return;
		}
		if ( get_option( 'newsai_indexnow_enabled', 'no' ) !== 'yes' ) {
			return;
		}
		// Reuse the shared per-post lock to avoid duplicates from concurrent save_post callbacks.
		wp_schedule_single_event( time() + 5, 'newsai_indexnow_dispatch', [ (int) $post_id, (bool) $is_update ] );
	}
}

if ( ! function_exists( 'newsai_indexnow_dispatch' ) ) {
	function newsai_indexnow_dispatch( $post_id, $is_update = false ) {
		$post_id = (int) $post_id;
		$post    = get_post( $post_id );
		if ( ! $post || $post->post_status !== 'publish' ) {
			return;
		}
		$url = (string) get_permalink( $post_id );
		if ( $url === '' || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
			return;
		}
		newsai_indexnow_submit( [ $url ], $post_id );
	}
}
add_action( 'newsai_indexnow_dispatch', 'newsai_indexnow_dispatch', 10, 2 );

if ( ! function_exists( 'newsai_indexnow_submit' ) ) {
	/**
	 * Submit one or more URLs to IndexNow. Returns an array with the HTTP code + body excerpt.
	 *
	 * @param string[] $urls    URLs to submit (1-10000 allowed by the spec; we cap at 100).
	 * @param int      $post_id Optional, used to write per-post meta for diagnostics.
	 * @return array{code:int,error:string,url:string}
	 */
	function newsai_indexnow_submit( $urls, $post_id = 0 ) {
		$key = newsai_indexnow_get_key();
		$key_location = newsai_indexnow_key_url();
		$host = wp_parse_url( home_url(), PHP_URL_HOST );
		$urls = array_values(
			array_filter(
				array_map( 'esc_url_raw', (array) $urls ),
				static function ( $u ) { return $u !== '' && filter_var( $u, FILTER_VALIDATE_URL ); }
			)
		);
		if ( empty( $urls ) || $host === null ) {
			$err = 'No URLs to submit.';
			update_option( 'newsai_indexnow_last_error', $err, false );
			return [ 'code' => 0, 'error' => $err, 'url' => '' ];
		}
		if ( count( $urls ) > 100 ) {
			$urls = array_slice( $urls, 0, 100 );
		}
		$endpoint = (string) apply_filters( 'newsai_indexnow_endpoint', 'https://api.indexnow.org/indexnow' );
		$payload  = [
			'host'        => $host,
			'key'         => $key,
			'keyLocation' => $key_location,
			'urlList'     => $urls,
		];
		$response = wp_remote_post(
			$endpoint,
			[
				'timeout' => 8,
				'headers' => [ 'Content-Type' => 'application/json; charset=utf-8' ],
				'body'    => wp_json_encode( $payload ),
			]
		);

		$first_url = $urls[0];
		update_option( 'newsai_indexnow_last_ping', time(), false );
		update_option( 'newsai_indexnow_last_url', esc_url_raw( $first_url ), false );

		if ( is_wp_error( $response ) ) {
			$err = sanitize_text_field( $response->get_error_message() );
			update_option( 'newsai_indexnow_last_code', 0, false );
			update_option( 'newsai_indexnow_last_error', $err, false );
			if ( $post_id > 0 ) {
				update_post_meta( $post_id, '_newsai_indexnow_last_error', $err );
				update_post_meta( $post_id, '_newsai_indexnow_last_code', 0 );
				update_post_meta( $post_id, '_newsai_indexnow_last_ping', time() );
			}
			return [ 'code' => 0, 'error' => $err, 'url' => $first_url ];
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = wp_strip_all_tags( (string) wp_remote_retrieve_body( $response ) );
		update_option( 'newsai_indexnow_last_code', $code, false );

		// IndexNow returns 200 on accepted, 202 on accepted with queueing, 422 with details.
		if ( $code >= 200 && $code < 300 ) {
			delete_option( 'newsai_indexnow_last_error' );
			update_option( 'newsai_indexnow_last_success_code', $code, false );
			update_option( 'newsai_indexnow_last_success_ping', time(), false );
			update_option( 'newsai_indexnow_last_success_url', esc_url_raw( $first_url ), false );
			if ( $post_id > 0 ) {
				delete_post_meta( $post_id, '_newsai_indexnow_last_error' );
				update_post_meta( $post_id, '_newsai_indexnow_last_code', $code );
				update_post_meta( $post_id, '_newsai_indexnow_last_ping', time() );
			}
			return [ 'code' => $code, 'error' => '', 'url' => $first_url ];
		}

		$err = $body !== '' ? sanitize_text_field( mb_substr( $body, 0, 240 ) ) : ( 'HTTP ' . $code );
		update_option( 'newsai_indexnow_last_error', $err, false );
		if ( $post_id > 0 ) {
			update_post_meta( $post_id, '_newsai_indexnow_last_error', $err );
			update_post_meta( $post_id, '_newsai_indexnow_last_code', $code );
			update_post_meta( $post_id, '_newsai_indexnow_last_ping', time() );
		}
		return [ 'code' => $code, 'error' => $err, 'url' => $first_url ];
	}
}

// ============================================================
// AJAX: test submission (admin use)
// ============================================================

if ( ! function_exists( 'newsai_indexnow_ajax_test' ) ) {
	function newsai_indexnow_ajax_test() {
		check_ajax_referer( 'newsai_ajax_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
		}
		if ( ! function_exists( 'newsai_is_pro' ) || ! newsai_is_pro() ) {
			wp_send_json_error( [ 'message' => 'pro_required' ], 402 );
		}
		$key_url  = newsai_indexnow_key_url();
		$expected = (string) get_option( 'newsai_indexnow_key', '' );
		$response = wp_remote_get( $key_url, [ 'timeout' => 8, 'redirection' => 2 ] );
		if ( is_wp_error( $response ) ) {
			wp_send_json_error( [
				'step'    => 'key_file',
				'message' => $response->get_error_message(),
				'url'     => $key_url,
			] );
		}
		$code = (int) wp_remote_retrieve_response_code( $response );
		// Normalize: strip UTF-8 BOM + surrounding whitespace some CDNs/optimizers add.
		$body_raw = (string) wp_remote_retrieve_body( $response );
		$body     = trim( preg_replace( '/^\xEF\xBB\xBF/', '', $body_raw ) );
		if ( $code !== 200 ) {
			wp_send_json_error( [
				'step'    => 'key_file',
				'message' => sprintf( 'Key file unreachable (HTTP %d). Check your permalinks and server rewrites.', $code ),
				'url'     => $key_url,
			] );
		}
		// Accept any response that *contains* the key — covers themes/CDNs that wrap the .txt
		// in extra markup. Compared case-insensitively because the key is alphanumeric lowercase.
		if ( $expected === '' || stripos( $body, $expected ) === false ) {
			wp_send_json_error( [
				'step'    => 'key_file',
				'message' => sprintf( 'Key file reachable (HTTP %d) but body does not contain the expected key. The URL is probably hitting a 404 fallback page.', $code ),
				'url'     => $key_url,
			] );
		}
		$result = newsai_indexnow_submit( [ home_url( '/' ) ] );
		if ( $result['code'] >= 200 && $result['code'] < 300 ) {
			wp_send_json_success( [
				'message'  => sprintf( 'Submitted homepage successfully (HTTP %d).', $result['code'] ),
				'key_url'  => $key_url,
				'code'     => $result['code'],
			] );
		}
		wp_send_json_error( [
			'step'    => 'submit',
			'message' => $result['error'] !== '' ? $result['error'] : ( 'HTTP ' . $result['code'] ),
			'code'    => $result['code'],
			'key_url' => $key_url,
		] );
	}
}
add_action( 'wp_ajax_newsai_indexnow_test', 'newsai_indexnow_ajax_test' );

if ( ! function_exists( 'newsai_indexing_ajax_test' ) ) {
	function newsai_indexing_ajax_test() {
		check_ajax_referer( 'newsai_ajax_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
		}
		if ( ! function_exists( 'newsai_is_pro' ) || ! newsai_is_pro() ) {
			wp_send_json_error( [ 'message' => 'pro_required' ], 402 );
		}
		$token_error = '';
		$token       = function_exists( 'newsai_get_service_account_access_token' )
			? newsai_get_service_account_access_token( $token_error )
			: '';
		if ( $token === '' ) {
			wp_send_json_error( [
				'step'    => 'token',
				'message' => $token_error !== '' ? $token_error : 'Could not obtain Google access token. Verify your service account JSON.',
			] );
		}

		// Send one real URL_UPDATED ping (latest published post) so status updates in Settings.
		$test_url = '';
		$latest   = get_posts(
			[
				'post_type'      => 'post',
				'post_status'    => 'publish',
				'posts_per_page' => 1,
				'orderby'        => 'date',
				'order'          => 'DESC',
				'no_found_rows'  => true,
			]
		);
		if ( ! empty( $latest[0] ) ) {
			$test_url = (string) get_permalink( $latest[0] );
		}
		if ( $test_url === '' || ! filter_var( $test_url, FILTER_VALIDATE_URL ) ) {
			$test_url = (string) home_url( '/' );
		}

		if ( ! function_exists( 'newsai_ping_dispatch_google' ) ) {
			wp_send_json_success( [
				'message'       => __( 'Token OK (ping module not loaded).', 'newsroom-ai-assistant' ),
				'service_email' => (string) get_option( 'newsai_indexing_service_email', '' ),
			] );
		}

		$result = newsai_ping_dispatch_google( $test_url );
		if ( empty( $result['ok'] ) ) {
			wp_send_json_error( [
				'step'    => 'ping',
				'message' => isset( $result['message'] ) ? (string) $result['message'] : __( 'Indexing API ping failed.', 'newsroom-ai-assistant' ),
				'code'    => isset( $result['code'] ) ? (int) $result['code'] : 0,
				'url'     => $test_url,
			] );
		}

		wp_send_json_success( [
			'message'       => sprintf(
				/* translators: 1: HTTP code, 2: tested URL */
				__( 'Ping OK (HTTP %1$d) for %2$s', 'newsroom-ai-assistant' ),
				(int) ( $result['code'] ?? 200 ),
				$test_url
			),
			'service_email' => (string) get_option( 'newsai_indexing_service_email', '' ),
			'url'           => $test_url,
			'code'          => (int) ( $result['code'] ?? 200 ),
		] );
	}
}
add_action( 'wp_ajax_newsai_indexing_test', 'newsai_indexing_ajax_test' );

// ============================================================
// AJAX: submit a single URL on demand (chief dashboard widget).
//
// Routes the URL through whichever pipelines the site has enabled: Google Indexing API
// (re-using the same access-token + URL_UPDATED call as the save-post queue) and/or Bing
// IndexNow. Returns a per-provider result so the UI can show ✅/❌ for each.
// ============================================================

if ( ! function_exists( 'newsai_ping_dispatch_google' ) ) {
	function newsai_ping_dispatch_google( $url ) {
		$safe_url = esc_url_raw( (string) $url );
		$token_error = '';
		$token       = function_exists( 'newsai_get_service_account_access_token' )
			? newsai_get_service_account_access_token( $token_error )
			: '';
		if ( $token === '' ) {
			// Fall back to a manually-pasted bearer token to keep parity with the queue.
			$token = trim( (string) get_option( 'newsai_indexing_bearer_token', '' ) );
		}
		if ( $token === '' ) {
			update_option( 'newsai_indexing_last_code', 0, false );
			update_option( 'newsai_indexing_last_ping', time(), false );
			update_option( 'newsai_indexing_last_url', $safe_url, false );
			update_option(
				'newsai_indexing_last_error',
				$token_error !== '' ? sanitize_text_field( $token_error ) : 'No Google credentials configured.',
				false
			);
			return [
				'ok'      => false,
				'code'    => 0,
				'message' => $token_error !== '' ? $token_error : 'No Google credentials configured.',
			];
		}
		$response = wp_remote_post(
			'https://indexing.googleapis.com/v3/urlNotifications:publish',
			[
				'timeout' => 8,
				'headers' => [
					'Authorization' => 'Bearer ' . $token,
					'Content-Type'  => 'application/json; charset=utf-8',
				],
				'body'    => wp_json_encode( [ 'url' => $safe_url, 'type' => 'URL_UPDATED' ] ),
			]
		);
		if ( is_wp_error( $response ) ) {
			update_option( 'newsai_indexing_last_code', 0, false );
			update_option( 'newsai_indexing_last_ping', time(), false );
			update_option( 'newsai_indexing_last_url', $safe_url, false );
			update_option( 'newsai_indexing_last_error', sanitize_text_field( $response->get_error_message() ), false );
			return [
				'ok'      => false,
				'code'    => 0,
				'message' => sanitize_text_field( $response->get_error_message() ),
			];
		}
		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = trim( (string) wp_remote_retrieve_body( $response ) );
		update_option( 'newsai_indexing_last_code', $code, false );
		update_option( 'newsai_indexing_last_ping', time(), false );
		update_option( 'newsai_indexing_last_url', $safe_url, false );
		if ( $code >= 200 && $code < 300 ) {
			delete_option( 'newsai_indexing_last_error' );
			update_option( 'newsai_indexing_last_success_code', $code, false );
			update_option( 'newsai_indexing_last_success_ping', time(), false );
			update_option( 'newsai_indexing_last_success_url', $safe_url, false );
			return [
				'ok'      => true,
				'code'    => $code,
				'message' => sprintf( 'Accepted (HTTP %d)', $code ),
			];
		}
		$err = $body !== '' ? sanitize_text_field( wp_strip_all_tags( $body ) ) : ( 'HTTP ' . $code );
		update_option( 'newsai_indexing_last_error', mb_substr( $err, 0, 240 ), false );
		return [
			'ok'      => false,
			'code'    => $code,
			'message' => mb_substr( $err, 0, 240 ),
		];
	}
}

if ( ! function_exists( 'newsai_ajax_ping_url' ) ) {
	function newsai_ajax_ping_url() {
		check_ajax_referer( 'newsai_ajax_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
		}
		if ( ! function_exists( 'newsai_is_pro' ) || ! newsai_is_pro() ) {
			wp_send_json_error( [ 'message' => 'pro_required' ], 402 );
		}
		$raw = isset( $_POST['url'] ) ? (string) wp_unslash( $_POST['url'] ) : '';
		$url = esc_url_raw( trim( $raw ) );
		if ( $url === '' || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
			wp_send_json_error( [ 'message' => 'invalid_url' ], 400 );
		}
		// Sanity check: only allow URLs on this site to avoid abuse / accidental leaks.
		$site_host = wp_parse_url( home_url(), PHP_URL_HOST );
		$url_host  = wp_parse_url( $url, PHP_URL_HOST );
		if ( $site_host && $url_host && strcasecmp( $site_host, $url_host ) !== 0 ) {
			wp_send_json_error( [ 'message' => 'URL host does not match this site.' ], 400 );
		}

		$google_on   = ( get_option( 'newsai_indexing_enabled', 'no' ) === 'yes' );
		$indexnow_on = ( get_option( 'newsai_indexnow_enabled', 'no' ) === 'yes' );
		if ( ! $google_on && ! $indexnow_on ) {
			wp_send_json_error( [ 'message' => 'No indexing provider is enabled. Enable Google or IndexNow in Settings → Instant Indexing.' ] );
		}

		$out = [];
		if ( $google_on ) {
			$out['google'] = newsai_ping_dispatch_google( $url );
		}
		if ( $indexnow_on ) {
			$res = newsai_indexnow_submit( [ $url ] );
			$ok  = ( $res['code'] >= 200 && $res['code'] < 300 );
			$out['indexnow'] = [
				'ok'      => $ok,
				'code'    => (int) $res['code'],
				'message' => $ok ? sprintf( 'Accepted (HTTP %d)', $res['code'] ) : ( $res['error'] !== '' ? $res['error'] : ( 'HTTP ' . $res['code'] ) ),
			];
		}
		wp_send_json_success( $out );
	}
}
add_action( 'wp_ajax_newsai_ping_url', 'newsai_ajax_ping_url' );
