<?php

declare(strict_types=1);

namespace App\Framework\Cryptography;

use App\Framework\Random\RandomGenerator;
use InvalidArgumentException;

/**
 * Secure Token Generator Service
 *
 * Generates cryptographically secure tokens for various use cases including
 * API keys, session tokens, CSRF tokens, and verification tokens.
 */
final readonly class SecureTokenGenerator
{
    // Token type constants
    public const string TYPE_API_KEY = 'api_key';
    public const string TYPE_SESSION = 'session';
    public const string TYPE_CSRF = 'csrf';
    public const string TYPE_VERIFICATION = 'verification';
    public const string TYPE_BEARER = 'bearer';
    public const string TYPE_REFRESH = 'refresh';
    public const string TYPE_OTP = 'otp';
    public const string TYPE_WEBHOOK = 'webhook';

    // Encoding formats
    public const string FORMAT_BASE64 = 'base64';
    public const string FORMAT_BASE64_URL = 'base64url';
    public const string FORMAT_HEX = 'hex';
    public const string FORMAT_BASE32 = 'base32';
    public const string FORMAT_ALPHANUMERIC = 'alphanumeric';

    public function __construct(
        private RandomGenerator $randomGenerator
    ) {
    }

    /**
     * Generate a secure token
     * @param array<string, mixed>|null $metadata
     */
    public function generate(
        string $type,
        int $length = 32,
        string $format = self::FORMAT_BASE64_URL,
        ?string $prefix = null,
        ?array $metadata = null
    ): SecureToken {
        if ($length < 16) {
            throw new InvalidArgumentException('Token length must be at least 16 bytes');
        }

        if ($length > 256) {
            throw new InvalidArgumentException('Token length cannot exceed 256 bytes');
        }

        $supportedTypes = [
            self::TYPE_API_KEY, self::TYPE_SESSION, self::TYPE_CSRF,
            self::TYPE_VERIFICATION, self::TYPE_BEARER, self::TYPE_REFRESH,
            self::TYPE_OTP, self::TYPE_WEBHOOK,
        ];

        if (! in_array($type, $supportedTypes, true)) {
            throw new InvalidArgumentException('Unsupported token type');
        }

        $supportedFormats = [
            self::FORMAT_BASE64, self::FORMAT_BASE64_URL, self::FORMAT_HEX,
            self::FORMAT_BASE32, self::FORMAT_ALPHANUMERIC,
        ];

        if (! in_array($format, $supportedFormats, true)) {
            throw new InvalidArgumentException('Unsupported format');
        }

        // Generate random bytes
        $randomBytes = $this->randomGenerator->bytes($length);

        // Encode in specified format
        $encodedToken = $this->encodeToken($randomBytes, $format);

        // Add prefix if specified
        if ($prefix !== null) {
            $encodedToken = $prefix . '_' . $encodedToken;
        }

        return new SecureToken(
            value: $encodedToken,
            type: $type,
            format: $format,
            length: $length,
            prefix: $prefix,
            rawBytes: $randomBytes,
            metadata: $metadata ?? []
        );
    }

    /**
     * Generate API key with standard format
     */
    public function generateApiKey(string $prefix = 'ak', int $length = 32): SecureToken
    {
        return $this->generate(
            type: self::TYPE_API_KEY,
            length: $length,
            format: self::FORMAT_BASE64_URL,
            prefix: $prefix,
            metadata: ['purpose' => 'api_authentication']
        );
    }

    /**
     * Generate session token
     */
    public function generateSessionToken(int $length = 32): SecureToken
    {
        return $this->generate(
            type: self::TYPE_SESSION,
            length: $length,
            format: self::FORMAT_BASE64_URL,
            metadata: ['purpose' => 'session_management', 'secure' => true]
        );
    }

    /**
     * Generate CSRF token
     */
    public function generateCsrfToken(int $length = 32): SecureToken
    {
        return $this->generate(
            type: self::TYPE_CSRF,
            length: $length,
            format: self::FORMAT_BASE64_URL,
            metadata: ['purpose' => 'csrf_protection']
        );
    }

    /**
     * Generate verification token (email, phone, etc.)
     */
    public function generateVerificationToken(string $purpose, int $length = 32): SecureToken
    {
        return $this->generate(
            type: self::TYPE_VERIFICATION,
            length: $length,
            format: self::FORMAT_BASE64_URL,
            metadata: ['purpose' => $purpose, 'single_use' => true]
        );
    }

    /**
     * Generate Bearer token for OAuth/JWT-like usage
     */
    public function generateBearerToken(int $length = 32): SecureToken
    {
        return $this->generate(
            type: self::TYPE_BEARER,
            length: $length,
            format: self::FORMAT_BASE64_URL,
            metadata: ['purpose' => 'api_authorization']
        );
    }

    /**
     * Generate refresh token
     */
    public function generateRefreshToken(int $length = 64): SecureToken
    {
        return $this->generate(
            type: self::TYPE_REFRESH,
            length: $length,
            format: self::FORMAT_BASE64_URL,
            metadata: ['purpose' => 'token_refresh', 'long_lived' => true]
        );
    }

    /**
     * Generate OTP (One-Time Password) token
     */
    public function generateOtpToken(int $digits = 6): SecureToken
    {
        if ($digits < 4 || $digits > 12) {
            throw new InvalidArgumentException('OTP digits must be between 4 and 12');
        }

        // Generate numeric OTP
        $max = (int)str_repeat('9', $digits);
        $min = (int)str_pad('1', $digits, '0', STR_PAD_RIGHT);

        $otpValue = (string)$this->randomGenerator->int($min, $max);

        // Pad with leading zeros if necessary
        $otpValue = str_pad($otpValue, $digits, '0', STR_PAD_LEFT);

        return new SecureToken(
            value: $otpValue,
            type: self::TYPE_OTP,
            format: 'numeric',
            length: $digits,
            prefix: null,
            rawBytes: $otpValue, // For OTP, raw bytes is the numeric string
            metadata: ['digits' => $digits, 'purpose' => 'otp', 'single_use' => true]
        );
    }

    /**
     * Generate webhook signature token
     */
    public function generateWebhookToken(string $prefix = 'whsec', int $length = 32): SecureToken
    {
        return $this->generate(
            type: self::TYPE_WEBHOOK,
            length: $length,
            format: self::FORMAT_BASE64_URL,
            prefix: $prefix,
            metadata: ['purpose' => 'webhook_signature']
        );
    }

    /**
     * Generate token with custom alphabet
     */
    public function generateCustom(
        string $alphabet,
        int $length,
        string $type = 'custom'
    ): SecureToken {
        if (strlen($alphabet) < 16) {
            throw new InvalidArgumentException('Alphabet must contain at least 16 characters');
        }

        if ($length < 8) {
            throw new InvalidArgumentException('Token length must be at least 8 characters');
        }

        // Remove duplicate characters from alphabet
        $alphabet = implode('', array_unique(str_split($alphabet)));
        $alphabetSize = strlen($alphabet);

        $token = '';
        for ($i = 0; $i < $length; $i++) {
            $index = $this->randomGenerator->int(0, $alphabetSize - 1);
            $token .= $alphabet[$index];
        }

        return new SecureToken(
            value: $token,
            type: $type,
            format: 'custom',
            length: $length,
            prefix: null,
            rawBytes: $token, // For custom format, store the token string
            metadata: ['alphabet' => $alphabet, 'alphabet_size' => $alphabetSize]
        );
    }

    /**
     * Generate batch of tokens
     * @return array<int, SecureToken>
     */
    public function generateBatch(
        string $type,
        int $count,
        int $length = 32,
        string $format = self::FORMAT_BASE64_URL,
        ?string $prefix = null
    ): array {
        if ($count <= 0) {
            throw new InvalidArgumentException('Count must be positive');
        }

        if ($count > 1000) {
            throw new InvalidArgumentException('Batch size cannot exceed 1000');
        }

        $tokens = [];
        for ($i = 0; $i < $count; $i++) {
            $tokens[] = $this->generate($type, $length, $format, $prefix);
        }

        return $tokens;
    }

    /**
     * Validate token format
     */
    public function isValidFormat(string $token, string $format): bool
    {
        return match ($format) {
            self::FORMAT_BASE64 => $this->isValidBase64($token),
            self::FORMAT_BASE64_URL => $this->isValidBase64Url($token),
            self::FORMAT_HEX => $this->isValidHex($token),
            self::FORMAT_BASE32 => $this->isValidBase32($token),
            self::FORMAT_ALPHANUMERIC => $this->isValidAlphanumeric($token),
            default => false
        };
    }

    /**
     * Estimate token entropy in bits
     */
    public function getEntropy(SecureToken $token): float
    {
        return match ($token->getFormat()) {
            self::FORMAT_BASE64 => $token->getLength() * log(64, 2),
            self::FORMAT_BASE64_URL => $token->getLength() * log(64, 2),
            self::FORMAT_HEX => $token->getLength() * log(16, 2),
            self::FORMAT_BASE32 => $token->getLength() * log(32, 2),
            self::FORMAT_ALPHANUMERIC => $token->getLength() * log(62, 2),
            'numeric' => $token->getLength() * log(10, 2),
            'custom' => $token->getLength() * log($token->getMetadata()['alphabet_size'] ?? 64, 2),
            default => 0.0
        };
    }

    /**
     * Encode raw bytes to specified format
     */
    private function encodeToken(string $bytes, string $format): string
    {
        return match ($format) {
            self::FORMAT_BASE64 => base64_encode($bytes),
            self::FORMAT_BASE64_URL => rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='),
            self::FORMAT_HEX => bin2hex($bytes),
            self::FORMAT_BASE32 => $this->base32Encode($bytes),
            self::FORMAT_ALPHANUMERIC => $this->alphanumericEncode($bytes),
            default => throw new InvalidArgumentException("Unsupported format: {$format}")
        };
    }

    /**
     * Base32 encoding
     */
    private function base32Encode(string $data): string
    {
        $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        $encoded = '';
        $bits = 0;
        $value = 0;

        foreach (str_split($data) as $char) {
            $value = ($value << 8) | ord($char);
            $bits += 8;

            while ($bits >= 5) {
                $encoded .= $alphabet[($value >> ($bits - 5)) & 31];
                $bits -= 5;
            }
        }

        if ($bits > 0) {
            $encoded .= $alphabet[($value << (5 - $bits)) & 31];
        }

        return $encoded;
    }

    /**
     * Alphanumeric encoding (0-9, A-Z, a-z)
     */
    private function alphanumericEncode(string $bytes): string
    {
        $alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
        $encoded = '';

        foreach (str_split($bytes) as $byte) {
            $value = ord($byte);
            $encoded .= $alphabet[$value % 62];
        }

        return $encoded;
    }

    /**
     * Validation helpers
     */
    private function isValidBase64(string $token): bool
    {
        return preg_match('/^[A-Za-z0-9+\/]+=*$/', $token) === 1;
    }

    private function isValidBase64Url(string $token): bool
    {
        return preg_match('/^[A-Za-z0-9\-_]*$/', $token) === 1;
    }

    private function isValidHex(string $token): bool
    {
        return preg_match('/^[0-9a-fA-F]+$/', $token) === 1;
    }

    private function isValidBase32(string $token): bool
    {
        return preg_match('/^[A-Z2-7]+=*$/', $token) === 1;
    }

    private function isValidAlphanumeric(string $token): bool
    {
        return preg_match('/^[0-9A-Za-z]+$/', $token) === 1;
    }

    /**
     * Factory method
     */
    public static function create(RandomGenerator $randomGenerator): self
    {
        return new self($randomGenerator);
    }
}
