<?php

declare(strict_types=1);

namespace App\Framework\Cryptography;

use InvalidArgumentException;

/**
 * Public Key Value Object
 *
 * Represents a cryptographic public key with metadata.
 */
final readonly class PublicKey
{
    public function __construct(
        private string $keyMaterial,
        private string $algorithm,
        private int $keySize,
        private ?string $curve = null
    ) {
        if (empty($keyMaterial)) {
            throw new InvalidArgumentException('Key material cannot be empty');
        }

        if (empty($algorithm)) {
            throw new InvalidArgumentException('Algorithm cannot be empty');
        }

        $supportedAlgorithms = ['rsa', 'ecdsa'];
        if (! in_array($algorithm, $supportedAlgorithms, true)) {
            throw new InvalidArgumentException('Unsupported algorithm');
        }

        if ($keySize < 256) {
            throw new InvalidArgumentException('Key size must be at least 256 bits');
        }

        if ($algorithm === 'ecdsa' && $curve === null) {
            throw new InvalidArgumentException('ECDSA keys must specify a curve');
        }
    }

    /**
     * Get the key material (PEM format)
     */
    public function getKeyMaterial(): string
    {
        return $this->keyMaterial;
    }

    /**
     * Get the algorithm
     */
    public function getAlgorithm(): string
    {
        return $this->algorithm;
    }

    /**
     * Get the key size in bits
     */
    public function getKeySize(): int
    {
        return $this->keySize;
    }

    /**
     * Get the curve name (ECDSA only)
     */
    public function getCurve(): ?string
    {
        return $this->curve;
    }

    /**
     * Check if this is an RSA key
     */
    public function isRsa(): bool
    {
        return $this->algorithm === 'rsa';
    }

    /**
     * Check if this is an ECDSA key
     */
    public function isEcdsa(): bool
    {
        return $this->algorithm === 'ecdsa';
    }

    /**
     * Export key to array (includes key material - safe for public keys)
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        $data = [
            'key_material' => $this->keyMaterial,
            'algorithm' => $this->algorithm,
            'key_size' => $this->keySize,
        ];

        if ($this->curve !== null) {
            $data['curve'] = $this->curve;
        }

        return $data;
    }

    /**
     * Create from array
     * @param array<string, mixed> $data
     */
    public static function fromArray(array $data): self
    {
        $requiredFields = ['key_material', 'algorithm', 'key_size'];

        foreach ($requiredFields as $field) {
            if (! isset($data[$field])) {
                throw new InvalidArgumentException("Missing required field: {$field}");
            }
        }

        return new self(
            keyMaterial: $data['key_material'],
            algorithm: $data['algorithm'],
            keySize: (int)$data['key_size'],
            curve: $data['curve'] ?? null
        );
    }

    /**
     * Get key fingerprint (SHA-256 hash)
     */
    public function getFingerprint(): string
    {
        return hash('sha256', $this->keyMaterial);
    }

    /**
     * Get short fingerprint (first 16 chars of full fingerprint)
     */
    public function getShortFingerprint(): string
    {
        return substr($this->getFingerprint(), 0, 16);
    }

    /**
     * Get key in DER format (binary)
     */
    public function getDerFormat(): string
    {
        $resource = openssl_pkey_get_public($this->keyMaterial);
        if ($resource === false) {
            throw new InvalidArgumentException('Invalid public key');
        }

        $details = openssl_pkey_get_details($resource);
        if ($details === false || ! isset($details['key'])) {
            throw new InvalidArgumentException('Failed to get key details');
        }

        // Convert PEM to DER
        $pem = $details['key'];
        $pem = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----'], '', $pem);
        $pem = str_replace(["\r", "\n", " "], '', $pem);

        $der = base64_decode($pem);
        if ($der === false) {
            throw new InvalidArgumentException('Failed to convert to DER format');
        }

        return $der;
    }

    /**
     * Get key type description
     */
    public function getDescription(): string
    {
        $description = strtoupper($this->algorithm);

        if ($this->algorithm === 'rsa') {
            $description .= " {$this->keySize}-bit";
        } elseif ($this->algorithm === 'ecdsa') {
            $description .= " {$this->curve}";
        }

        return $description;
    }

    /**
     * Check equality with another public key
     */
    public function equals(self $other): bool
    {
        return hash_equals($this->keyMaterial, $other->keyMaterial);
    }

    /**
     * Validate key material
     */
    public function isValid(): bool
    {
        $resource = openssl_pkey_get_public($this->keyMaterial);

        return $resource !== false;
    }

    /**
     * Get key as JWK (JSON Web Key) format
     * @return array<string, mixed>
     */
    public function toJwk(): array
    {
        if (! $this->isValid()) {
            throw new InvalidArgumentException('Invalid key material');
        }

        $resource = openssl_pkey_get_public($this->keyMaterial);
        $details = openssl_pkey_get_details($resource);

        if ($this->algorithm === 'rsa') {
            return [
                'kty' => 'RSA',
                'alg' => 'RS256',
                'use' => 'sig',
                'n' => $this->base64UrlEncode($details['rsa']['n']),
                'e' => $this->base64UrlEncode($details['rsa']['e']),
            ];
        }

        if ($this->algorithm === 'ecdsa') {
            $crv = match ($this->curve) {
                'prime256v1' => 'P-256',
                'secp384r1' => 'P-384',
                'secp521r1' => 'P-521',
                default => throw new InvalidArgumentException('Unsupported curve for JWK')
            };

            return [
                'kty' => 'EC',
                'alg' => 'ES256',
                'use' => 'sig',
                'crv' => $crv,
                'x' => $this->base64UrlEncode($details['ec']['x']),
                'y' => $this->base64UrlEncode($details['ec']['y']),
            ];
        }

        throw new InvalidArgumentException('Unsupported algorithm for JWK conversion');
    }

    /**
     * Base64 URL encode (for JWK format)
     */
    private function base64UrlEncode(string $data): string
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }
}
