#!/opt/local/bin/ruby3.4

require 'optparse'
options = {}
VERSION = '1.6.1'

optparse = OptionParser.new do |opts|
  opts.banner = 'Usage: macchanger [options] device'

  opts.on('-v', '--version', 'Displays MacChanger version') do
    puts "Version: #{VERSION}"
    exit
  end

  opts.on('-m', '--mac MAC', 'Set the MAC address, macchanger -m XX:XX:XX:XX:XX:XX en0') do |m|
    options[:mac] = m.downcase
    puts m
  end

  opts.on('-r', '--random', 'Set random MAC address, macchanger -r en0') do |r|
    options[:random] = r
  end

  opts.on('-s', '--show', 'Show the MAC address, macchanger -s en0') do |s|
    options[:show] = s
  end

  opts.on('--enable-private', 'Enable private Wi-Fi addresses (macOS Sequoia+ feature)') do
    options[:enable_private] = true
  end

  opts.on('--disable-private', 'Disable private Wi-Fi addresses (use real MAC)') do
    options[:disable_private] = true
  end

  opts.on('--status', 'Show private Wi-Fi address status for all networks') do
    options[:status] = true
  end
end

class MacChanger
  def self.show(device)
    show = `/sbin/ifconfig #{device} |grep ether`
    show[7, 17] if show
  end

  def self.generate
    # least significant bit of most significant octet has to be 0 to to be unicast
    [format('%0.2x', rand(256) & ~1), (1..5).map { format('%0.2x', rand(256)) }].join(':')
  end

  def self.valid?(mac)
    unless mac.match(/^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$/)
      fail OptionParser::InvalidArgument, 'Mac address is not valid'
    end
  end

  def self.down?(device)
    unless `/sbin/ifconfig -d |grep -E '^#{device}:'`.empty?
      fail OptionParser::InvalidArgument, "Device #{device} is down"
    end
  end

  def self.get_cpu_architecture
    arch_output = `uname -m`.strip
    return arch_output
  end

  def self.arm_mac?
    arch = get_cpu_architecture
    return arch == "arm64"
  end

  def self.get_interface_type(device)
    # Check if it's Wi-Fi interface
    wifi_check = `networksetup -listallhardwareports | grep -A1 "Wi-Fi" | grep "#{device}"`
    return "Wi-Fi" unless wifi_check.empty?

    # Check if it's Ethernet
    ethernet_check = `networksetup -listallhardwareports | grep -A1 "Ethernet" | grep "#{device}"`
    return "Ethernet" unless ethernet_check.empty?

    return "Unknown"
  end



  def self.change_mac_arm_method(device, mac)
    interface_type = get_interface_type(device)

    if interface_type == "Wi-Fi"
      puts "INFO: Type of interface is #{interface_type}. Will disassociate from any network."

      # Turn off Wi-Fi, turn back on, change MAC, detect hardware
      system("sudo /usr/sbin/networksetup -setairportpower #{device} off")
      sleep 2
      system("sudo /usr/sbin/networksetup -setairportpower #{device} on")
      sleep 2

      puts "Setting MAC address to #{mac}..."
      success = system("sudo /sbin/ifconfig #{device} ether #{mac}")

      system("sudo /usr/sbin/networksetup -detectnewhardware")
      sleep 2

      if success
        new_mac = show(device)
        if new_mac && new_mac.strip == mac
          return true
        else
          puts "Failed to set MAC address. Make sure you're running with sudo privileges."
          return false
        end
      else
        puts "Failed to set MAC address. Make sure you're running with sudo privileges."
        return false
      end
    else
      # For Ethernet interfaces, try direct change
      puts "Setting MAC address to #{mac}..."
      success = system("sudo /sbin/ifconfig #{device} ether #{mac}")

      if success
        new_mac = show(device)
        if new_mac && new_mac.strip == mac
          return true
        else
          puts "Failed to set MAC address. Make sure you're running with sudo privileges."
          return false
        end
      else
        puts "Failed to set MAC address. Make sure you're running with sudo privileges."
        return false
      end
    end
  end

  def self.change_mac_intel_method(device, mac)
    interface_type = get_interface_type(device)
    puts "INFO: Type of interface is #{interface_type}. Will disassociate from any network."

    if interface_type == "Wi-Fi"
      puts "WARNING: The airport command line tool is deprecated and will be removed in a future release."
      puts "For diagnosing Wi-Fi related issues, use the Wireless Diagnostics app or wdutil command line tool."

      # Try the old airport method for Intel Macs
      system("sudo /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport #{device} -z")
      sleep 2
    end

    puts "Setting MAC address to #{mac}..."
    success = system("sudo /sbin/ifconfig #{device} ether #{mac}")

    if success
      new_mac = show(device)
      if new_mac && new_mac.strip == mac
        return true
      else
        puts "Failed to set MAC address. Make sure you're running with sudo privileges."
        return false
      end
    else
      puts "Failed to set MAC address. Make sure you're running with sudo privileges."
      return false
    end
  end

  def self.start(options)
    if options[:random]
      options[:mac] = generate
    end

    # Use different methods based on CPU architecture
    if arm_mac?
      success = change_mac_arm_method(options[:device], options[:mac])
    else
      success = change_mac_intel_method(options[:device], options[:mac])
    end

    unless success
      puts "ERROR: This device / interface has no MAC address to set."
      puts ""
      puts "TROUBLESHOOTING TIPS:"
      puts "1. Make sure you're running with sudo privileges"
      puts "2. Try running the command again"
      puts "3. Some MAC addresses may be rejected by the system"
    end
  end
end

begin
  optparse.parse!
  options[:device] = ARGV[0] or fail OptionParser::MissingArgument, 'device'
  MacChanger.down?(options[:device])

  if options[:show]
    puts "Your mac address is: #{MacChanger.show(options[:device])}"
  else
    fail OptionParser::InvalidOption, 'MAC address or random option' if options[:mac].nil? && options[:random].nil?
    MacChanger.valid?(options[:mac]) unless options[:random]
    MacChanger.start(options)
  end
rescue OptionParser::InvalidArgument, OptionParser::MissingArgument, OptionParser::InvalidOption => error
  puts error
  puts optparse
end
