<?php

declare(strict_types=1);

namespace App\Framework\Cuid;

use DateTimeImmutable;
use InvalidArgumentException;

/**
 * Cuid Value Object
 *
 * Collision-resistant Unique Identifier
 * - 25 characters total (c + timestamp + counter + fingerprint + random)
 * - Base36 encoded (0-9, a-z) - case insensitive
 * - Optimized for horizontal scaling and collision resistance
 * - Always starts with 'c' for collision-resistant
 */
final readonly class Cuid
{
    public const int LENGTH = 25;
    public const string PREFIX = 'c';

    // Base36 alphabet: 0-9, a-z (lowercase only)
    public const string ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';

    // Component lengths
    public const int TIMESTAMP_LENGTH = 8;
    public const int COUNTER_LENGTH = 4;
    public const int FINGERPRINT_LENGTH = 4;
    public const int RANDOM_LENGTH = 8; // PREFIX(1) + TIMESTAMP(8) + COUNTER(4) + FINGERPRINT(4) + RANDOM(8) = 25

    private string $value;

    private int $timestamp;

    private int $counter;

    private string $fingerprint;

    private string $random;

    public function __construct(string $value)
    {
        if (empty($value)) {
            throw new InvalidArgumentException('Cuid cannot be empty');
        }

        if (strlen($value) !== self::LENGTH) {
            throw new InvalidArgumentException('Cuid must be exactly ' . self::LENGTH . ' characters long');
        }

        if (! str_starts_with($value, self::PREFIX)) {
            throw new InvalidArgumentException('Cuid must start with "c"');
        }

        if (! $this->isValidBase36($value)) {
            throw new InvalidArgumentException('Cuid contains invalid characters');
        }

        $this->value = $value;
        $this->parseComponents();
    }

    /**
     * Create from components
     */
    public static function fromComponents(
        int $timestamp,
        int $counter,
        string $fingerprint,
        string $random
    ): self {
        if (strlen($fingerprint) !== self::FINGERPRINT_LENGTH) {
            throw new InvalidArgumentException('Fingerprint must be exactly ' . self::FINGERPRINT_LENGTH . ' characters');
        }

        if (strlen($random) !== self::RANDOM_LENGTH) {
            throw new InvalidArgumentException('Random part must be exactly ' . self::RANDOM_LENGTH . ' characters');
        }

        // Convert components to Base36
        $timestampBase36 = self::toBase36($timestamp, self::TIMESTAMP_LENGTH);
        $counterBase36 = self::toBase36($counter, self::COUNTER_LENGTH);

        $value = self::PREFIX . $timestampBase36 . $counterBase36 . $fingerprint . $random;

        return new self($value);
    }

    /**
     * Create from string
     */
    public static function fromString(string $value): self
    {
        return new self($value);
    }

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

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

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

    /**
     * Get the timestamp component (Unix timestamp in milliseconds)
     */
    public function getTimestamp(): int
    {
        return $this->timestamp;
    }

    /**
     * Get the timestamp as DateTime
     */
    public function getDateTime(): DateTimeImmutable
    {
        // Convert milliseconds to seconds
        $seconds = intval($this->timestamp / 1000);
        $microseconds = ($this->timestamp % 1000) * 1000;

        return DateTimeImmutable::createFromFormat('U.u', sprintf('%d.%06d', $seconds, $microseconds));
    }

    /**
     * Get the counter component
     */
    public function getCounter(): int
    {
        return $this->counter;
    }

    /**
     * Get the fingerprint component
     */
    public function getFingerprint(): string
    {
        return $this->fingerprint;
    }

    /**
     * Get the random component
     */
    public function getRandom(): string
    {
        return $this->random;
    }

    /**
     * Check equality with another Cuid
     */
    public function equals(self $other): bool
    {
        return $this->value === $other->value;
    }

    /**
     * Compare with another Cuid for sorting
     */
    public function compare(self $other): int
    {
        return strcmp($this->value, $other->value);
    }

    /**
     * Check if this Cuid is older than another
     */
    public function isOlderThan(self $other): bool
    {
        return $this->timestamp < $other->timestamp;
    }

    /**
     * Check if this Cuid is newer than another
     */
    public function isNewerThan(self $other): bool
    {
        return $this->timestamp > $other->timestamp;
    }

    /**
     * Get age in milliseconds
     */
    public function getAgeInMilliseconds(): int
    {
        return intval(microtime(true) * 1000) - $this->timestamp;
    }

    /**
     * Get age in seconds
     */
    public function getAgeInSeconds(): float
    {
        return $this->getAgeInMilliseconds() / 1000.0;
    }

    /**
     * Check if Cuid is from the same process/machine (same fingerprint)
     */
    public function isSameProcess(self $other): bool
    {
        return $this->fingerprint === $other->fingerprint;
    }

    /**
     * Validate Base36 characters (lowercase only)
     */
    private function isValidBase36(string $value): bool
    {
        $pattern = '/^[' . preg_quote(self::ALPHABET, '/') . ']+$/';

        return preg_match($pattern, $value) === 1;
    }

    /**
     * Parse components from the Cuid value
     */
    private function parseComponents(): void
    {
        $offset = 1; // Skip the 'c' prefix

        // Extract timestamp (8 chars)
        $timestampBase36 = substr($this->value, $offset, self::TIMESTAMP_LENGTH);
        $this->timestamp = self::fromBase36($timestampBase36);
        $offset += self::TIMESTAMP_LENGTH;

        // Extract counter (4 chars)
        $counterBase36 = substr($this->value, $offset, self::COUNTER_LENGTH);
        $this->counter = self::fromBase36($counterBase36);
        $offset += self::COUNTER_LENGTH;

        // Extract fingerprint (4 chars)
        $this->fingerprint = substr($this->value, $offset, self::FINGERPRINT_LENGTH);
        $offset += self::FINGERPRINT_LENGTH;

        // Extract random part (8 chars)
        $this->random = substr($this->value, $offset, self::RANDOM_LENGTH);
    }

    /**
     * Convert integer to Base36 with padding
     */
    private static function toBase36(int $value, int $length): string
    {
        $base36 = base_convert((string)$value, 10, 36);

        return str_pad($base36, $length, '0', STR_PAD_LEFT);
    }

    /**
     * Convert Base36 string to integer
     */
    private static function fromBase36(string $base36): int
    {
        return intval(base_convert($base36, 36, 10));
    }
}
