<?php

declare(strict_types=1);

namespace FormConnector\Integration\CareHQ;

/**
 * CareHQ Request Signing Helpers
 *
 * Shared HMAC-SHA256 helpers for signing outbound CareHQ requests and verifying
 * inbound CareHQ webhook deliveries. The signing secret is issued by CareHQ on
 * the first successful health ping registration and stored locally.
 *
 * Signature algorithm:
 *   payload   = "{timestamp}.{nonce}.{raw_json_body}"
 *   signature = "sha256=" . hex(hmac_sha256(secret, payload))
 *
 * Headers:
 *   x-carehq-signature: sha256=<hex>
 *   x-carehq-timestamp: <unix milliseconds>
 *   x-carehq-nonce:     <unique value>
 *
 * @see two-way.md
 */
class Signing {

	/** Option key for the per-site signing secret issued by CareHQ. */
	public const OPTION_SECRET = 'fc_carehq_signing_secret';

	/** Option key for the last successful health ping time (ISO-8601). */
	public const OPTION_LAST_PING = 'fc_carehq_last_health_ping_time';

	/** Option key for the last successful sync time (ISO-8601). */
	public const OPTION_LAST_SYNC = 'fc_carehq_last_sync_time';

	/** Option key for current health status reported by CareHQ. */
	public const OPTION_HEALTH_STATUS = 'fc_carehq_health_status';

	/** Option key for the latest plugin-version status reported by CareHQ. */
	public const OPTION_PLUGIN_UPDATE = 'fc_carehq_plugin_update';

	/** Option key for the last webhook event received (any type). */
	public const OPTION_LAST_WEBHOOK = 'fc_carehq_last_webhook_event';

	/** Option keys for cached webhook-driven state. */
	public const OPTION_ENTITLEMENT_BANNER  = 'fc_carehq_entitlement_banner';
	public const OPTION_SUBSCRIPTION_STATE  = 'fc_carehq_subscription_state';
	public const OPTION_ADMIN_USER_EVENT    = 'fc_carehq_admin_user_event';

	/** Allowed clock skew when verifying inbound signatures. */
	private const SIGNATURE_WINDOW_MS = 5 * 60 * 1000;

	/** Transient prefix used to detect replayed nonces on inbound webhooks. */
	private const NONCE_TRANSIENT_PREFIX = 'fc_carehq_nonce_';

	/**
	 * Get the locally stored signing secret, if registered with CareHQ.
	 */
	public static function get_secret(): string {
		$secret = get_option(self::OPTION_SECRET, '');
		return is_string($secret) ? $secret : '';
	}

	/**
	 * Persist a freshly issued signing secret. Stored with autoload disabled.
	 */
	public static function store_secret(string $secret): void {
		$secret = trim($secret);
		if ($secret === '') {
			return;
		}
		update_option(self::OPTION_SECRET, $secret, false);
	}

	/**
	 * Build signed HTTP headers for an outbound request.
	 *
	 * The body MUST be the exact raw JSON string that will be transmitted.
	 * Re-encoding after signing will invalidate the signature.
	 *
	 * @param string $body         The raw request body.
	 * @param string $api_key      The CareHQ-issued API key.
	 * @return array<string,string> Header map suitable for wp_remote_request().
	 */
	public static function build_signed_headers(string $body, string $api_key): array {
		$headers = [
			'Content-Type' => 'application/json',
			'Accept'       => 'application/json',
		];

		if ($api_key !== '') {
			$headers['x-api-key'] = $api_key;
		}

		$secret = self::get_secret();
		if ($secret === '') {
			// No secret yet (initial registration) — send unsigned but still keyed.
			return $headers;
		}

		$timestamp = (string) (int) round(microtime(true) * 1000);
		$nonce     = wp_generate_uuid4();
		$payload   = $timestamp . '.' . $nonce . '.' . $body;
		$signature = 'sha256=' . hash_hmac('sha256', $payload, $secret);

		$headers['x-carehq-signature'] = $signature;
		$headers['x-carehq-timestamp'] = $timestamp;
		$headers['x-carehq-nonce']     = $nonce;

		return $headers;
	}

	/**
	 * Verify an inbound CareHQ-signed request.
	 *
	 * Rejects requests with a missing/expired signature, an invalid HMAC, or a
	 * previously-seen nonce (replay protection).
	 *
	 * @return true|\WP_Error True when the signature is valid.
	 */
	public static function verify_inbound(\WP_REST_Request $request): bool|\WP_Error {
		$secret = self::get_secret();
		if ($secret === '') {
			return new \WP_Error(
				'carehq_missing_secret',
				__('CareHQ signing secret is missing. Reconnect your license to register this site.', 'form-connector'),
				['status' => 401]
			);
		}

		$body      = $request->get_body();
		$timestamp = $request->get_header('x_carehq_timestamp') ?: $request->get_header('x-carehq-timestamp');
		$nonce     = $request->get_header('x_carehq_nonce')     ?: $request->get_header('x-carehq-nonce');
		$signature = $request->get_header('x_carehq_signature') ?: $request->get_header('x-carehq-signature');

		if (!$timestamp || !$nonce || !$signature) {
			return new \WP_Error(
				'carehq_missing_signature',
				__('Missing CareHQ signature headers.', 'form-connector'),
				['status' => 401]
			);
		}

		$now_ms = (int) round(microtime(true) * 1000);
		if (abs($now_ms - (int) $timestamp) > self::SIGNATURE_WINDOW_MS) {
			return new \WP_Error(
				'carehq_expired_signature',
				__('CareHQ signature has expired.', 'form-connector'),
				['status' => 401]
			);
		}

		$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $nonce . '.' . $body, $secret);
		if (!hash_equals($expected, (string) $signature)) {
			return new \WP_Error(
				'carehq_invalid_signature',
				__('Invalid CareHQ signature.', 'form-connector'),
				['status' => 401]
			);
		}

		// Replay protection — keep the nonce slightly longer than the window.
		$transient_key = self::NONCE_TRANSIENT_PREFIX . md5((string) $nonce);
		if (get_transient($transient_key)) {
			return new \WP_Error(
				'carehq_replay',
				__('CareHQ webhook nonce has already been used.', 'form-connector'),
				['status' => 409]
			);
		}
		set_transient($transient_key, 1, 10 * MINUTE_IN_SECONDS);

		return true;
	}
}
