# encoding: utf-8
#
# This file is part of the devdnsd gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
# Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
#

# A small DNS server to enable local .dev domain resolution.
module DevDNSd
  # Methods to handle interfaces aliases.
  module Aliases
    extend ActiveSupport::Concern

    # Manages aliases.
    #
    # @param operation [Symbol] The type of operation. Can be `:add` or `:remove`.
    # @param message [String] The message to show if no addresses are found.
    # @param options [Hash] The options provided by the user.
    # @return [Boolean] `true` if operation succeeded, `false` otherwise.
    def manage_aliases(operation, message, options)
      config = self.config
      options.each { |k, v| config.send("#{k}=", v) if config.respond_to?("#{k}=") }

      addresses = compute_addresses

      if addresses.present?
        # Now, for every address, call the command
        addresses.all? { |address| manage_address(operation, address, options[:dry_run]) }
      else
        @logger.error(message)
        false
      end
    end

    # Adds or removes an alias from the interface.
    #
    # @param type [Symbol] The operation to execute. Can be `:add` or `:remove`.
    # @param address [String] The address to manage.
    # @param dry_run [Boolean] If only show which modifications will be done.
    # @return [Boolean] `true` if operation succeeded, `false` otherwise.
    def manage_address(type, address, dry_run = false)
      rv, command, prefix = setup_management(type, address)

      # Now execute
      if rv
        if !dry_run
          execute_manage(command, prefix, type, address, config)
        else
          log_management(:dry_run, prefix, type, i18n.remove, i18n.add, address, config)
        end
      end

      rv
    end

    # Computes the list of address to manage.
    #
    # @param type [Symbol] The type of addresses to consider. Valid values are `:ipv4`, `:ipv6`, otherwise all addresses are considered.
    # @return [Array] The list of addresses to add or remove from the interface.
    def compute_addresses(type = :all)
      config = self.config
      config.addresses.present? ? filter_addresses(config, type) : generate_addresses(config, type)
    end

    # Checks if an address is a valid IPv4 address.
    #
    # @param address [String] The address to check.
    # @return [Boolean] `true` if the address is a valid IPv4 address, `false` otherwise.
    def ipv4?(address)
      address = address.ensure_string

      mo = /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\Z/.match(address)
      (mo && mo.captures.all? { |i| i.to_i < 256 }) ? true : false
    end
    alias_method :is_ipv4?, :ipv4?

    # Checks if an address is a valid IPv6 address.
    #
    # @param address [String] The address to check.
    # @return [Boolean] `true` if the address is a valid IPv6 address, `false` otherwise.
    def ipv6?(address)
      address = address.ensure_string

      catch(:valid) do
        # IPv6 (normal)
        check_normal_ipv6(address)
        # IPv6 (IPv4 compat)
        check_compat_ipv6(address)

        false
      end
    end

    private

    # :nodoc:
    def setup_management(type, address)
      @addresses ||= compute_addresses
      length = @addresses.length
      length_s = length.to_s.length
      progress = ((@addresses.index(address) || 0) + 1).indexize(length: length_s)
      message = "{mark=blue}[{mark=bright white}#{progress}{mark=reset blue}/{/mark}#{length}{/mark}]{/mark}"

      [true, build_command(type, address), message]
    rescue ArgumentError
      [false]
    end

    # :nodoc:
    def filter_addresses(config, type)
      filters = [:ipv4, :ipv6].select { |i| type == i || type == :all }.compact
      config.addresses.select { |address| filters.any? { |filter| send("#{filter}?", address) } }.compact.uniq
    end

    # :nodoc:
    def generate_addresses(config, type)
      ip = IPAddr.new(config.start_address.ensure_string)
      raise ArgumentError if type != :all && !ip.send("#{type}?")
      Array.new([config.aliases, 1].max) do |_|
        current = ip
        ip = ip.succ
        current
      end
    rescue ArgumentError
      []
    end

    # :nodoc:
    def build_command(type, address)
      template = config.send((type == :remove) ? :remove_command : :add_command)
      Mustache.render(template, {interface: config.interface, address: address.to_s}) + " > /dev/null 2>&1"
    end

    # :nodoc:
    def execute_manage(command, prefix, type, address, config)
      rv = execute_command(command)
      log_management(:run, prefix, type, i18n.removing, i18n.adding, address, config)
      log_management_error(config, address, manage_labels(type)) unless rv
      rv
    end

    # :nodoc:
    def manage_labels(type)
      type == :remove ? [i18n.remove, i18n.from] : [i18n.add, i18n.to]
    end

    # :nodoc:
    def log_management_error(config, address, labels)
      @logger.error(replace_markers(i18n.general_error(labels[0], address, labels[1], config.interface)))
    end

    # :nodoc:
    def log_management(message, prefix, type, remove_label, add_label, address, config)
      labels = (type == :remove ? [remove_label, i18n.from] : [add_label, i18n.to])
      @logger.info(replace_markers(i18n.send(message, prefix, labels[0], address, labels[1], config.interface)))
    end

    # :nodoc:
    def check_compat_ipv6(address)
      throw(:valid, true) if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:/ =~ address && ipv4?($')
      throw(:valid, true) if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:)?/ =~ address && ipv4?($')
      throw(:valid, true) if /\A::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:)?/ =~ address && ipv4?($')
    end

    # :nodoc:
    def check_normal_ipv6(address)
      throw(:valid, true) if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*\Z/ =~ address
      throw(:valid, true) if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*)?\Z/ =~ address
      throw(:valid, true) if /\A::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*)?\Z/ =~ address
    end
  end
end
