<?php
declare(strict_types=1);

namespace Wohno\Plugin\Ajax;

use Wohno\Plugin\Api\Client;
use Wohno\Plugin\Api\ListingQuery;
use Wohno\Plugin\Render\ListingsRenderer;

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

/**
 * `admin-ajax` endpoint for cursor-based "load more" pagination.
 *
 * Returns the next page of cards as escaped HTML. Reads only public, non-PII
 * listing data; nonce-protected, IP-rate-limited, and all inputs re-sanitised
 * through {@see ListingQuery::from_atts()} (a bounded param surface).
 */
final class LoadMore {

	public const ACTION = 'wohno_load_more';
	public const NONCE  = 'wohno_load_more';

	private const RATE_LIMIT       = 30;
	private const RATE_WINDOW_SECS = MINUTE_IN_SECONDS;

	/**
	 * Constructor.
	 *
	 * @param Client           $client   API client.
	 * @param ListingsRenderer $renderer HTML renderer.
	 */
	public function __construct(
		private readonly Client $client,
		private readonly ListingsRenderer $renderer
	) {
	}

	/**
	 * Register the AJAX handlers (logged-in + logged-out visitors).
	 *
	 * @return void
	 */
	public function register(): void {
		add_action( 'wp_ajax_' . self::ACTION, array( $this, 'handle' ) );
		add_action( 'wp_ajax_nopriv_' . self::ACTION, array( $this, 'handle' ) );
	}

	/**
	 * Handle a load-more request and emit a JSON response.
	 *
	 * @return void
	 */
	public function handle(): void {
		if ( ! check_ajax_referer( self::NONCE, 'nonce', false ) ) {
			wp_send_json_error( array( 'message' => __( 'Sicherheitsprüfung fehlgeschlagen.', 'wohno' ) ), 403 );
		}
		if ( ! $this->allow_request() ) {
			wp_send_json_error( array( 'message' => __( 'Zu viele Anfragen.', 'wohno' ) ), 429 );
		}

		$query_json = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
		$cursor     = isset( $_POST['cursor'] ) ? sanitize_text_field( wp_unslash( $_POST['cursor'] ) ) : '';

		// "Load more" always carries a real cursor (page ≥ 2). Requiring it keeps
		// this unauthenticated endpoint from seeding the first-page prewarm
		// registry with attacker-chosen queries.
		if ( '' === $cursor ) {
			wp_send_json_error( array( 'message' => __( 'Ungültige Anfrage.', 'wohno' ) ), 400 );
		}

		$decoded        = json_decode( $query_json, true );
		$atts           = is_array( $decoded ) ? $decoded : array();
		$atts['cursor'] = $cursor;

		// from_atts re-sanitises every field and clamps it to the allowed surface.
		$result = $this->client->listings( ListingQuery::from_atts( $atts ) );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		$pagination = $result['pagination'];
		wp_send_json_success(
			array(
				'html'        => $this->renderer->cards_html( $result['listings'] ),
				'next_cursor' => $pagination['next_cursor'] ?? null,
				'has_more'    => ! empty( $pagination['has_more'] ),
			)
		);
	}

	/**
	 * Simple per-IP rate limit (best-effort, transient-backed).
	 *
	 * @return bool True if the request is within the limit.
	 */
	private function allow_request(): bool {
		$ip  = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown';
		$key = 'wohno_lm_' . md5( $ip );

		$count = (int) get_transient( $key );
		if ( $count >= self::RATE_LIMIT ) {
			return false;
		}
		set_transient( $key, $count + 1, self::RATE_WINDOW_SECS );

		return true;
	}
}
