<?php

declare(strict_types=1);

namespace App\Framework\Totp;

use App\Framework\Core\Encoding\Base32Alphabet;
use App\Framework\Core\Encoding\Base32Encoder;
use App\Framework\Random\RandomGenerator;
use InvalidArgumentException;
use SensitiveParameter;

/**
 * TOTP Secret Value Object
 *
 * Immutable value object representing a TOTP secret key with validation,
 * encoding support, and security features for Time-based One-Time Passwords.
 */
final readonly class TotpSecret
{
    private const int MIN_SECRET_LENGTH = 10;  // 80 bits minimum (RFC 4226)
    private const int RECOMMENDED_LENGTH = 20; // 160 bits recommended
    private const int MAX_SECRET_LENGTH = 128; // Practical maximum

    private function __construct(
        private string $binarySecret
    ) {
        $this->validate($binarySecret);
    }

    /**
     * Create TOTP secret from binary data
     */
    public static function fromBinary(#[SensitiveParameter] string $binaryData): self
    {
        return new self($binaryData);
    }

    /**
     * Create TOTP secret from Base32 encoded string
     */
    public static function fromBase32(#[SensitiveParameter] string $base32Secret): self
    {
        if (empty($base32Secret)) {
            throw new InvalidArgumentException('TOTP secret cannot be empty');
        }

        if (! Base32Alphabet::RFC3548->isValidEncoded($base32Secret)) {
            throw new InvalidArgumentException('Invalid Base32 TOTP secret format');
        }

        $binaryData = Base32Encoder::decode($base32Secret);

        return new self($binaryData);
    }

    /**
     * Generate a cryptographically secure random TOTP secret
     */
    public static function generate(
        RandomGenerator $randomGenerator,
        int $length = self::RECOMMENDED_LENGTH
    ): self {
        if ($length < self::MIN_SECRET_LENGTH) {
            throw new InvalidArgumentException(
                sprintf('TOTP secret must be at least %d bytes', self::MIN_SECRET_LENGTH)
            );
        }

        if ($length > self::MAX_SECRET_LENGTH) {
            throw new InvalidArgumentException(
                sprintf('TOTP secret cannot exceed %d bytes', self::MAX_SECRET_LENGTH)
            );
        }

        $binarySecret = $randomGenerator->bytes($length);

        return new self($binarySecret);
    }

    /**
     * Get the binary representation of the secret
     */
    public function getBinary(): string
    {
        return $this->binarySecret;
    }

    /**
     * Get the Base32 encoded representation of the secret
     */
    public function toBase32(): string
    {
        return Base32Encoder::encode($this->binarySecret);
    }

    /**
     * Get the Base32 encoded secret formatted for display
     */
    public function toFormattedBase32(int $groupSize = 4): string
    {
        $base32 = $this->toBase32();

        return Base32Encoder::formatForDisplay($base32, $groupSize);
    }

    /**
     * Get the length of the secret in bytes
     */
    public function getLength(): int
    {
        return strlen($this->binarySecret);
    }

    /**
     * Get the length of the secret in bits
     */
    public function getBitLength(): int
    {
        return $this->getLength() * 8;
    }

    /**
     * Check if this secret meets security requirements
     */
    public function isSecure(): bool
    {
        return $this->getLength() >= self::RECOMMENDED_LENGTH;
    }

    /**
     * Get security level assessment
     */
    public function getSecurityLevel(): TotpSecurityLevel
    {
        $length = $this->getLength();

        return match (true) {
            $length >= 32 => TotpSecurityLevel::HIGH,      // 256+ bits
            $length >= 20 => TotpSecurityLevel::STANDARD,  // 160+ bits (recommended)
            $length >= 16 => TotpSecurityLevel::MEDIUM,    // 128+ bits
            $length >= 10 => TotpSecurityLevel::LOW,       // 80+ bits (minimum)
            default => TotpSecurityLevel::INSUFFICIENT
        };
    }

    /**
     * Generate QR code data URI for authenticator apps
     */
    public function toQrCodeUri(
        string $accountName,
        string $issuer,
        int $digits = 6,
        int $period = 30,
        string $algorithm = 'SHA1'
    ): string {
        $params = [
            'secret' => $this->toBase32(),
            'issuer' => $issuer,
            'algorithm' => strtoupper($algorithm),
            'digits' => $digits,
            'period' => $period,
        ];

        $queryString = http_build_query($params);
        $label = urlencode($issuer . ':' . $accountName);

        return sprintf('otpauth://totp/%s?%s', $label, $queryString);
    }

    /**
     * Derive a new secret using HKDF for key rotation
     */
    public function deriveNew(RandomGenerator $randomGenerator, string $info = 'totp-rotation'): self
    {
        // Use HKDF to derive a new secret from the current one
        $salt = $randomGenerator->bytes(16);
        $derivedKey = hash_hkdf('sha256', $this->binarySecret, $this->getLength(), $info, $salt);

        return new self($derivedKey);
    }

    /**
     * Check if two secrets are equal (timing-safe comparison)
     */
    public function equals(TotpSecret $other): bool
    {
        return hash_equals($this->binarySecret, $other->binarySecret);
    }

    /**
     * Get entropy assessment
     */
    public function getEntropy(): float
    {
        return $this->getBitLength();
    }

    /**
     * Check if secret has sufficient entropy for production use
     */
    public function hasSufficientEntropy(): bool
    {
        return $this->getEntropy() >= 160; // 160 bits recommended
    }

    /**
     * Get metadata about the secret
     */
    public function getMetadata(): array
    {
        return [
            'length_bytes' => $this->getLength(),
            'length_bits' => $this->getBitLength(),
            'entropy' => $this->getEntropy(),
            'security_level' => $this->getSecurityLevel()->value,
            'is_secure' => $this->isSecure(),
            'sufficient_entropy' => $this->hasSufficientEntropy(),
            'base32_length' => strlen($this->toBase32()),
        ];
    }

    /**
     * Create a masked representation for logging
     */
    public function toMaskedString(): string
    {
        $base32 = $this->toBase32();
        $visibleLength = min(4, strlen($base32));

        return substr($base32, 0, $visibleLength) . str_repeat('*', strlen($base32) - $visibleLength);
    }

    /**
     * Validate the binary secret
     */
    private function validate(string $binarySecret): void
    {
        if (empty($binarySecret)) {
            throw new InvalidArgumentException('TOTP secret cannot be empty');
        }

        $length = strlen($binarySecret);

        if ($length < self::MIN_SECRET_LENGTH) {
            throw new InvalidArgumentException(
                sprintf('TOTP secret must be at least %d bytes, got %d', self::MIN_SECRET_LENGTH, $length)
            );
        }

        if ($length > self::MAX_SECRET_LENGTH) {
            throw new InvalidArgumentException(
                sprintf('TOTP secret cannot exceed %d bytes, got %d', self::MAX_SECRET_LENGTH, $length)
            );
        }
    }

    /**
     * Prevent serialization of sensitive data
     */
    public function __serialize(): array
    {
        throw new \RuntimeException('TotpSecret cannot be serialized');
    }

    /**
     * Prevent unserialization of sensitive data
     */
    public function __unserialize(array $data): void
    {
        throw new \RuntimeException('TotpSecret cannot be unserialized');
    }
}
