<?php

declare(strict_types=1);

namespace PhpWA\Crypto\Signal;

use InvalidArgumentException;
use PhpWA\Crypto\RandomSourceInterface;
use PhpWA\Crypto\SodiumCompatXEdDsa;
use PhpWA\Crypto\SystemRandomSource;
use PhpWA\Crypto\X25519;
use PhpWA\Crypto\XEdDsaInterface;

final class SignalCurve
{
    public const DJB_TYPE = 5;

    /** @var RandomSourceInterface */ private $random;
    /** @var XEdDsaInterface */ private $xeddsa;

    public function __construct(?RandomSourceInterface $random = null, ?XEdDsaInterface $xeddsa = null)
    {
        $this->random = $random ?? new SystemRandomSource();
        $this->xeddsa = $xeddsa ?? new SodiumCompatXEdDsa($this->random);
    }

    public function generateKeyPair(): SignalKeyPair
    {
        $pair = X25519::generate($this->random);
        return new SignalKeyPair(self::prefix($pair->publicKey()), $pair->privateKey());
    }

    public function keyPairFromPrivate(string $privateKey): SignalKeyPair
    {
        $pair = X25519::fromPrivate($privateKey);
        return new SignalKeyPair(self::prefix($pair->publicKey()), $privateKey);
    }

    public function agreement(string $publicKey, string $privateKey): string
    {
        return X25519::sharedSecret($privateKey, self::unprefix($publicKey));
    }

    public function sign(string $privateKey, string $message): string
    {
        return $this->xeddsa->sign($privateKey, $message);
    }

    public function verify(string $publicKey, string $message, string $signature): bool
    {
        try {
            return $this->xeddsa->verify(self::unprefix($publicKey), $message, $signature);
        } catch (\Throwable $error) {
            return false;
        }
    }

    public static function prefix(string $publicKey): string
    {
        if (strlen($publicKey) === 33) {
            self::assertPublicKey($publicKey);
            return $publicKey;
        }
        if (strlen($publicKey) !== 32) {
            throw new InvalidArgumentException('Signal public key must contain 32 or 33 bytes.');
        }
        return chr(self::DJB_TYPE) . $publicKey;
    }

    public static function unprefix(string $publicKey): string
    {
        if (strlen($publicKey) === 32) { return $publicKey; }
        self::assertPublicKey($publicKey);
        return substr($publicKey, 1);
    }

    public static function assertPublicKey(string $publicKey): void
    {
        if (strlen($publicKey) !== 33 || ord($publicKey[0]) !== self::DJB_TYPE) {
            throw new InvalidArgumentException('Signal public key must use the 33-byte DJB format.');
        }
    }
}
