<?php

declare(strict_types=1);

namespace App\Framework\WebPush\Services;

use App\Framework\WebPush\ValueObjects\PushMessage;
use App\Framework\WebPush\ValueObjects\PushNotification;
use App\Framework\WebPush\ValueObjects\PushSubscription;
use App\Framework\WebPush\ValueObjects\SendResult;
use App\Framework\WebPush\ValueObjects\VapidKeyPair;

/**
 * Web Push Notification Service.
 *
 * Sendet Web Push Notifications an Browser mit VAPID Authentication.
 * Implementiert RFC 8291 (Message Encryption) und RFC 8292 (VAPID).
 */
final readonly class WebPushService
{
    public function __construct(
        private VapidKeyPair $vapidKeys,
        private string $vapidSubject // Email or URL (e.g., mailto:admin@example.com)
    ) {
    }

    /**
     * Sendet Push Notification an Subscription.
     */
    public function send(PushNotification $notification): SendResult
    {
        try {
            $subscription = $notification->getSubscription();

            // Generate VAPID headers
            $vapidHeaders = $this->generateVapidHeaders($subscription);

            // Encrypt payload
            $encryptedPayload = $this->encryptPayload(
                $notification->getPayload(),
                $subscription
            );

            // Send HTTP request to push service
            return $this->sendRequest(
                $subscription,
                $encryptedPayload,
                $vapidHeaders,
                $notification->getMessage()->ttl ?? 86400
            );
        } catch (\Exception $e) {
            return SendResult::failure(500, $e->getMessage());
        }
    }

    /**
     * Sendet Push Message an einzelne Subscription.
     */
    public function sendTo(PushSubscription $subscription, PushMessage $message): SendResult
    {
        $notification = new PushNotification($message, $subscription);

        return $this->send($notification);
    }

    /**
     * Sendet Push Message an mehrere Subscriptions.
     *
     * @param PushSubscription[] $subscriptions
     * @return SendResult[]
     */
    public function sendToMany(array $subscriptions, PushMessage $message): array
    {
        $results = [];

        foreach ($subscriptions as $subscription) {
            $results[] = $this->sendTo($subscription, $message);
        }

        return $results;
    }

    /**
     * Generiert VAPID Authorization Headers.
     *
     * @return array<string, string>
     */
    private function generateVapidHeaders(PushSubscription $subscription): array
    {
        $endpoint = $subscription->endpoint->toString();
        $url = parse_url($endpoint);
        $audience = ($url['scheme'] ?? 'https') . '://' . ($url['host'] ?? 'push.service');

        // Generate JWT token
        $jwt = $this->generateVapidJwt($audience);

        // Generate Crypto-Key header
        $publicKey = $this->base64UrlDecode($this->vapidKeys->publicKey);
        $cryptoKey = 'p256ecdsa=' . $this->base64UrlEncode($publicKey);

        return [
            'Authorization' => 'WebPush ' . $jwt,
            'Crypto-Key' => $cryptoKey,
        ];
    }

    /**
     * Generiert VAPID JWT Token nach RFC 8292.
     */
    private function generateVapidJwt(string $audience): string
    {
        $header = [
            'typ' => 'JWT',
            'alg' => 'ES256',
        ];

        $payload = [
            'aud' => $audience,
            'exp' => time() + 43200, // 12 hours
            'sub' => $this->vapidSubject,
        ];

        $headerEncoded = $this->base64UrlEncode(json_encode($header));
        $payloadEncoded = $this->base64UrlEncode(json_encode($payload));
        $dataToSign = $headerEncoded . '.' . $payloadEncoded;

        // Sign with VAPID private key (ES256 = ECDSA with P-256 and SHA-256)
        $privateKeyPem = $this->convertPrivateKeyToPem($this->vapidKeys->privateKey);
        $privateKey = openssl_pkey_get_private($privateKeyPem);

        if ($privateKey === false) {
            throw new \RuntimeException('Failed to load VAPID private key');
        }

        $signature = '';
        $success = openssl_sign($dataToSign, $signature, $privateKey, OPENSSL_ALGO_SHA256);

        if (! $success) {
            throw new \RuntimeException('Failed to sign JWT');
        }

        // Convert DER signature to raw format (R + S)
        $signature = $this->convertDerSignatureToRaw($signature);

        return $dataToSign . '.' . $this->base64UrlEncode($signature);
    }

    /**
     * Verschlüsselt Payload für Push Service nach RFC 8291.
     *
     * @return array{body: string, headers: array<string, string>}
     */
    private function encryptPayload(string $payload, PushSubscription $subscription): array
    {
        // Generate local key pair for ECDH
        $localKeyPair = $this->generateLocalKeyPair();

        // Perform ECDH to get shared secret
        $sharedSecret = $this->performEcdh(
            $localKeyPair['private'],
            $this->base64UrlDecode($subscription->p256dhKey)
        );

        // Derive encryption key and nonce
        $salt = random_bytes(16);
        $authSecret = $this->base64UrlDecode($subscription->authToken);

        $keys = $this->deriveKeys($sharedSecret, $authSecret, $salt);

        // Encrypt payload with AES-GCM
        $encrypted = $this->aesGcmEncrypt($payload, $keys['key'], $keys['nonce']);

        // Build headers
        $headers = [
            'Content-Encoding' => 'aes128gcm',
            'Encryption' => 'salt=' . $this->base64UrlEncode($salt),
        ];

        return [
            'body' => $encrypted,
            'headers' => $headers,
        ];
    }

    /**
     * Sendet HTTP Request an Push Service.
     *
     * @param array<string, string> $vapidHeaders
     * @param array{body: string, headers: array<string, string>} $encryptedPayload
     */
    private function sendRequest(
        PushSubscription $subscription,
        array $encryptedPayload,
        array $vapidHeaders,
        int $ttl
    ): SendResult {
        $ch = curl_init($subscription->endpoint->toString());

        if ($ch === false) {
            return SendResult::failure(500, 'Failed to initialize cURL');
        }

        $headers = array_merge(
            ['Content-Type: application/octet-stream'],
            ['TTL: ' . $ttl],
            array_map(
                fn ($key, $value) => $key . ': ' . $value,
                array_keys($vapidHeaders),
                $vapidHeaders
            ),
            array_map(
                fn ($key, $value) => $key . ': ' . $value,
                array_keys($encryptedPayload['headers']),
                $encryptedPayload['headers']
            )
        );

        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $encryptedPayload['body'],
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
        ]);

        $response = curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($statusCode === 201) {
            return SendResult::success($statusCode);
        }

        if ($statusCode === 404 || $statusCode === 410) {
            return SendResult::subscriptionExpired($statusCode);
        }

        return SendResult::failure(
            $statusCode,
            'Push service returned status ' . $statusCode . ': ' . $response
        );
    }

    /**
     * Generiert lokales EC Key Pair für ECDH.
     *
     * @return array{public: string, private: resource}
     */
    private function generateLocalKeyPair(): array
    {
        $config = [
            'curve_name' => 'prime256v1',
            'private_key_type' => OPENSSL_KEYTYPE_EC,
        ];

        $resource = openssl_pkey_new($config);

        if ($resource === false) {
            throw new \RuntimeException('Failed to generate local key pair');
        }

        $details = openssl_pkey_get_details($resource);

        if ($details === false || ! isset($details['ec']['x']) || ! isset($details['ec']['y'])) {
            throw new \RuntimeException('Failed to extract public key');
        }

        $publicKey = "\x04" . $details['ec']['x'] . $details['ec']['y'];

        return [
            'public' => $publicKey,
            'private' => $resource,
        ];
    }

    /**
     * Führt ECDH Key Agreement durch.
     *
     * @param resource $privateKey
     */
    private function performEcdh($privateKey, string $publicKey): string
    {
        $sharedSecret = openssl_pkey_derive($publicKey, $privateKey);

        if ($sharedSecret === false) {
            throw new \RuntimeException('ECDH key agreement failed');
        }

        return $sharedSecret;
    }

    /**
     * Leitet Encryption Key und Nonce ab (HKDF).
     *
     * @return array{key: string, nonce: string}
     */
    private function deriveKeys(string $sharedSecret, string $authSecret, string $salt): array
    {
        // Implementation of HKDF for Web Push
        // This is simplified - production should use proper HKDF implementation
        $info = 'Content-Encoding: aes128gcm' . "\x00";
        $prk = hash_hmac('sha256', $sharedSecret . $authSecret, $salt, true);

        $key = hash_hmac('sha256', $info . "\x01", $prk, true);
        $nonce = hash_hmac('sha256', $info . "\x02", $prk, true);

        return [
            'key' => substr($key, 0, 16),
            'nonce' => substr($nonce, 0, 12),
        ];
    }

    /**
     * Verschlüsselt mit AES-GCM.
     */
    private function aesGcmEncrypt(string $data, string $key, string $nonce): string
    {
        // Pad data
        $paddedData = "\x00\x00" . $data;

        $tag = '';
        $encrypted = openssl_encrypt(
            $paddedData,
            'aes-128-gcm',
            $key,
            OPENSSL_RAW_DATA,
            $nonce,
            $tag
        );

        if ($encrypted === false) {
            throw new \RuntimeException('AES-GCM encryption failed');
        }

        return $encrypted . $tag;
    }

    /**
     * Konvertiert DER Signature zu Raw Format (für ES256).
     */
    private function convertDerSignatureToRaw(string $derSignature): string
    {
        // ES256 signature is 64 bytes: 32 bytes R + 32 bytes S
        // DER encoding needs to be converted to raw format
        // Simplified implementation - production should use proper ASN.1 parser

        $pos = 0;
        $length = strlen($derSignature);

        // Skip SEQUENCE tag
        if ($derSignature[$pos] !== "\x30") {
            throw new \RuntimeException('Invalid DER signature');
        }
        $pos += 2;

        // Extract R
        if ($derSignature[$pos] !== "\x02") {
            throw new \RuntimeException('Invalid DER signature');
        }
        $pos++;
        $rLength = ord($derSignature[$pos]);
        $pos++;
        $r = substr($derSignature, $pos, $rLength);
        $pos += $rLength;

        // Extract S
        if ($derSignature[$pos] !== "\x02") {
            throw new \RuntimeException('Invalid DER signature');
        }
        $pos++;
        $sLength = ord($derSignature[$pos]);
        $pos++;
        $s = substr($derSignature, $pos, $sLength);

        // Pad to 32 bytes each
        $r = str_pad(ltrim($r, "\x00"), 32, "\x00", STR_PAD_LEFT);
        $s = str_pad(ltrim($s, "\x00"), 32, "\x00", STR_PAD_LEFT);

        return $r . $s;
    }

    /**
     * Konvertiert VAPID Private Key zu PEM Format.
     */
    private function convertPrivateKeyToPem(string $base64UrlKey): string
    {
        $privateKeyBinary = $this->base64UrlDecode($base64UrlKey);

        // Build EC private key in PEM format
        $der = $this->buildEcPrivateKeyDer($privateKeyBinary);
        $pem = "-----BEGIN EC PRIVATE KEY-----\n";
        $pem .= chunk_split(base64_encode($der), 64, "\n");
        $pem .= "-----END EC PRIVATE KEY-----\n";

        return $pem;
    }

    /**
     * Baut EC Private Key DER Structure.
     */
    private function buildEcPrivateKeyDer(string $privateKey): string
    {
        // Simplified DER encoding for EC private key
        // Production should use proper ASN.1 encoder
        $oid = "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07"; // OID for prime256v1

        $sequence = "\x02\x01\x01" // version
            . "\x04\x20" . $privateKey // private key (32 bytes)
            . "\xa0\x0a" . $oid; // curve OID

        return "\x30" . chr(strlen($sequence)) . $sequence;
    }

    /**
     * Base64 URL-safe Encoding.
     */
    private function base64UrlEncode(string $data): string
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

    /**
     * Base64 URL-safe Decoding.
     */
    private function base64UrlDecode(string $data): string
    {
        return base64_decode(strtr($data, '-_', '+/'), true) ?: '';
    }
}
