require 'hosts'
require 'logger'
require 'open-uri'
require 'fileutils'

class HostsUpdater

  SOURCES = {
    :mdl => ['http://www.malwaredomainlist.com/hostslist/hosts.txt', 'Malware Domain List'],
    :mvps => ['http://winhelp2002.mvps.org/hosts.txt', 'MVPS Hosts'],
    :swc => ['http://someonewhocares.org/hosts/hosts', "Dan Pollock's List"],
    :yoyo => ['http://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext', "Peter Lowe's Ad Server List"]
  }

  DEFAULT_OPTIONS = {
    :clear => false,
    :hosts_file => '/etc/hosts',
    :hosts_directory => '/etc/hosts.d/',
    :hosts_auto_name => 'hosts.auto',
    :hosts_custom_name => 'hosts.custom',
    :hosts_whitelist_name => 'hosts.whitelist',
    :ip => '0.0.0.0',
    :sources => SOURCES,
    :update => false,
    :quiet => false,
    :verbose => false
  }

  def initialize(options = {})
    sources = options.delete(:sources) || {}
    @options = DEFAULT_OPTIONS.merge(options)
    @options[:sources].merge!(sources)
  end

  # Create required files, update auto file if necessary and generate hosts file.
  # Need to be ran as root in order to access /etc/hosts
  def run
    unless Process.uid == 0
      logger.error 'Error: Must run as root'
      exit
    end

    bootstrap
    update_auto_file if @options[:update]
    update_hosts_file

    logger.info 'Done.'
  end

  private

  def logger
    return @logger if defined?(@logger)
    @logger = Logger.new(STDOUT)
    @logger.level = if @options[:quiet]
                      Logger::ERROR
                    elsif @options[:verbose]
                      Logger::DEBUG
                    else
                      Logger::INFO
                    end
    @logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
    @logger
  end

  # Create /etc/hosts.d and files inside.
  # If /etc/hosts.d/hosts.custom does not exists then it will copy content of /etc/hosts there.
  def bootstrap
    unless Dir.exists? @options[:hosts_directory]
      logger.debug "Creating configuration directory at #{@options[:hosts_directory]}"
      FileUtils.mkdir_p @options[:hosts_directory]
    end

    unless File.exists? hosts_custom_location
      logger.debug "Copying #{@options[:hosts_file]} to #{hosts_custom_location}"
      File.write(hosts_custom_location, File.read(@options[:hosts_file]))
    end

    unless File.exists? hosts_auto_location
      logger.debug "Writing default #{hosts_auto_location}"
      FileUtils.touch(hosts_auto_location)
    end

    unless File.exists? hosts_whitelist_location
      logger.debug "Writing default #{hosts_whitelist_location}"
      File.write(hosts_whitelist_location, "# List domains that should be ignored here.\n# One domain per line, lines starting with # will be ignored.\n")
    end
  end

  # Download files mentioned in SOURCES and combine them into one saved as hosts_auto_location.
  # Any comments and duplicates are ommited from original files.
  def update_auto_file
    elements = []
    @options[:sources].each do |source_key, source_data|
      next unless source_data
      elements += download_source(source_data)
    end
    hosts = Hosts::File.new
    hosts.elements = elements.uniq(&:name).sort { |e1,e2| e1.name <=> e2.name }
    hosts.elements.insert 0, Hosts::Comment.new(' This file was auto-generated by hosts-updater.')
    hosts.elements.insert 1, Hosts::Comment.new(' Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.')
    hosts.elements.insert 2, Hosts::EmptyElement.new

    File.write(hosts_auto_location, hosts.to_s(:force_generation => true))
  end

  # Combine hosts.custom and hosts.auto files into one and write to hosts_location.
  # Domains listed in hosts.whitelist are ommited from hosts.auto list, but not from hosts.custom
  def update_hosts_file
    hosts = Hosts::File.new(@options[:hosts_file])
    hosts.elements = Hosts::File.read(hosts_custom_location).elements

    unless @options[:clear]
      auto = Hosts::File.read(hosts_auto_location).elements
      auto.reject! { |el| ! el.is_a? Aef::Hosts::Entry }
      auto.reject! { |el| whitelist.include? el.name }
      auto.each do |el|
        el.address = @options[:ip]
      end
      hosts.elements << Hosts::EmptyElement.new
      hosts.elements << Hosts::Section.new('HOSTS-UPDATER', :elements => auto)
    end

    logger.debug "Writing to #{@options[:hosts_file]}"
    hosts.write
  end

  def download_source(source)
    logger.debug "Downloading source #{source[0]}" + (source[1] ? " (#{source[1]})" : "")
    data = open(source[0])
    Hosts::File.parse(data.read).elements.select { |el| el.is_a? Aef::Hosts::Entry }
  end

  # Read hosts.whitelist file and ignore all lines starting with #
  def whitelist
    @whitelist ||= File.read(hosts_whitelist_location).lines.collect(&:strip).reject { |el| /^#/ =~ el }
  end

  def hosts_auto_location
    @options[:hosts_directory] + @options[:hosts_auto_name]
  end

  def hosts_custom_location
    @options[:hosts_directory] + @options[:hosts_custom_name]
  end

  def hosts_whitelist_location
    @options[:hosts_directory] + @options[:hosts_whitelist_name]
  end

end
