<?php
/**
 * SecurWPKeyManager handles site identity and loopback authentication.
 *
 * Derives all keys from the shared MasterKey:
 * - Ed25519 site keypair for proof-of-possession (activation, recovery).
 * - Per-scan HMAC secrets for loopback request verification.
 *
 */
declare(strict_types=1);

namespace SecurWP\Includes;

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

class SecurWPKeyManager {

    private const ED25519_HKDF_INFO  = 'securwp-ed25519-v2';
    private const SCAN_HMAC_HKDF_INFO = 'securwp-scan-hmac-v1';
    private const SERVER_VERIFY_HMAC_HKDF_INFO = 'securwp-server-verify-hmac-v1';

    /**
     * Derive a per-scan secret for loopback verification.
     *
     * @param string $scan_id Scan UUID.
     * @param string $action  Action name.
     * @return string Hex-encoded HMAC secret, or empty string on failure.
     */
    public static function get_scan_secret( string $scan_id, string $action ): string {
        if ( $scan_id === '' || $action === '' ) {
            return '';
        }

        try {
            $master = MasterKey::get();
        } catch ( \Throwable $e ) {
            return '';
        }

        $scan_base = hash_hkdf( 'sha256', $master, 32, self::SCAN_HMAC_HKDF_INFO );
        $host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
        $payload = strlen( $scan_id ) . ':' . $scan_id . strlen( $action ) . ':' . $action . strlen( $host ) . ':' . $host;
        return hash_hmac( 'sha256', $payload, $scan_base );
    }

    /**
     * Build a loopback signature for a dispatch.
     *
     * @param string $scan_id   Scan UUID.
     * @param string $action    Action name.
     * @param string $token     Dispatch token.
     * @param int    $timestamp Unix timestamp.
     * @return string Hex-encoded HMAC signature.
     */
    public static function build_signature( string $scan_id, string $action, string $token, int $timestamp ): string {
        $secret = self::get_scan_secret( $scan_id, $action );
        if ( $secret === '' ) {
            return '';
        }

        return hash_hmac( 'sha256', $scan_id . '|' . $action . '|' . $token . '|' . $timestamp, $secret );
    }

    /**
     * Derive the HMAC secret used for signed internal server-verification requests.
     *
     * @return string Hex-encoded HMAC secret, or empty string on failure.
     */
    public static function get_server_verification_secret(): string {
        try {
            $master = MasterKey::get();
        } catch ( \Throwable $e ) {
            return '';
        }

        try {
            $verify_base = hash_hkdf( 'sha256', $master, 32, self::SERVER_VERIFY_HMAC_HKDF_INFO );
            $host        = (string) wp_parse_url( home_url(), PHP_URL_HOST );
            $payload     = strlen( $host ) . ':' . $host;

            return hash_hmac( 'sha256', $payload, $verify_base );
        } finally {
            if ( extension_loaded( 'sodium' ) ) {
                sodium_memzero( $master );
                if ( isset( $verify_base ) ) {
                    sodium_memzero( $verify_base );
                }
            }
        }
    }

    /**
     * Build a signed internal verification request signature.
     *
     * @param string $method    HTTP method.
     * @param string $path      Request path.
     * @param string $query     Raw query string.
     * @param string $nonce     One-time request nonce.
     * @param int    $timestamp Unix timestamp.
     * @return string Hex-encoded HMAC signature, or empty string on failure.
     */
    public static function build_server_verification_signature( string $method, string $path, string $query, string $nonce, int $timestamp ): string {
        $secret = self::get_server_verification_secret();
        if ( $secret === '' ) {
            return '';
        }

        return hash_hmac(
            'sha256',
            strtoupper( $method ) . '|' . $path . '|' . $query . '|' . $nonce . '|' . $timestamp,
            $secret
        );
    }

    /**
     * stable site public key (base64url).
     * Used by Console during activation to verify request origin.
     *
     * @return string Base64url-encoded public key, or empty string on failure.
     */
    public static function get_site_public_key_b64(): string {
        $pair = self::get_site_keypair();
        if ( $pair === null ) {
            return '';
        }
        return self::b64url_encode( $pair['public'] );
    }

    /**
     * Sign a message with the stable site private key (Ed25519, detached).
     *
     * @param string $message Message bytes (UTF-8).
     * @return string Base64url signature or empty string on failure.
     */
    public static function sign_site_message( string $message ): string {
        $pair = self::get_site_keypair();
        if ( $pair === null ) {
            return '';
        }
        try {
            $sig = sodium_crypto_sign_detached( $message, $pair['secret'] );
            return self::b64url_encode( $sig );
        } catch ( \Throwable $e ) {
            return '';
        }
    }

    /**
     * Derive the stable Ed25519 site keypair from the master key.
     *
     * @return array{public: string, secret: string}|null
     */
    private static function get_site_keypair(): ?array {
        if ( ! function_exists( 'sodium_crypto_sign_seed_keypair' ) ) {
            return null;
        }

        try {
            $master = MasterKey::get();
        } catch ( \Throwable $e ) {
            return null;
        }

        $seed = hash_hkdf( 'sha256', $master, 32, self::ED25519_HKDF_INFO );

        try {
            $keypair = sodium_crypto_sign_seed_keypair( $seed );
            $result = [
                'public' => sodium_crypto_sign_publickey( $keypair ),
                'secret' => sodium_crypto_sign_secretkey( $keypair ),
            ];
            return $result;
        } catch ( \Throwable $e ) {
            return null;
        } finally {
            // Best-effort zeroing of sensitive key material.
            if ( extension_loaded( 'sodium' ) ) {
                sodium_memzero( $master );
                sodium_memzero( $seed );
                if ( isset( $keypair ) ) {
                    sodium_memzero( $keypair );
                }
            }
        }
    }

    /**
     * Base64url encode (RFC 4648 §5, no padding).
     *
     * @param string $bytes Raw bytes.
     * @return string
     */
    private static function b64url_encode( string $bytes ): string {
        return rtrim( strtr( base64_encode( $bytes ), '+/', '-_' ), '=' );
    }
}
