<?php

declare(strict_types=1);

namespace PhpWA\Crypto;

use InvalidArgumentException;
use PhpWA\Exception\CryptoException;

final class CryptoPrimitives
{
    public static function sha256(string $data): string { return hash('sha256', $data, true); }
    public static function sha512(string $data): string { return hash('sha512', $data, true); }
    public static function md5(string $data): string { return hash('md5', $data, true); }
    public static function hmacSha256(string $key, string $data): string { return hash_hmac('sha256', $data, $key, true); }

    public static function hkdf(string $inputKeyMaterial, int $length, string $salt = '', string $info = ''): string
    {
        if ($length < 0 || $length > 255 * 32) {
            throw new InvalidArgumentException('HKDF-SHA256 output length is out of range.');
        }
        if ($length === 0) { return ''; }
        if ($salt === '') { $salt = str_repeat("\0", 32); }
        $pseudoRandomKey = hash_hmac('sha256', $inputKeyMaterial, $salt, true);
        $output = '';
        $previous = '';
        for ($counter = 1; strlen($output) < $length; $counter++) {
            $previous = hash_hmac('sha256', $previous . $info . chr($counter), $pseudoRandomKey, true);
            $output .= $previous;
        }
        return substr($output, 0, $length);
    }

    public static function aesGcmEncrypt(string $key, string $plaintext, string $nonce, string $aad = ''): string
    {
        self::requireLength($key, 32, 'AES-256-GCM key');
        if (strlen($nonce) < 8 || strlen($nonce) > 128) {
            throw new InvalidArgumentException('AES-GCM nonce length is out of range.');
        }
        $tag = '';
        $ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $aad, 16);
        if (!is_string($ciphertext) || strlen($tag) !== 16) {
            throw new CryptoException('AES-GCM encryption failed.');
        }
        return $ciphertext . $tag;
    }

    public static function aesGcmDecrypt(string $key, string $ciphertextAndTag, string $nonce, string $aad = ''): string
    {
        self::requireLength($key, 32, 'AES-256-GCM key');
        if (strlen($nonce) < 8 || strlen($nonce) > 128 || strlen($ciphertextAndTag) < 16) {
            throw new CryptoException('Invalid AES-GCM input.');
        }
        $tag = substr($ciphertextAndTag, -16);
        $ciphertext = substr($ciphertextAndTag, 0, -16);
        $plaintext = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
        if (!is_string($plaintext)) {
            throw new CryptoException('AES-GCM authentication failed.');
        }
        return $plaintext;
    }

    public static function aesCbcEncrypt(string $key, string $plaintext, string $iv): string
    {
        self::requireLength($key, 32, 'AES-256-CBC key');
        self::requireLength($iv, 16, 'AES-CBC IV');
        $padding = 16 - (strlen($plaintext) % 16);
        $padded = $plaintext . str_repeat(chr($padding), $padding);
        $ciphertext = openssl_encrypt($padded, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
        if (!is_string($ciphertext)) {
            throw new CryptoException('AES-CBC encryption failed.');
        }
        return $ciphertext;
    }

    public static function aesCbcDecrypt(string $key, string $ciphertext, string $iv): string
    {
        self::requireLength($key, 32, 'AES-256-CBC key');
        self::requireLength($iv, 16, 'AES-CBC IV');
        if ($ciphertext === '' || (strlen($ciphertext) % 16) !== 0) {
            throw new CryptoException('Invalid AES-CBC ciphertext length.');
        }
        $padded = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
        if (!is_string($padded) || $padded === '') {
            throw new CryptoException('AES-CBC decryption failed.');
        }
        $padding = ord(substr($padded, -1));
        if ($padding < 1 || $padding > 16) {
            throw new CryptoException('Invalid PKCS#7 padding.');
        }
        $expected = str_repeat(chr($padding), $padding);
        if (!hash_equals($expected, substr($padded, -$padding))) {
            throw new CryptoException('Invalid PKCS#7 padding.');
        }
        return substr($padded, 0, -$padding);
    }

    private static function requireLength(string $value, int $length, string $name): void
    {
        if (strlen($value) !== $length) {
            throw new InvalidArgumentException($name . ' must contain exactly ' . $length . ' bytes.');
        }
    }

    private function __construct()
    {
    }
}
