#!/usr/bin/env ruby
# frozen_string_literal: true
#
# uuid256.rb — reference implementation of README.md (UUID256, random layout) in Ruby (2.6+, stdlib only).
#
#   ruby uuid256.rb                    # self-tests + exact duplicate check over 10,000,000 ids
#   ruby uuid256.rb -n 2000000 -i 3    # 2e6 ids with 3 planted duplicates (proves detection)
#   ruby uuid256.rb -g [count]         # just print one (or count) new ids, nothing else
#
#   require_relative 'uuid256'
#   id  = Uuid256.generate            # 32-byte binary String, §5.1: SecureRandom, ver=4 / var=10 applied
#   txt = Uuid256.to_canonical(id)    # §3.1: "0001020304050607-08090a0b-4c0d0e0f-90111213-1415161718191a1b1c1d1e1f"
#   b   = Uuid256.parse(txt)          # §6: strict; Uuid256.parse(txt, strict: false) is lenient; raises Uuid256::ParseError
#
# Bulk check: all randomness from SecureRandom.random_bytes (§5.2 — OS CSPRNG) in one call, §4 applied to
# every id, then the 32-byte ids are sorted and neighbours compared.  Exact, O(n log n), ~100 B of RAM per id.

require 'securerandom'

module Uuid256
  BYTES     = 32
  TEXT_LEN  = 68                                  # 64 hex + 4 hyphens
  VERSION   = 0x4
  HYPHENS   = [16, 25, 34, 43].freeze             # 0-based offsets in canonical text
  STRICT_RE = /\A[0-9a-f]{16}-[0-9a-f]{8}-4[0-9a-f]{7}-[89ab][0-9a-f]{7}-[0-9a-f]{24}\z/.freeze
  HEX64_RE  = /\A[0-9a-f]{64}\z/.freeze
  NIL = ("\x00" * BYTES).b.freeze                 # §8
  MAX = ("\xff" * BYTES).b.freeze

  class ParseError < ArgumentError; end

  module_function

  # §4 — force version nibble (byte 12) and variant bits (byte 16). Mutates and returns the (binary) string.
  def set_ver_var!(b)
    b.setbyte(12, (b.getbyte(12) & 0x0F) | (VERSION << 4))   # hex digit 24 = '4'
    b.setbyte(16, (b.getbyte(16) & 0x3F) | 0x80)             # hex digit 32 in [89ab]
    b
  end

  # §5.1 — one id straight from the OS CSPRNG (32-byte binary String, big-endian: byte 0 most significant).
  def generate
    set_ver_var!(SecureRandom.random_bytes(BYTES))
  end

  # §6 — ver == 4 && var == 10.
  def strict?(b)
    b.bytesize == BYTES && (b.getbyte(12) >> 4) == VERSION && (b.getbyte(16) >> 6) == 0b10
  end

  # §3.1 — canonical text: 16-8-8-8-24, lowercase, 68 chars.
  def to_canonical(b)
    raise ArgumentError, 'UUID256 is exactly 32 bytes' unless b.bytesize == BYTES
    h = b.unpack1('H*')                                        # 64 lowercase hex digits
    "#{h[0, 16]}-#{h[16, 8]}-#{h[24, 8]}-#{h[32, 8]}-#{h[40, 24]}"
  end

  # §6 — parse canonical (68) or compact (64) form, any case. Raises ParseError.
  def parse(s, strict: true)
    s = s.b.downcase(:ascii)                                   # bytewise + ASCII-only: String#downcase raises on invalid UTF-8
    if s.length == TEXT_LEN
      raise ParseError, 'hyphen at wrong position' unless HYPHENS.all? { |i| s[i] == '-' }
      s = s.delete('-')
    elsif s.length != 64
      raise ParseError, 'wrong length'
    end
    raise ParseError, 'invalid character' unless HEX64_RE.match?(s)
    b = [s].pack('H*')
    raise ParseError, 'unsupported version/variant' if strict && !strict?(b)
    b
  end
end

# ---------------------------------------------------------------------------
# Self-tests (spec §11 vectors, §3.2/§6 parser rules, live generation)
# ---------------------------------------------------------------------------
def self_test
  fails = 0
  vectors = [
    ['000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', '0001020304050607-08090a0b-4c0d0e0f-90111213-1415161718191a1b1c1d1e1f'],
    ['fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0', 'fffefdfcfbfaf9f8-f7f6f5f4-43f2f1f0-afeeedec-ebeae9e8e7e6e5e4e3e2e1e0'],
  ]
  rejects = lambda do |s, strict = true|
    Uuid256.parse(s, strict: strict); false
  rescue Uuid256::ParseError
    true
  end
  vectors.each_with_index do |(raw, expected), i|
    b = Uuid256.set_ver_var!([raw].pack('H*'))
    txt = Uuid256.to_canonical(b)
    ok = txt == expected && Uuid256.strict?(b) && Uuid256.parse(txt) == b && Uuid256.parse(txt.upcase) == b &&
         Uuid256::STRICT_RE.match?(txt) && !rejects.call(raw, false) && rejects.call(raw)   # raw compact: lenient ok, strict rejects
    puts "  spec §11 vector #{i + 1}: #{ok ? 'PASS' : 'FAIL'}  #{txt}"
    fails += 1 unless ok
  end
  nil_txt = Uuid256.to_canonical(Uuid256::NIL)
  ok = rejects.call('0001020304050607_08090a0b-4c0d0e0f-90111213-1415161718191a1b1c1d1e1f') &&
       rejects.call('0001020304050607-08090a0b-4c0d0e0f-90111213-1415161718191a1b1c1d1e1') &&
       rejects.call('0001020304050607-08090a0b-4c0d0e0f-90111213-1415161718191a1b1c1d1e1g') &&
       rejects.call(nil_txt) && !rejects.call(nil_txt, false) && Uuid256.parse(nil_txt, strict: false) == Uuid256::NIL &&
       Uuid256.to_canonical(Uuid256::MAX) == "#{'f' * 16}-#{'f' * 8}-#{'f' * 8}-#{'f' * 8}-#{'f' * 24}"
  puts "  parser rules (§3.2/§6):  #{ok ? 'PASS' : 'FAIL'}"
  fails += 1 unless ok
  3.times do
    b = Uuid256.generate
    txt = Uuid256.to_canonical(b)
    ok = Uuid256.strict?(b) && Uuid256::STRICT_RE.match?(txt) && Uuid256.parse(txt) == b
    puts "  generate: #{txt}  #{ok ? 'ok' : 'BAD'}"
    fails += 1 unless ok
  end
  fails
end

# ---------------------------------------------------------------------------
# Bulk exact duplicate check
# ---------------------------------------------------------------------------
def bulk_generate(n)
  buf = SecureRandom.random_bytes(n * Uuid256::BYTES)          # §5.2, one call
  p = 0
  while p < buf.bytesize                                       # §4 on every id
    buf.setbyte(p + 12, (buf.getbyte(p + 12) & 0x0F) | (Uuid256::VERSION << 4))
    buf.setbyte(p + 16, (buf.getbyte(p + 16) & 0x3F) | 0x80)
    p += Uuid256::BYTES
  end
  buf
end

def find_duplicates(buf, n)
  ids = Array.new(n) { |i| buf.byteslice(i * Uuid256::BYTES, Uuid256::BYTES) }
  order = (0...n).sort_by { |i| ids[i] }                       # exact: sort 32-byte binary strings
  dups = []
  order.each_cons(2) do |a, b|
    next unless ids[a] == ids[b]
    i, j = a < b ? [a, b] : [b, a]
    dups << [i, j, ids[a]]
  end
  dups
end

def commas(v)
  v.to_s.reverse.scan(/\d{1,3}/).join(',').reverse
end

def main(argv)
  n = 10_000_000
  planted = 0
  args = argv.dup
  if args.size >= 2 && %w[-p -P].include?(args[0])                    # parse mode: -p strict, -P lenient
    begin
      puts "ok #{Uuid256.parse(args[1], strict: args[0] == '-p').unpack1('H*')}"; return 0
    rescue Uuid256::ParseError => e
      puts "error #{{ 'wrong length' => 'length', 'hyphen at wrong position' => 'hyphen', 'invalid character' => 'char', 'unsupported version/variant' => 'version' }[e.message]}"; return 1
    end
  end
  until args.empty?
    a = args.shift
    case a
    when '-g'                                                  # just print ids: -g [count]
      count = args.first.to_s =~ /\A\d+\z/ ? args.shift.to_i : 1               # -g N: exactly N (0 allowed); junk → 1
      count.times { puts Uuid256.to_canonical(Uuid256.generate) }
      return 0
    when '-n' then n = args.shift.to_i
    when '-i' then planted = args.shift.to_i
    else
      puts 'usage: ruby uuid256.rb [-g [count]] [-n count] [-i planted_dups]'; return 2
    end
  end
  if n < 2 || planted < 0 || planted > n / 2
    puts '-n must be >= 2 and -i at most n/2'; return 2
  end

  puts "UUID256 reference implementation (Ruby) — README.md (256-bit random, 16-8-8-8-24 text)\n\n"
  puts 'Self-tests:'
  if self_test > 0
    puts '  self-test FAILED — aborting'; return 1
  end

  k = 200_000
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  k.times { Uuid256.generate }
  per_call = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) / k
  puts format("\ngenerate per-call cost: %.2f µs → %.2f M ids/s single-threaded (1e9 ids would take ~%.0f min just to generate)",
              per_call * 1e6, 1 / per_call / 1e6, 1e9 * per_call / 60)

  puts "\nBulk exact duplicate check: n=#{commas(n)} ids, planted duplicates=#{planted}"
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  buf = bulk_generate(n)
  t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  # planted duplicates: id[dst] := id[src] — sources evenly spaced over the lower half, each mirrored
  # into the upper half, so all 2K positions are distinct and no source is itself a target (K <= n/2).
  plant_step = planted.positive? ? (n / 2) / planted : 0
  planted.times do |kk|
    src = plant_step * kk
    dst = n - 1 - src
    buf[dst * Uuid256::BYTES, Uuid256::BYTES] = buf.byteslice(src * Uuid256::BYTES, Uuid256::BYTES)
    puts "  planted: id[#{commas(dst)}] := id[#{commas(src)}]"
  end
  bad = 0
  p = 0
  while p < buf.bytesize
    bad += 1 if (buf.getbyte(p + 12) >> 4) != Uuid256::VERSION || (buf.getbyte(p + 16) >> 6) != 0b10
    p += Uuid256::BYTES
  end
  dups = find_duplicates(buf, n)
  t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  dups.each { |i, j, b| puts "  DUPLICATE  id[#{commas(i)}] == id[#{commas(j)}]  #{Uuid256.to_canonical(b)}" }
  step = [1, n / 1000].max
  rt_ok = (0...n).step(step).count { |i| id = buf.byteslice(i * Uuid256::BYTES, Uuid256::BYTES); Uuid256.parse(Uuid256.to_canonical(id)) == id }

  puts "\n==== RESULT ===="
  puts format('ids generated:                   %s   (SecureRandom + §4 in %.1fs; sort+scan in %.1fs)', commas(n), t1 - t0, t2 - t1)
  puts "version/variant violations:      #{commas(bad)}"
  puts "text round-trips (sampled):      #{commas(rt_ok)} ok"
  puts "FULL 256-bit DUPLICATES:         #{commas(dups.size)}" + (planted > 0 ? "   (planted: #{planted} — #{dups.size == planted ? 'all detected' : 'COUNT MISMATCH'})" : '')
  puts "  → no duplicates among #{commas(n)} ids" if dups.empty?
  log2p = 2 * Math.log2(n) - 251
  puts format('expected P(any collision) §5.3:  n²/2²⁵¹ ≈ 2^%.1f ≈ %.1e', log2p, 2**log2p)
  dups.size == planted ? 0 : 1
end

exit(main(ARGV)) if $PROGRAM_NAME == __FILE__
