<?php
/**
 * IndexNow API Client
 *
 * Handles communication with the IndexNow API for instant URL indexing.
 *
 * @package SEOvaultSEO\IndexNow
 */

namespace SEOvaultSEO\IndexNow;

use SEOvaultSEO\Core\Options;

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

/**
 * IndexNow API class.
 *
 * Sends URLs to the IndexNow endpoint and logs results.
 */
class IndexNowApi {

	/**
	 * IndexNow API endpoint.
	 *
	 * @var string
	 */
	const API_URL = 'https://api.indexnow.org/indexnow/';

	/**
	 * Maximum URLs per request.
	 *
	 * @var int
	 */
	const MAX_URLS_PER_REQUEST = 10000;

	/**
	 * Throttle limit in seconds between auto-submissions for the same URL.
	 *
	 * @var int
	 */
	const THROTTLE_LIMIT = 10;

	/**
	 * Maximum log entries to keep.
	 *
	 * @var int
	 */
	const MAX_LOG_ENTRIES = 100;

	/**
	 * Last response code.
	 *
	 * @var int
	 */
	protected $last_code = 0;

	/**
	 * Last error message.
	 *
	 * @var string
	 */
	protected $last_error = '';

	/**
	 * Whether the last request was successful.
	 *
	 * @var bool
	 */
	protected $is_success = false;

	/**
	 * Whether the current submission is manual.
	 *
	 * @var bool
	 */
	public $is_manual = true;

	/**
	 * Singleton instance.
	 *
	 * @var IndexNowApi|null
	 */
	private static $instance = null;

	/**
	 * Get singleton instance.
	 *
	 * @return IndexNowApi
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Submit URLs to the IndexNow API.
	 *
	 * @param string|array $urls   URL or array of URLs to submit.
	 * @param bool|null    $manual Whether this is a manual submission.
	 * @return bool True on success, false on failure.
	 */
	public function submit( $urls, $manual = null ) {
		$this->reset();

		if ( null !== $manual ) {
			$this->is_manual = (bool) $manual;
		}

		$key = Options::get_indexnow_key();
		if ( '' === $key ) {
			$this->last_error = __( 'IndexNow API key is not configured.', 'seovault-ai' );
			return false;
		}

		$urls = (array) $urls;
		$urls = array_filter( array_map( 'esc_url_raw', $urls ) );
		if ( empty( $urls ) ) {
			$this->last_error = __( 'No valid URLs to submit.', 'seovault-ai' );
			return false;
		}

		$payload = wp_json_encode(
			array(
				'host'        => $this->get_host(),
				'key'         => $key,
				'keyLocation' => $this->get_key_location(),
				'urlList'     => array_values( $urls ),
			)
		);

		$response = wp_remote_post(
			self::API_URL,
			array(
				'body'    => $payload,
				'headers' => array(
					'Content-Type' => 'application/json',
					'Host'         => $this->get_host(),
				),
				'timeout' => 15,
			)
		);

		if ( is_wp_error( $response ) ) {
			$this->last_error = $response->get_error_message();
			$this->log( $urls, 0, $this->last_error );
			return false;
		}

		$this->last_code = wp_remote_retrieve_response_code( $response );

		if ( in_array( $this->last_code, array( 200, 202, 204 ), true ) ) {
			$this->is_success = true;
			$this->log( $urls, $this->last_code, 'OK' );
			return true;
		}

		$this->last_error = $this->get_error_message_for_code( $this->last_code );
		$this->log( $urls, $this->last_code, $this->last_error );
		return false;
	}

	/**
	 * Get the host for the API payload.
	 *
	 * @return string
	 */
	public function get_host() {
		$host = wp_parse_url( home_url(), PHP_URL_HOST );
		if ( empty( $host ) ) {
			$host = 'localhost';
		}
		return $host;
	}

	/**
	 * Get the API key file location URL.
	 *
	 * @return string
	 */
	public function get_key_location() {
		$key = Options::get_indexnow_key();
		return trailingslashit( home_url() ) . $key . '.txt';
	}

	/**
	 * Get the last error.
	 *
	 * @return string
	 */
	public function get_error() {
		return $this->last_error;
	}

	/**
	 * Get the last response code.
	 *
	 * @return int
	 */
	public function get_response_code() {
		return $this->last_code;
	}

	/**
	 * Check if the last request was successful.
	 *
	 * @return bool
	 */
	public function is_successful() {
		return $this->is_success;
	}

	/**
	 * Log a submission to the database.
	 *
	 * @param array  $urls    URLs submitted.
	 * @param int    $status  HTTP response code.
	 * @param string $message Response message.
	 * @return void
	 */
	public function log( $urls, $status, $message = '' ) {
		$log     = get_option( 'seovaultseo_indexnow_log', array() );
		$log_url = $this->get_loggable_url( $urls );

		if ( '' === $log_url ) {
			return;
		}

		$log[] = array(
			'url'               => $log_url,
			'status'            => (int) $status,
			'manual_submission' => (bool) $this->is_manual,
			'message'           => sanitize_text_field( $message ),
			'time'              => time(),
		);

		$log = array_slice( $log, -self::MAX_LOG_ENTRIES );

		update_option( 'seovaultseo_indexnow_log', $log, false );
	}

	/**
	 * Get the submission log.
	 *
	 * @return array
	 */
	public function get_log() {
		$log = get_option( 'seovaultseo_indexnow_log', array() );
		return is_array( $log ) ? $log : array();
	}

	/**
	 * Clear the submission log.
	 *
	 * @return bool
	 */
	public function clear_log() {
		return delete_option( 'seovaultseo_indexnow_log' );
	}

	/**
	 * Check if a URL was recently submitted to avoid duplicate auto-submissions.
	 *
	 * @param string $url URL to check.
	 * @return bool True if URL was submitted within the throttle window.
	 */
	public function is_throttled( $url ) {
		$log = $this->get_log();
		if ( empty( $log ) ) {
			return false;
		}

		$logs = array_reverse( $log );
		if ( ! empty( $logs[0] ) && $logs[0]['url'] === $url && ( time() - $logs[0]['time'] ) < self::THROTTLE_LIMIT ) {
			return true;
		}

		return false;
	}

	/**
	 * Get a loggable URL string from an array of URLs.
	 * If multiple URLs, returns the first with [+N] suffix.
	 *
	 * @param array $urls URLs.
	 * @return string
	 */
	public function get_loggable_url( $urls ) {
		$urls  = array_values( (array) $urls );
		$count = count( $urls );
		if ( 0 === $count ) {
			return '';
		}

		$url = $urls[0];
		if ( $count > 1 ) {
			$url .= ' [+' . ( $count - 1 ) . ']';
		}

		return $url;
	}

	/**
	 * Generate a new IndexNow API key.
	 *
	 * @return string 32-character hex key.
	 */
	public function generate_key() {
		try {
			return bin2hex( random_bytes( 16 ) );
		} catch ( \Exception $e ) {
			// Fallback must stay hex-only so path validation accepts it.
			return substr( hash( 'sha256', wp_generate_password( 64, true, true ) ), 0, 32 );
		}
	}

	/**
	 * Whether an IndexNow key is safe to use as a filename component.
	 *
	 * Accepts hex keys (current generator) and alphanumeric keys from the
	 * legacy fallback. Rejects path separators and traversal sequences.
	 *
	 * @param string $key API key.
	 * @return bool
	 */
	public static function is_valid_key( $key ) {
		return is_string( $key ) && 1 === preg_match( '/^[a-zA-Z0-9]{16,64}$/', $key );
	}

	/**
	 * Get the key file path on the server.
	 *
	 * @param string $key API key.
	 * @return string Absolute path, or empty string if the key is invalid.
	 */
	public function get_key_file_path( $key = '' ) {
		if ( '' === $key ) {
			$key = Options::get_indexnow_key();
		}
		if ( ! self::is_valid_key( $key ) ) {
			return '';
		}
		return trailingslashit( ABSPATH ) . $key . '.txt';
	}

	/**
	 * Write the key file to the site root.
	 *
	 * @param string $key API key.
	 * @return bool True on success.
	 */
	public function write_key_file( $key ) {
		if ( ! self::is_valid_key( $key ) ) {
			return false;
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';

		global $wp_filesystem;

		if ( ! is_object( $wp_filesystem ) && ! WP_Filesystem() ) {
			return false;
		}

		$file_path = $this->get_key_file_path( $key );
		if ( '' === $file_path ) {
			return false;
		}

		return (bool) $wp_filesystem->put_contents( $file_path, $key, FS_CHMOD_FILE );
	}

	/**
	 * Delete the key file from the site root.
	 *
	 * @param string $key API key.
	 * @return void
	 */
	public function delete_key_file( $key ) {
		if ( ! self::is_valid_key( $key ) ) {
			return;
		}

		$file_path = $this->get_key_file_path( $key );
		if ( '' !== $file_path && file_exists( $file_path ) ) {
			wp_delete_file( $file_path );
		}
	}

	/**
	 * Reset internal state before a new request.
	 *
	 * @return void
	 */
	private function reset() {
		$this->last_error = '';
		$this->last_code  = 0;
		$this->is_success = false;
	}

	/**
	 * Get a human-readable error message for an HTTP status code.
	 *
	 * @param int $code HTTP status code.
	 * @return string
	 */
	private function get_error_message_for_code( $code ) {
		$messages = array(
			400 => __( 'Invalid request.', 'seovault-ai' ),
			403 => __( 'Invalid API key.', 'seovault-ai' ),
			422 => __( 'Invalid URL format.', 'seovault-ai' ),
			429 => __( 'Too many requests. Please try again later.', 'seovault-ai' ),
			500 => __( 'IndexNow server error.', 'seovault-ai' ),
		);

		if ( isset( $messages[ $code ] ) ) {
			return $messages[ $code ];
		}

		return sprintf(
			/* translators: %d: HTTP status code */
			__( 'Unexpected response code: %d', 'seovault-ai' ),
			$code
		);
	}
}
