<?php

declare(strict_types=1);

namespace App\Framework\Cuid;

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

/**
 * Cuid Generator Service
 *
 * Generates Collision-resistant Unique Identifiers with machine fingerprinting.
 */
final class CuidGenerator
{
    private int $counter = 0;

    private string $fingerprint;

    public function __construct(
        private readonly RandomGenerator $randomGenerator,
        ?string $customFingerprint = null
    ) {
        $this->fingerprint = $customFingerprint ?? $this->generateFingerprint();
        $this->counter = $this->randomGenerator->int(0, 36 ** Cuid::COUNTER_LENGTH - 1);
    }

    /**
     * Generate a new Cuid
     */
    public function generate(): Cuid
    {
        $timestamp = $this->getCurrentTimestamp();
        $counter = $this->getNextCounter();
        $random = $this->generateRandomPart();

        return Cuid::fromComponents($timestamp, $counter, $this->fingerprint, $random);
    }

    /**
     * Generate a Cuid at a specific timestamp (milliseconds)
     */
    public function generateAt(int $timestampMs): Cuid
    {
        $counter = $this->getNextCounter();
        $random = $this->generateRandomPart();

        return Cuid::fromComponents($timestampMs, $counter, $this->fingerprint, $random);
    }

    /**
     * Generate a Cuid in the past (useful for testing)
     */
    public function generateInPast(int $millisecondsAgo): Cuid
    {
        $timestamp = $this->getCurrentTimestamp() - $millisecondsAgo;

        return $this->generateAt($timestamp);
    }

    /**
     * Generate a batch of Cuids with incrementing counters
     * @return array<int, Cuid>
     */
    public function generateBatch(int $count): array
    {
        if ($count <= 0) {
            throw new InvalidArgumentException('Count must be positive');
        }

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

        $timestamp = $this->getCurrentTimestamp();
        $cuids = [];

        for ($i = 0; $i < $count; $i++) {
            $counter = $this->getNextCounter();
            $random = $this->generateRandomPart();

            $cuids[] = Cuid::fromComponents($timestamp, $counter, $this->fingerprint, $random);
        }

        return $cuids;
    }

    /**
     * Generate a sequence of Cuids with incrementing timestamps
     * @return array<int, Cuid>
     */
    public function generateSequence(int $count, int $intervalMs = 1): array
    {
        if ($count <= 0) {
            throw new InvalidArgumentException('Count must be positive');
        }

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

        if ($intervalMs < 0) {
            throw new InvalidArgumentException('Interval must be non-negative');
        }

        $cuids = [];
        $timestamp = $this->getCurrentTimestamp();

        for ($i = 0; $i < $count; $i++) {
            $currentTimestamp = $timestamp + ($i * $intervalMs);
            $cuids[] = $this->generateAt($currentTimestamp);
        }

        return $cuids;
    }

    /**
     * Generate a Cuid with a specific counter value (for testing)
     */
    public function generateWithCounter(int $counter): Cuid
    {
        if ($counter < 0 || $counter >= 36 ** Cuid::COUNTER_LENGTH) {
            throw new InvalidArgumentException('Counter must be between 0 and ' . (36 ** Cuid::COUNTER_LENGTH - 1));
        }

        $timestamp = $this->getCurrentTimestamp();
        $random = $this->generateRandomPart();

        return Cuid::fromComponents($timestamp, $counter, $this->fingerprint, $random);
    }

    /**
     * Parse a Cuid string and validate it
     */
    public function parse(string $cuidString): Cuid
    {
        return Cuid::fromString($cuidString);
    }

    /**
     * Validate if a string is a valid Cuid
     */
    public function isValid(string $value): bool
    {
        if (strlen($value) !== Cuid::LENGTH) {
            return false;
        }

        if (! str_starts_with($value, Cuid::PREFIX)) {
            return false;
        }

        try {
            Cuid::fromString($value);

            return true;
        } catch (InvalidArgumentException) {
            return false;
        }
    }

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

    /**
     * Get the current counter value
     */
    public function getCurrentCounter(): int
    {
        return $this->counter;
    }

    /**
     * Reset the counter (useful for testing)
     */
    public function resetCounter(): void
    {
        $this->counter = 0;
    }

    /**
     * Check if two Cuids could have been generated by the same generator
     */
    public function isSameGenerator(Cuid $cuid): bool
    {
        return $cuid->getFingerprint() === $this->fingerprint;
    }

    /**
     * Get current timestamp in milliseconds
     */
    private function getCurrentTimestamp(): int
    {
        return intval(microtime(true) * 1000);
    }

    /**
     * Get next counter value with wraparound
     */
    private function getNextCounter(): int
    {
        $current = $this->counter;
        $this->counter = ($this->counter + 1) % (36 ** Cuid::COUNTER_LENGTH);

        return $current;
    }

    /**
     * Generate random part (8 chars Base36)
     */
    private function generateRandomPart(): string
    {
        $random = '';

        for ($i = 0; $i < Cuid::RANDOM_LENGTH; $i++) {
            $randomIndex = $this->randomGenerator->int(0, 35);
            $random .= Cuid::ALPHABET[$randomIndex];
        }

        return $random;
    }

    /**
     * Generate machine fingerprint based on system characteristics
     */
    private function generateFingerprint(): string
    {
        // Create a unique fingerprint based on system characteristics
        $data = [
            php_uname('n'), // hostname
            php_uname('s'), // OS
            php_uname('r'), // release
            php_uname('m'), // machine type
            getmypid(),     // process ID
            $_SERVER['SERVER_ADDR'] ?? 'localhost',
            $_SERVER['SERVER_PORT'] ?? '80',
        ];

        $hash = hash('sha256', implode('|', $data));

        // Convert first 16 chars of hash to Base36
        $fingerprint = '';
        for ($i = 0; $i < Cuid::FINGERPRINT_LENGTH; $i++) {
            $hexPair = substr($hash, $i * 2, 2);
            $decimal = hexdec($hexPair);
            $base36Char = base_convert((string)($decimal % 36), 10, 36);
            $fingerprint .= $base36Char;
        }

        return $fingerprint;
    }

    /**
     * Create factory method
     */
    public static function create(RandomGenerator $randomGenerator, ?string $customFingerprint = null): self
    {
        return new self($randomGenerator, $customFingerprint);
    }

    /**
     * Create with a deterministic fingerprint (for testing)
     */
    public static function createWithFingerprint(RandomGenerator $randomGenerator, string $fingerprint): self
    {
        if (strlen($fingerprint) !== Cuid::FINGERPRINT_LENGTH) {
            throw new InvalidArgumentException('Fingerprint must be exactly ' . Cuid::FINGERPRINT_LENGTH . ' characters');
        }

        return new self($randomGenerator, $fingerprint);
    }
}
