<?php

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

final class NPATD_Ability_Execution_Guards {
	const DEFAULT_RATE_LIMIT_MAX_REQUESTS   = 5;
	const DEFAULT_RATE_LIMIT_WINDOW_SECONDS = 300;

	public static function init(): void {
		add_action( 'wp_ability_invoked', array( __CLASS__, 'on_ability_invoked' ), 10, 3 );
		add_filter( 'wp_pre_execute_ability', array( __CLASS__, 'maybe_rate_limit' ), 10, 4 );
	}

	public static function on_ability_invoked( string $ability_name, $input, $ability ): void {
		if ( ! self::is_target_ability( $ability_name ) ) {
			return;
		}

		$source = 'unknown';
		if ( is_array( $input ) && isset( $input['source'] ) && is_scalar( $input['source'] ) ) {
			$source = sanitize_key( (string) $input['source'] );
		}

		NPATD_Observability::log_event(
			'ability_invoked',
			array(
				'ability_name' => $ability_name,
				'actor'        => self::actor_label(),
				'source'       => $source,
			)
		);
	}

	public static function maybe_rate_limit( $pre, string $ability_name, $input, $ability ) {
		if ( ! self::is_target_ability( $ability_name ) ) {
			return $pre;
		}

		$max_requests = (int) apply_filters( 'npatd_rate_limit_max_requests', self::DEFAULT_RATE_LIMIT_MAX_REQUESTS, $ability_name );
		$window       = (int) apply_filters( 'npatd_rate_limit_window_seconds', self::DEFAULT_RATE_LIMIT_WINDOW_SECONDS, $ability_name );

		if ( $max_requests <= 0 || $window <= 0 ) {
			return $pre;
		}

		$now        = time();
		$bucket_key = self::rate_limit_key( $ability_name );
		$bucket     = get_transient( $bucket_key );

		if ( ! is_array( $bucket ) || ! isset( $bucket['count'], $bucket['reset_at'] ) ) {
			$bucket = array(
				'count'    => 0,
				'reset_at' => $now + $window,
			);
		}

		$bucket['count']    = max( 0, (int) $bucket['count'] );
		$bucket['reset_at'] = max( $now + 1, (int) $bucket['reset_at'] );

		if ( $bucket['reset_at'] <= $now ) {
			$bucket['count']    = 0;
			$bucket['reset_at'] = $now + $window;
		}

		$bucket['count']++;

		$ttl = max( 1, $bucket['reset_at'] - $now );
		set_transient( $bucket_key, $bucket, $ttl );

		if ( $bucket['count'] <= $max_requests ) {
			return $pre;
		}

		$retry_after = max( 1, $bucket['reset_at'] - $now );

		NPATD_Observability::log_event(
			'ability_rate_limited',
			array(
				'ability_name' => $ability_name,
				'actor'        => self::actor_label(),
				'retry_after'  => $retry_after,
			)
		);

		return new WP_Error(
			'ability_rate_limit_exceeded',
			__( 'Too many ability requests. Please retry in a moment.', 'nomad-pipeline-audio-to-draft' ),
			array(
				'status'      => 429,
				'retry_after' => $retry_after,
			)
		);
	}

	private static function is_target_ability( string $ability_name ): bool {
		return NPATD_Plugin::ABILITY_NAME === $ability_name;
	}

	private static function actor_label(): string {
		$user_id = get_current_user_id();
		if ( $user_id > 0 ) {
			return 'user:' . $user_id;
		}

		return 'ip:' . self::hash_ip( self::client_ip() );
	}

	private static function rate_limit_key( string $ability_name ): string {
		$identity = self::actor_label();
		return 'npatd_ability_rl_' . md5( $ability_name . '|' . $identity );
	}

	private static function client_ip(): string {
		if ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && is_string( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
			$parts = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
			$ip    = trim( (string) $parts[0] );
			if ( '' !== $ip ) {
				return $ip;
			}
		}

		if ( isset( $_SERVER['REMOTE_ADDR'] ) && is_string( $_SERVER['REMOTE_ADDR'] ) ) {
			$ip = trim( $_SERVER['REMOTE_ADDR'] );
			if ( '' !== $ip ) {
				return $ip;
			}
		}

		return 'unknown';
	}

	private static function hash_ip( string $ip ): string {
		$hash = hash_hmac( 'sha256', $ip, wp_salt( 'auth' ) );
		return substr( $hash, 0, 12 );
	}
}