# frozen_string_literal: true
#
# Author:: Benjamin Black (<bb@chef.io>)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

Ohai.plugin(:Network) do
  require_relative "../../mixin/network_helper"

  provides "network", "network/interfaces"
  provides "counters/network", "counters/network/interfaces"

  include Ohai::Mixin::NetworkHelper

  def parse_media(media_string)
    media = {}
    line_array = media_string.split

    0.upto(line_array.length - 1) do |i|
      unless line_array[i].eql?("none")

        if line_array[i + 1] =~ /^\<([a-zA-Z\-\,]+)\>$/
          media[line_array[i]] = {} unless media.key?(line_array[i])
          if media[line_array[i]].key?("options")
            $1.split(",").each do |opt|
              media[line_array[i]]["options"] << opt unless media[line_array[i]]["options"].include?(opt)
            end
          else
            media[line_array[i]]["options"] = $1.split(",")
          end
        elsif line_array[i].eql?("autoselect")
          media["autoselect"] = {} unless media.key?("autoselect")
          media["autoselect"]["options"] = []
        end
      else
        media["none"] = { "options" => [] }
      end
    end

    media
  end

  def darwin_encaps_lookup(ifname)
    return "Loopback" if ifname.eql?("lo")
    return "1394" if ifname.eql?("fw")
    return "IPIP" if ifname.eql?("gif")
    return "6to4" if ifname.eql?("stf")
    return "dot1q" if ifname.eql?("vlan")

    "Unknown"
  end

  def scope_lookup(scope)
    return "Node" if scope.eql?("::1")
    return "Link" if /^fe80\:/.match?(scope)
    return "Site" if /^fec0\:/.match?(scope)

    "Global"
  end

  def excluded_setting?(setting)
    setting.match("_sw_cksum")
  end

  def locate_interface(ifaces, ifname, mac)
    return ifname unless ifaces[ifname].nil?
    # oh well, time to go hunting!
    return ifname.chop if /\*$/.match?(ifname)

    ifaces.each_key do |ifc|
      ifaces[ifc][:addresses].each_key do |addr|
        return ifc if addr.eql? mac
      end
    end

    nil
  end

  collect_data(:darwin) do
    network Mash.new unless network
    network[:interfaces] ||= Mash.new
    counters Mash.new unless counters
    counters[:network] ||= Mash.new

    so = shell_out("route -n get default")
    so.stdout.lines do |line|
      if line =~ /(\w+): ([\w\.]+)/
        case $1
        when "gateway"
          network[:default_gateway] = $2
        when "interface"
          network[:default_interface] = $2
        end
      end
    end

    iface = Mash.new
    so = shell_out("ifconfig -a")
    cint = nil
    so.stdout.lines do |line|
      if line =~ /^([0-9a-zA-Z\.\:\-]+): \S+ mtu (\d+)$/
        cint = $1
        iface[cint] ||= Mash.new
        iface[cint][:addresses] ||= Mash.new
        iface[cint][:mtu] = $2
        if line =~ /\sflags\=\d+\<((UP|BROADCAST|DEBUG|SMART|SIMPLEX|LOOPBACK|POINTOPOINT|NOTRAILERS|RUNNING|NOARP|PROMISC|ALLMULTI|SLAVE|MASTER|MULTICAST|DYNAMIC|,)+)\>\s/
          flags = $1.split(",")
        else
          flags = []
        end
        iface[cint][:flags] = flags.flatten
        if cint =~ /^(\w+)(\d+.*)/
          iface[cint][:type] = $1
          iface[cint][:number] = $2
          iface[cint][:encapsulation] = darwin_encaps_lookup($1)
        end
      end
      if line =~ /^\s+ether ([0-9a-f\:]+)/
        iface[cint][:addresses] ||= Mash.new
        iface[cint][:addresses][$1] = { "family" => "lladdr" }
        iface[cint][:encapsulation] = "Ethernet"
      end
      if line =~ /^\s+lladdr ([0-9a-f\:]+)\s/
        iface[cint][:addresses] ||= Mash.new
        iface[cint][:addresses][$1] = { "family" => "lladdr" }
      end
      if line =~ /\s+inet (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) netmask 0x(([0-9a-f]){1,8})\s*$/
        iface[cint][:addresses] ||= Mash.new
        iface[cint][:addresses][$1] = { "family" => "inet", "netmask" => hex_to_dec_netmask($2) }
      end
      if line =~ /\s+inet (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) netmask 0x(([0-9a-f]){1,8}) broadcast (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/
        iface[cint][:addresses] ||= Mash.new
        iface[cint][:addresses][$1] = { "family" => "inet", "netmask" => hex_to_dec_netmask($2), "broadcast" => $4 }
      end
      if line =~ /\s+inet6 ([a-f0-9\:]+)(\s*|(\%[a-z0-9]+)\s*) prefixlen (\d+)\s*/
        iface[cint][:addresses] ||= Mash.new
        iface[cint][:addresses][$1] = { "family" => "inet6", "prefixlen" => $4, "scope" => scope_lookup($1) }
      end
      if line =~ /\s+inet6 ([a-f0-9\:]+)(\s*|(\%[a-z0-9]+)\s*) prefixlen (\d+) scopeid 0x([a-f0-9]+)/
        iface[cint][:addresses] ||= Mash.new
        iface[cint][:addresses][$1] = { "family" => "inet6", "prefixlen" => $4, "scope" => scope_lookup($1) }
      end
      if line =~ /^\s+media: ((\w+)|(\w+ [a-zA-Z0-9\-\<\>]+)) status: (\w+)/
        iface[cint][:media] ||= Mash.new
        iface[cint][:media][:selected] = parse_media($1)
        iface[cint][:status] = $4
      end
      if line =~ /^\s+supported media: (.*)/
        iface[cint][:media] ||= Mash.new
        iface[cint][:media][:supported] = parse_media($1)
      end
    end

    so = shell_out("arp -an")
    so.stdout.lines do |line|
      if line =~ /^\S+ \((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\) at ([a-fA-F0-9\:]+) on ([a-zA-Z0-9\.\:\-]+).*\[(\w+)\]/
        # MAC addr really should be normalized to include all the zeroes.
        next if iface[$3].nil? # this should never happen

        iface[$3][:arp] ||= Mash.new
        iface[$3][:arp][$1] = $2
      end
    end

    settings = Mash.new
    so = shell_out("sysctl net")
    so.stdout.lines do |line|
      if line =~ /^([a-zA-Z0-9\.\_]+)\: (.*)/
        # should normalize names between platforms for the same settings.
        settings[$1] = $2 unless excluded_setting?($1)
      end
    end

    network[:settings] = settings
    network[:interfaces] = iface

    net_counters = Mash.new
    so = shell_out("netstat -i -d -l -b -n")
    so.stdout.lines do |line|
      if line =~ /^([a-zA-Z0-9\.\:\-\*]+)\s+\d+\s+\<[a-zA-Z0-9\#]+\>\s+([a-f0-9\:]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/ ||
          line =~ /^([a-zA-Z0-9\.\:\-\*]+)\s+\d+\s+\<[a-zA-Z0-9\#]+\>(\s+)(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/
        ifname = locate_interface(iface, $1, $2)
        next if iface[ifname].nil? # this shouldn't happen, but just in case

        net_counters[ifname] ||= Mash.new
        net_counters[ifname] = { rx: { bytes: $5, packets: $3, errors: $4, drop: 0, overrun: 0, frame: 0, compressed: 0, multicast: 0 },
                                 tx: { bytes: $8, packets: $6, errors: $7, drop: 0, overrun: 0, collisions: $9, carrier: 0, compressed: 0 },
        }
      end
    end

    counters[:network][:interfaces] = net_counters
  end
end
