<?php
/**
 * Minimal Web Push Sender for PocketBooking Pro PWA
 * 
 * Sends Silent Push notifications with VAPID authentication.
 * Does not encrypt payloads - relies on Service Worker to show notification.
 * 
 * @package SimpleBookingCalendar
 * @since 2.1.0
 */

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

class Simpbook_WebPush_Sender {
    
    private $public_key;
    private $private_key;
    private $subject;

    public function __construct($public_key, $private_key, $subject = '') {
        $this->public_key = $public_key;
        $this->private_key = $private_key;
        $this->subject = $subject ?: 'mailto:' . get_option('admin_email');
    }

    /**
     * Sends a push notification to a single subscription
     * 
     * @param string $subscription_json JSON-encoded subscription object
     * @param mixed $payload_data Array with notification data (title, body, url)
     * @return array ['success' => bool, 'status' => int, 'expired' => bool]
     */
    public function send($subscription_json, $payload_data = null) {
        $subscription = json_decode($subscription_json, true);
        if (!$subscription || !isset($subscription['endpoint'])) {
            // error_log("[Simpbook WebPush] Invalid subscription format");
            return ['success' => false, 'status' => 0, 'expired' => false];
        }

        $endpoint = $subscription['endpoint'];
        
        return $this->send_request($endpoint, $payload_data);
    }

    /**
     * Sends HTTP request to push endpoint
     * 
     * @param string $endpoint Push endpoint URL
     * @param array|null $payload_data Notification data
     * @return array ['success' => bool, 'status' => int, 'expired' => bool]
     */
    private function send_request($endpoint, $payload_data = null) {
        // Parse endpoint
        $url_parts = wp_parse_url($endpoint);
        if (!$url_parts || !isset($url_parts['scheme']) || !isset($url_parts['host'])) {
            // error_log("[Simpbook WebPush] Invalid endpoint URL: $endpoint");
            return ['success' => false, 'status' => 0, 'expired' => false];
        }
        
        $origin = $url_parts['scheme'] . '://' . $url_parts['host'];
        
        // Create VAPID headers
        try {
            $vapid_headers = $this->get_vapid_headers($origin);
        } catch (Exception $e) {
            // error_log("[Simpbook WebPush] VAPID generation failed: " . $e->getMessage());
            return ['success' => false, 'status' => 0, 'expired' => false];
        }
        
        // Prepare payload
        $body = '';
        if ($payload_data && is_array($payload_data)) {
            $body = json_encode($payload_data);
        }
        
        // Prepare headers for wp_remote_post
        $request_headers = array(
            'TTL'          => '86400',
            'Content-Type' => 'application/json',
            'Urgency'      => 'high',
        );

        // Add VAPID headers
        foreach ($vapid_headers as $h) {
            $parts = explode(':', $h, 2);
            if (count($parts) === 2) {
                $request_headers[trim($parts[0])] = trim($parts[1]);
            }
        }

        // Send request using wp_remote_post
        $response = wp_remote_post($endpoint, array(
            'headers'   => $request_headers,
            'body'      => $body,
            'timeout'   => 10,
            'sslverify' => true,
        ));

        if (is_wp_error($response)) {
            $error = $response->get_error_message();
            // error_log("[Simpbook WebPush] WP Remote Request failed: $error");
            return array('success' => false, 'status' => 0, 'expired' => false);
        }

        $status = wp_remote_retrieve_response_code($response);
        $response_body = wp_remote_retrieve_body($response);

        // HTTP 410 = Gone (subscription expired)
        // HTTP 404 = Not Found (subscription invalid)
        $expired = ($status === 410 || $status === 404);

        if ($status < 200 || $status >= 300) {
            // error_log("[Simpbook WebPush] HTTP $status for $endpoint - Response: $response_body");
            return array('success' => false, 'status' => $status, 'expired' => $expired);
        }

        return ['success' => true, 'status' => $status, 'expired' => false];
    }
    
    /**
     * Generate VAPID authorization headers
     */
    private function get_vapid_headers($audience) {
        // JWT Header
        $jwt_header = [
            'typ' => 'JWT',
            'alg' => 'ES256'
        ];
        
        // JWT Payload
        $jwt_payload = [
            'aud' => $audience,
            'exp' => time() + 43200, // 12 hours
            'sub' => $this->subject
        ];
        
        // Encode header and payload
        $segments = [];
        $segments[] = $this->base64url_encode(json_encode($jwt_header));
        $segments[] = $this->base64url_encode(json_encode($jwt_payload));
        
        $signing_input = implode('.', $segments);
        
        // Sign with private key
        $signature = $this->sign_es256($signing_input, $this->private_key);
        $segments[] = $this->base64url_encode($signature);
        
        $jwt = implode('.', $segments);
        
        // Public key in uncompressed format
        $public_key_uncompressed = $this->base64url_decode($this->public_key);
        
        return [
            'Authorization: vapid t=' . $jwt . ', k=' . $this->public_key
        ];
    }
    
    /**
     * Sign data using ES256 (ECDSA with P-256 and SHA-256)
     */
    private function sign_es256($data, $private_key_base64url) {
        // Decode the private key
        $private_key_raw = $this->base64url_decode($private_key_base64url);
        
        if (strlen($private_key_raw) !== 32) {
            throw new Exception(esc_html('Invalid private key length: ' . strlen($private_key_raw) . ' (expected 32)'));
        }
        
        // Build minimal EC private key DER structure for P-256
        $der = $this->build_ec_private_key_der($private_key_raw);
        
        // Convert to PEM
        $pem = "-----BEGIN EC PRIVATE KEY-----\n";
        $pem .= chunk_split(base64_encode($der), 64, "\n");
        $pem .= "-----END EC PRIVATE KEY-----";
        
        // Sign
        $signature_der = '';
        $result = openssl_sign($data, $signature_der, $pem, OPENSSL_ALGO_SHA256);
        
        if (!$result) {
            throw new Exception('OpenSSL signing failed');
        }
        
        // Convert DER signature to raw format (R || S, 64 bytes total)
        $signature_raw = $this->der_to_raw_signature($signature_der);
        
        if (strlen($signature_raw) !== 64) {
            throw new Exception(esc_html('Invalid signature length: ' . strlen($signature_raw) . ' (expected 64)'));
        }
        
        return $signature_raw;
    }
    
    /**
     * Build EC Private Key DER structure for P-256 curve
     */
    private function build_ec_private_key_der($d) {
        // ECPrivateKey ::= SEQUENCE {
        //   version INTEGER { ecPrivkeyVer1(1) },
        //   privateKey OCTET STRING,
        //   parameters [0] EXPLICIT ECParameters {{ NamedCurve }} OPTIONAL,
        //   publicKey [1] EXPLICIT BIT STRING OPTIONAL
        // }
        
        $version = "\x02\x01\x01"; // INTEGER 1
        $private_key_octet = "\x04" . chr(strlen($d)) . $d; // OCTET STRING
        
        // OID for prime256v1 (1.2.840.10045.3.1.7)
        $oid = "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07";
        $parameters = "\xa0" . chr(strlen($oid)) . $oid; // [0] EXPLICIT
        
        $sequence_content = $version . $private_key_octet . $parameters;
        $sequence = "\x30" . chr(strlen($sequence_content)) . $sequence_content;
        
        return $sequence;
    }
    
    /**
     * Convert DER signature to raw format
     */
    private function der_to_raw_signature($der) {
        // DER: SEQUENCE { INTEGER r, INTEGER s }
        $offset = 0;
        
        // Check SEQUENCE tag
        if (ord($der[$offset++]) !== 0x30) {
            throw new Exception('Invalid DER signature: expected SEQUENCE');
        }
        
        // Skip SEQUENCE length
        $offset++;
        
        // Parse r
        if (ord($der[$offset++]) !== 0x02) {
            throw new Exception('Invalid DER signature: expected INTEGER for r');
        }
        $r_len = ord($der[$offset++]);
        $r = substr($der, $offset, $r_len);
        $offset += $r_len;
        
        // Parse s
        if (ord($der[$offset++]) !== 0x02) {
            throw new Exception('Invalid DER signature: expected INTEGER for s');
        }
        $s_len = ord($der[$offset++]);
        $s = substr($der, $offset, $s_len);
        
        // Remove leading zero bytes (added for signing)
        $r = ltrim($r, "\x00");
        $s = ltrim($s, "\x00");
        
        // Pad to 32 bytes each
        $r = str_pad($r, 32, "\x00", STR_PAD_LEFT);
        $s = str_pad($s, 32, "\x00", STR_PAD_LEFT);
        
        return $r . $s;
    }
    
    /**
     * Base64 URL encode
     */
    private function base64url_encode($data) {
        return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
    }
    
    /**
     * Base64 URL decode
     */
    private function base64url_decode($data) {
        $remainder = strlen($data) % 4;
        if ($remainder) {
            $data .= str_repeat('=', 4 - $remainder);
        }
        return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
    }
}
