<?php

declare(strict_types=1);

namespace PhpWA\Auth;

use InvalidArgumentException;
use PhpWA\Crypto\CryptoPrimitives;
use PhpWA\Crypto\KeyPair;
use PhpWA\Crypto\X25519;
use PhpWA\Exception\CryptoException;

final class PairingCodeCrypto
{
    public const ITERATIONS = 131072;

    public function wrapCompanionPublic(string $code, string $salt, string $iv, string $publicKey): string
    {
        $this->inputs($code, $salt, 32, 'pairing salt'); $this->length($iv, 16, 'pairing IV');
        $this->length($publicKey, 32, 'companion ephemeral public key');
        return $salt . $iv . $this->aesCtr($publicKey, $this->derive($code, $salt), $iv);
    }

    public function unwrapPrimaryPublic(string $code, string $wrapped): string
    {
        if (strlen($wrapped) !== 80) { throw new InvalidArgumentException('Wrapped primary ephemeral key must contain 80 bytes.'); }
        $salt = substr($wrapped, 0, 32); $iv = substr($wrapped, 32, 16); $ciphertext = substr($wrapped, 48, 32);
        return $this->aesCtr($ciphertext, $this->derive($code, $salt), $iv);
    }

    public function finish(
        string $code,
        KeyPair $companionEphemeral,
        KeyPair $companionIdentity,
        string $wrappedPrimaryEphemeral,
        string $primaryIdentityPublic,
        string $advRandom,
        string $bundleSalt,
        string $bundleNonce
    ): PairingCodeFinishMaterial {
        $this->length($primaryIdentityPublic, 32, 'primary identity public key');
        $this->length($advRandom, 32, 'ADV random value'); $this->length($bundleSalt, 32, 'key-bundle salt');
        $this->length($bundleNonce, 12, 'key-bundle nonce');
        $primaryEphemeral = $this->unwrapPrimaryPublic($code, $wrappedPrimaryEphemeral);
        $ephemeralShared = X25519::sharedSecret($companionEphemeral->privateKey(), $primaryEphemeral);
        $bundleKey = CryptoPrimitives::hkdf($ephemeralShared, 32, $bundleSalt, 'link_code_pairing_key_bundle_encryption_key');
        $plaintext = $companionIdentity->publicKey() . $primaryIdentityPublic . $advRandom;
        $wrappedBundle = $bundleSalt . $bundleNonce . CryptoPrimitives::aesGcmEncrypt($bundleKey, $plaintext, $bundleNonce, '');
        $identityShared = X25519::sharedSecret($companionIdentity->privateKey(), $primaryIdentityPublic);
        $advSecret = CryptoPrimitives::hkdf($ephemeralShared . $identityShared . $advRandom, 32, '', 'adv_secret');
        return new PairingCodeFinishMaterial($wrappedBundle, $advSecret);
    }

    public function derive(string $code, string $salt): string
    {
        $this->inputs($code, $salt, 32, 'pairing salt');
        $key = hash_pbkdf2('sha256', $code, $salt, self::ITERATIONS, 32, true);
        if (!is_string($key) || strlen($key) !== 32) { throw new CryptoException('Pairing-code PBKDF2 failed.'); }
        return $key;
    }

    private function aesCtr(string $input, string $key, string $iv): string
    {
        $output = openssl_encrypt($input, 'aes-256-ctr', $key, OPENSSL_RAW_DATA, $iv);
        if (!is_string($output) || strlen($output) !== strlen($input)) { throw new CryptoException('Pairing-code AES-CTR failed.'); }
        return $output;
    }
    private function inputs(string $code, string $value, int $length, string $name): void
    {
        if (preg_match('/\A[123456789ABCDEFGHJKLMNPQRSTVWXYZ]{8}\z/D', $code) !== 1) {
            throw new InvalidArgumentException('Pairing code must contain eight Crockford characters.');
        }
        $this->length($value, $length, $name);
    }
    private function length(string $value, int $length, string $name): void
    {
        if (strlen($value) !== $length) { throw new InvalidArgumentException(ucfirst($name) . ' has an invalid length.'); }
    }
}
