<?php

declare(strict_types=1);

namespace App\Framework\Core\ValueObjects;

/**
 * Client Identifier Value Object
 *
 * Represents a unique identifier for a client making requests.
 * Used for rate limiting, security tracking, audit logging, etc.
 *
 * Supports multiple identifier types:
 * - IP Address (most common)
 * - Session ID (for authenticated sessions)
 * - User ID (for logged-in users)
 * - API Key (for API clients)
 * - Custom (application-specific identifiers)
 *
 * Usage:
 * ```php
 * $identifier = ClientIdentifier::fromIp('192.168.1.1');
 * $identifier = ClientIdentifier::fromSession('sess_abc123');
 * $identifier = ClientIdentifier::fromUserId('user_123');
 * ```
 */
final readonly class ClientIdentifier
{
    private function __construct(
        public string $value,
        public ClientIdentifierType $type
    ) {
        if (empty($this->value)) {
            throw new \InvalidArgumentException('Client identifier value cannot be empty');
        }
    }

    /**
     * Create from IP address
     */
    public static function fromIp(string $ipAddress): self
    {
        if (! filter_var($ipAddress, FILTER_VALIDATE_IP)) {
            throw new \InvalidArgumentException("Invalid IP address: {$ipAddress}");
        }

        return new self($ipAddress, ClientIdentifierType::IP_ADDRESS);
    }

    /**
     * Create from session ID
     */
    public static function fromSession(string $sessionId): self
    {
        return new self($sessionId, ClientIdentifierType::SESSION_ID);
    }

    /**
     * Create from user ID
     */
    public static function fromUserId(string $userId): self
    {
        return new self($userId, ClientIdentifierType::USER_ID);
    }

    /**
     * Create from API key
     */
    public static function fromApiKey(string $apiKey): self
    {
        return new self($apiKey, ClientIdentifierType::API_KEY);
    }

    /**
     * Create custom identifier
     */
    public static function custom(string $value, string $customType = 'custom'): self
    {
        return new self($value, ClientIdentifierType::CUSTOM);
    }

    /**
     * Get string representation for hashing/storage
     */
    public function toString(): string
    {
        return $this->value;
    }

    /**
     * Get prefixed string with type information
     *
     * Format: "type:value"
     * Example: "ip:192.168.1.1", "session:sess_abc123"
     */
    public function toPrefixedString(): string
    {
        return $this->type->value . ':' . $this->value;
    }

    /**
     * Check if identifier is IP-based
     */
    public function isIpBased(): bool
    {
        return $this->type === ClientIdentifierType::IP_ADDRESS;
    }

    /**
     * Check if identifier is session-based
     */
    public function isSessionBased(): bool
    {
        return $this->type === ClientIdentifierType::SESSION_ID;
    }

    /**
     * Check if identifier is user-based
     */
    public function isUserBased(): bool
    {
        return $this->type === ClientIdentifierType::USER_ID;
    }

    /**
     * Equality comparison
     */
    public function equals(self $other): bool
    {
        return $this->value === $other->value
            && $this->type === $other->type;
    }

    /**
     * Get masked version for logging (privacy-safe)
     *
     * IP: 192.168.1.xxx
     * Session: sess_abc***
     * User: user_***
     */
    public function toMasked(): string
    {
        return match ($this->type) {
            ClientIdentifierType::IP_ADDRESS => $this->maskIpAddress(),
            ClientIdentifierType::SESSION_ID => $this->maskGeneric(8),
            ClientIdentifierType::USER_ID => $this->maskGeneric(5),
            ClientIdentifierType::API_KEY => $this->maskGeneric(4),
            ClientIdentifierType::CUSTOM => $this->maskGeneric(4)
        };
    }

    /**
     * Mask IP address (keep first 3 octets for IPv4, first 4 groups for IPv6)
     */
    private function maskIpAddress(): string
    {
        // IPv4
        if (str_contains($this->value, '.')) {
            $parts = explode('.', $this->value);
            if (count($parts) === 4) {
                return "{$parts[0]}.{$parts[1]}.{$parts[2]}.xxx";
            }
        }

        // IPv6
        if (str_contains($this->value, ':')) {
            $parts = explode(':', $this->value);
            if (count($parts) >= 4) {
                return implode(':', array_slice($parts, 0, 4)) . ':xxxx';
            }
        }

        return $this->maskGeneric(8);
    }

    /**
     * Generic masking: show first N characters, mask rest
     */
    private function maskGeneric(int $visibleChars = 4): string
    {
        $length = strlen($this->value);

        if ($length <= $visibleChars) {
            return str_repeat('*', $length);
        }

        $visible = substr($this->value, 0, $visibleChars);
        $masked = str_repeat('*', min(3, $length - $visibleChars));

        return $visible . $masked;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}
