<?php

declare(strict_types=1);

namespace App\Framework\Cryptography;

use DateTimeImmutable;
use InvalidArgumentException;

/**
 * Secure Token Value Object
 *
 * Represents a cryptographically secure token with metadata about its
 * generation, format, and intended use.
 */
final readonly class SecureToken
{
    private DateTimeImmutable $createdAt;

    public function __construct(
        private string $value,
        private string $type,
        private string $format,
        private int $length,
        private ?string $prefix,
        private string $rawBytes,
        /** @var array<string, mixed> */
        private array $metadata
    ) {
        if (empty($value)) {
            throw new InvalidArgumentException('Token value cannot be empty');
        }

        if (empty($type)) {
            throw new InvalidArgumentException('Token type cannot be empty');
        }

        if (empty($format)) {
            throw new InvalidArgumentException('Token format cannot be empty');
        }

        if ($length < 1) {
            throw new InvalidArgumentException('Token length must be positive');
        }

        $this->createdAt = new DateTimeImmutable();
    }

    /**
     * Get the token value
     */
    public function getValue(): string
    {
        return $this->value;
    }

    /**
     * Get the token type
     */
    public function getType(): string
    {
        return $this->type;
    }

    /**
     * Get the token format
     */
    public function getFormat(): string
    {
        return $this->format;
    }

    /**
     * Get the token length
     */
    public function getLength(): int
    {
        return $this->length;
    }

    /**
     * Get the token prefix (if any)
     */
    public function getPrefix(): ?string
    {
        return $this->prefix;
    }

    /**
     * Get the raw bytes
     */
    public function getRawBytes(): string
    {
        return $this->rawBytes;
    }

    /**
     * Get metadata
     * @return array<string, mixed>
     */
    public function getMetadata(): array
    {
        return $this->metadata;
    }

    /**
     * Get creation timestamp
     */
    public function getCreatedAt(): DateTimeImmutable
    {
        return $this->createdAt;
    }

    /**
     * String representation
     */
    public function toString(): string
    {
        return $this->value;
    }

    /**
     * Magic method for string conversion
     */
    public function __toString(): string
    {
        return $this->value;
    }

    /**
     * Get token without prefix
     */
    public function getValueWithoutPrefix(): string
    {
        if ($this->prefix === null) {
            return $this->value;
        }

        $prefixLength = strlen($this->prefix) + 1; // +1 for underscore

        return substr($this->value, $prefixLength);
    }

    /**
     * Check if token has prefix
     */
    public function hasPrefix(): bool
    {
        return $this->prefix !== null;
    }

    /**
     * Get token length in characters (not bytes)
     */
    public function getValueLength(): int
    {
        return strlen($this->value);
    }

    /**
     * Get raw bytes as hexadecimal
     */
    public function getRawBytesHex(): string
    {
        return bin2hex($this->rawBytes);
    }

    /**
     * Get raw bytes as Base64
     */
    public function getRawBytesBase64(): string
    {
        return base64_encode($this->rawBytes);
    }

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

    /**
     * Verify token value (timing-safe)
     */
    public function verify(string $candidateToken): bool
    {
        return hash_equals($this->value, $candidateToken);
    }

    /**
     * Check if token is of specific type
     */
    public function isType(string $type): bool
    {
        return $this->type === $type;
    }

    /**
     * Check if token is in specific format
     */
    public function isFormat(string $format): bool
    {
        return $this->format === $format;
    }

    /**
     * Check if token is API key
     */
    public function isApiKey(): bool
    {
        return $this->type === SecureTokenGenerator::TYPE_API_KEY;
    }

    /**
     * Check if token is session token
     */
    public function isSessionToken(): bool
    {
        return $this->type === SecureTokenGenerator::TYPE_SESSION;
    }

    /**
     * Check if token is CSRF token
     */
    public function isCsrfToken(): bool
    {
        return $this->type === SecureTokenGenerator::TYPE_CSRF;
    }

    /**
     * Check if token is verification token
     */
    public function isVerificationToken(): bool
    {
        return $this->type === SecureTokenGenerator::TYPE_VERIFICATION;
    }

    /**
     * Check if token is Bearer token
     */
    public function isBearerToken(): bool
    {
        return $this->type === SecureTokenGenerator::TYPE_BEARER;
    }

    /**
     * Check if token is OTP
     */
    public function isOtp(): bool
    {
        return $this->type === SecureTokenGenerator::TYPE_OTP;
    }

    /**
     * Check if token is single-use
     */
    public function isSingleUse(): bool
    {
        return $this->metadata['single_use'] ?? false;
    }

    /**
     * Check if token is long-lived
     */
    public function isLongLived(): bool
    {
        return $this->metadata['long_lived'] ?? false;
    }

    /**
     * Get token age in seconds
     */
    public function getAgeInSeconds(): int
    {
        return time() - $this->createdAt->getTimestamp();
    }

    /**
     * Get specific metadata value
     */
    public function getMetadataValue(string $key, mixed $default = null): mixed
    {
        return $this->metadata[$key] ?? $default;
    }

    /**
     * Export to array (for serialization)
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        return [
            'value' => $this->value,
            'type' => $this->type,
            'format' => $this->format,
            'length' => $this->length,
            'prefix' => $this->prefix,
            'raw_bytes' => base64_encode($this->rawBytes),
            'metadata' => $this->metadata,
            'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
        ];
    }

    /**
     * Create from array (for deserialization)
     * @param array<string, mixed> $data
     */
    public static function fromArray(array $data): self
    {
        $requiredFields = ['value', 'type', 'format', 'length', 'raw_bytes'];

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

        $rawBytes = base64_decode($data['raw_bytes'], true);
        if ($rawBytes === false) {
            throw new InvalidArgumentException('Invalid Base64 raw bytes');
        }

        $token = new self(
            value: $data['value'],
            type: $data['type'],
            format: $data['format'],
            length: (int)$data['length'],
            prefix: $data['prefix'] ?? null,
            rawBytes: $rawBytes,
            metadata: $data['metadata'] ?? []
        );

        // Note: Cannot override readonly createdAt property after construction
        // The timestamp will be set to current time during construction

        return $token;
    }

    /**
     * Get safe summary (excludes sensitive token value)
     * @return array<string, mixed>
     */
    public function getSafeSummary(): array
    {
        return [
            'type' => $this->type,
            'format' => $this->format,
            'length' => $this->length,
            'prefix' => $this->prefix,
            'value_length' => $this->getValueLength(),
            'has_prefix' => $this->hasPrefix(),
            'is_single_use' => $this->isSingleUse(),
            'is_long_lived' => $this->isLongLived(),
            'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
            'age_seconds' => $this->getAgeInSeconds(),
            'metadata_keys' => array_keys($this->metadata),
        ];
    }

    /**
     * Get token fingerprint (for identification without exposing value)
     */
    public function getFingerprint(): string
    {
        return hash('sha256', $this->value . $this->type . $this->createdAt->format('U.u'));
    }

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

    /**
     * Mask token value for logging
     */
    public function getMaskedValue(): string
    {
        if (strlen($this->value) <= 8) {
            return '***';
        }

        $start = substr($this->value, 0, 4);
        $end = substr($this->value, -4);
        $middle = str_repeat('*', max(3, strlen($this->value) - 8));

        return $start . $middle . $end;
    }
}
