<?php

declare(strict_types=1);

namespace Drupal\native_observability\Service;

/**
 * ULID-based Request ID generator.
 *
 * ULID advantages over UUIDv4:
 * - Lexicographically sortable (useful for listings).
 * - Still globally unique with extremely low collision probability.
 *
 * This implementation uses a lightweight ULID generator without dependencies.
 * The format is a 26-char Crockford Base32 string.
 */
final class UlidRequestIdGenerator implements RequestIdGeneratorInterface {

  /**
   * {@inheritdoc}
   */
  public function generate(): string {
    // ULID = 48-bit timestamp (ms) + 80-bit randomness.
    // We generate a monotonic-enough ULID for general request usage.
    $time_ms = (int) floor(microtime(TRUE) * 1000);
    $time = $this->encodeTime($time_ms);
    // 80 bits => 16 chars in Base32.
    $rand = $this->encodeRandom(16);
    return $time . $rand;
  }

  /**
   * Encodes the 48-bit timestamp part into 10 Base32 chars.
   *
   * @param int $time_ms
   *   Timestamp in milliseconds.
   *
   * @return string
   *   10-character Base32 time component.
   */
  private function encodeTime(int $time_ms): string {
    $alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
    $out = '';
    // 48 bits => 10 Base32 chars (50 bits, top bits are zeroed).
    for ($i = 0; $i < 10; $i++) {
      $index = $time_ms % 32;
      $out = $alphabet[$index] . $out;
      $time_ms = intdiv($time_ms, 32);
    }
    return $out;
  }

  /**
   * Encodes cryptographically secure random bytes into Base32 chars.
   *
   * @param int $chars
   *   Number of output Base32 chars required.
   *
   * @return string
   *   Base32-encoded random component.
   */
  private function encodeRandom(int $chars): string {
    $alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
    $out = '';

    // Each Base32 char = 5 bits.
    $bits_needed = $chars * 5;
    $bytes_needed = (int) ceil($bits_needed / 8);

    $bytes = random_bytes($bytes_needed);

    $buffer = 0;
    $buffer_bits = 0;
    $pos = 0;

    while (strlen($out) < $chars) {
      if ($buffer_bits < 5) {
        if ($pos >= strlen($bytes)) {
          // Extremely unlikely, but keep safe.
          $bytes .= random_bytes(1);
        }
        $buffer = ($buffer << 8) | ord($bytes[$pos]);
        $buffer_bits += 8;
        $pos++;
        continue;
      }

      $shift = $buffer_bits - 5;
      $index = ($buffer >> $shift) & 0x1F;
      $buffer_bits -= 5;

      $out .= $alphabet[$index];
    }

    return $out;
  }

}
