# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2

# Copyright 2013-present Facebook
#
# 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.

require 'json'
require 'fileutils'
require 'digest/md5'
require 'between_meals/util'

module BetweenMeals
  # Knife does not have a usable API for using it as a lib
  # This could be possibly refactored to touch its internals
  # instead of shelling out
  class Knife
    include BetweenMeals::Util

    attr_accessor :cookbook_dirs

    def initialize(opts = {})
      @logger = opts[:logger] || nil
      @user = opts[:user] || ENV['USER']
      @home = opts[:home] || ENV['HOME']
      # make sure people can pass in false :)
      @ssl = opts[:ssl].nil? ? true : opts[:ssl]
      @host = opts[:host] || 'localhost'
      @port = opts[:port] || 4000
      @config = opts[:config] ||
                "#{@home}/.chef/knife-#{@user}-taste-tester.rb"
      @knife = opts[:bin] || 'knife'
      @knife_verb_option = ''
      unless @logger.nil?
        if @logger.level == Logger::DEBUG
          @knife_verb_option = '-VV'
        elsif @logger.level == Logger::INFO
          @knife_verb_option = '-V'
        end
      end
      @berks = opts[:berks_bin] || 'berks'
      @berks_config = opts[:berks_config]
      @pem = opts[:pem] ||
             "#{@home}/.chef/#{@user}-taste-tester.pem"
      @role_dir = opts[:role_dir]
      @role_type = opts[:role_type] || 'rb'
      @cookbook_dirs = opts[:cookbook_dirs]
      @databag_dir = opts[:databag_dir]
      @checksum_dir = opts[:checksum_dir]
      @client_key =
        File.expand_path("#{@home}/.chef/#{@user}-taste-tester.pem")
    end

    def role_upload_all
      if File.exist?(@role_dir)
        roles = File.join(@role_dir, "*.#{@role_type}")
        exec!("#{@knife} role from file #{roles} #{@knife_verb_option} " +
          "-c #{@config}", @logger)
      end
    end

    def role_upload(roles)
      if roles.any?
        roles = roles.map do |x|
          File.join(@role_dir, "#{x.name}.#{@role_type}")
        end.join(' ')
        exec!("#{@knife} role from file #{roles} #{@knife_verb_option} " +
          "-c #{@config}", @logger)
      end
    end

    def role_delete(roles)
      if roles.any?
        roles.each do |role|
          exec!("#{@knife} role delete #{role.name} #{@knife_verb_option} " +
            "--yes -c #{@config}", @logger)
        end
      end
    end

    def cookbook_upload_all
      exec!("#{@knife} cookbook upload -a #{@knife_verb_option} " +
        "-c #{@config}", @logger)
    end

    def berks_cookbook_upload_all
      if @berks_config
        berks_config = '--config=' + @berks_config
      end
      @cookbook_dirs.each do |path|
        cookbooks = Dir["#{path}/*"].select { |o| File.directory?(o) }
        cookbooks.each do |cb|
          @logger.warn("Running berkshelf on cookbook: #{cb}")
          exec!("cd #{cb} && #{@berks} install #{berks_config} && " +
            "#{@berks} upload #{berks_config}", @logger)
        end
      end
    end

    def cookbook_upload(cookbooks)
      if cookbooks.any?
        cookbooks = cookbooks.map(&:name).join(' ')
        exec!("#{@knife} cookbook upload #{cookbooks} #{@knife_verb_option} " +
          "-c #{@config}", @logger)
      end
    end

    def berks_cookbook_upload(cookbooks)
      # cookbooks: array
      # cookbook_paths: array
      if @berks_config
        berks_config = '--config=' + @berks_config
      end
      if cookbooks.any?
        @cookbook_dirs.each do |path|
          cookbooks.each do |cb|
            next unless File.exist?("#{path}/#{cb}")

            @logger.warn("Running berkshelf on cookbook: #{cb}")
            exec!("cd #{path}/#{cb} && #{@berks} install #{berks_config} && " +
              "#{@berks} upload #{berks_config}", @logger)
          end
        end
      end
    end

    def cookbook_delete(cookbooks)
      if cookbooks.any?
        cookbooks.each do |cookbook|
          exec!("#{@knife} cookbook delete #{cookbook.name}" +
                  " --purge -a --yes #{@knife_verb_option} -c #{@config}",
                @logger)
        end
      end
    end

    def databag_upload_all
      glob = File.join(@databag_dir, '*', '*.json')
      items = Dir.glob(glob).map do |file|
        BetweenMeals::Changes::Databag.new(
          { :status => :modified, :path => file }, @databag_dir
        )
      end
      databag_upload(items)
    end

    def databag_upload(databags)
      if databags.any?
        databags.group_by(&:name).each do |dbname, dbs|
          create_databag_if_missing(dbname)
          dbitems = dbs.map do |x|
            File.join(@databag_dir, dbname, "#{x.item}.json")
          end.join(' ')
          exec!(
            "#{@knife} data bag from file #{dbname} #{dbitems} " +
            "#{@knife_verb_option} -c #{@config}",
            @logger,
          )
        end
      end
    end

    def databag_delete(databags)
      if databags.any?
        databags.group_by(&:name).each do |dbname, dbs|
          dbs.each do |db|
            exec!("#{@knife} data bag delete #{dbname} #{db.item}" +
                    " --yes #{@knife_verb_option} -c #{@config}", @logger)
          end
          delete_databag_if_empty(dbname)
        end
      end
    end

    def write_user_config
      scheme = @ssl ? 'https' : 'http'
      cfg = <<-BLOCK
user = ENV['USER']
log_level :info
log_location STDOUT
node_name user
chef_server_url "#{scheme}://#{@host}:#{@port}"
ssl_verify_mode :verify_none
cache_type 'BasicFile'
client_key '#{@client_key}'
cache_options(:path => File.expand_path("#{@checksum_dir}"))
cookbook_path [
BLOCK
      @cookbook_dirs.each do |dir|
        cfg << "  \"#{dir}\",\n"
      end
      cfg << "]\n"
      begin
        Dir.mkdir(File.dirname(@config), 0o755)
      rescue Errno::EEXIST
        # not an error if it's already there.
        nil
      end
      if !File.exist?(@config) ||
         ::Digest::MD5.hexdigest(cfg) !=
         ::Digest::MD5.hexdigest(File.read(@config))
        @logger.info("Generating #{@config}")
        File.write(@config, cfg)
      end

      # Won't work with shorter keys
      pem = <<-BLOCK
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCs4Ih8+R/2hcYS
tccwJHd0cXHcUibC2wGYmRwf1fKxXLADvfuLRVBHOI5Hgd/ZXF70dowC5mDQ03gr
ouk8e7RL72MCKzPuG2V92sh/FnyKhkNsHCOEKaRRiP9lHVbZkS9LEotCKF7eOkL0
SVkGWx8pVZzrOFmhZgHaOFJ2/2t1irUTRFqTikrRsP2KvnhHdDlnnbUumZWxSuEN
oN6aSAQEOkKbEOLSn/EIMzEb2jtks7L7wkRErajH094jGoZbQvLiwRHDeM0C9uG7
2sdQ45BG9EQOCdBzy1We5keqtJbXBcpwuBa0d1nQZIsGxnDb88+Kmh9h6k9/WmYN
zEQEeSSdAgMBAAECggEATFWQru4p6ObEwTo2y9EuVeJJzmkP6HZfzAu/WWdVFG/C
4MQgsCxY+DnGyVhViVq6KuO1iwpCsbLOmyYCKszMncMESs7czUSXmezjHwrEzz3d
w3zhSdhBUCdX7kP4N3VeFp4Hk5zT1viO2+MPRjkyF0RQV6S4HwY1xy+baiP6RRnS
dGhUYsdz6fjxSkYEQy3/xHm9VLT6ZDV4pN2aA+LOFeveHKcnOjKFCBy4WzkO6fvj
6H3jghxsHXoL7loCHfi9WX3xKjeXG/NjGbUfTH8P7IldUPha+ru/e8W/P+jjE1os
VkScWt08Vu6iTl1EkYeFxOMtSDZxeXNnDkPI0iDQgQKBgQDUMFYncQcZ4uLIfoZq
B+Lx7WJGlIwulOdobnraTd5lsrBHj1sC1+/f4cBSpJwUij2l3GdmmnOpuFAof5eu
mrBGu++5jy+0eIeT5O2d30O8GOBryJ+oAKI2/BPVCmM8d986wl5Esauycb++O7UO
RhpZFOCKbFvlNjhg+CdlvHSl7QKBgQDQkkvpnE//yWmhCPg27n7w3bTg5QPNrzTO
pF2iwvLK4XjRceTeW3P4f42HONzJNnmt5TexM9NbdE9g/exA5uNt59ZB5FeFiKAu
NmVXbmswPX6R/dlyidqzz1guGrL04e0dZehHZBNDr5Sio8IBjMWrpDIxjDJqEwUa
4qCu4e6jcQKBgQDN0FTAzRFmOnxenNsj3aJzpx27+DpAtI4A7aicNwuQ+VGjF5nf
mDRDpGU3xBLgmXZSewaQrx+hb/XQUnJ+Ge0BrylHg2tyUbav7U3N49F/kWGdKmwy
OOsfCkLyUbEP5fXQuNdXKj6wR0UE8EUeI0FLRsTFf3VjTsRAynLsa295wQKBgAo3
QDSfDWQP73aNw+qc3+bYVSW20erfLAz7DAMO3WmGha5sj7M8c3+2b64x4M6SNn+H
/KRXT4DpP4IWrd238WfOtTXhA1BtErtwuqH/rIxeVra74kyz59xqyXzond9UuZJ5
DVmB01e7X+Jfdv8wb/YqQrMelNGRQOzCMPCf7FphAoGAbUh5HzNF2aciQJGA6Qk8
zvgEHqbS0/QkJGOZ+UifPRanTDuGYQkPdHHOER4UghbM+Kz5rZbBicJ3bCyNOsah
IAMAEpsWX2s2A6phgMCx7kH6wMmoZn3hb7Thh9+PfR8Jtp2/7k+ibCeF4gEWUCs5
6wX4GR84dwyhG80yd4TP8Qo=
-----END PRIVATE KEY-----
      BLOCK

      begin
        Dir.mkdir(File.dirname(@pem), 0o755)
      rescue Errno::EEXIST
        # not an error if it's already there.
        nil
      end

      unless File.exist?(@pem)
        @logger.info("Generating #{@pem}")
        File.write(@pem, pem)
      end
    end

    private

    def create_databag_if_missing(databag)
      s = Mixlib::ShellOut.new("#{@knife} data bag list" +
                               ' --format json ' +
                               "-c #{@config}").run_command

      s.stdout.gsub!(/^[A-Z].*\n/, '')
      s.error!
      db = JSON.parse(s.stdout)
      unless db.include?(databag)
        exec!("#{@knife} data bag create #{databag} #{@knife_verb_option} " +
          "-c #{@config}", @logger)
      end
    end

    def delete_databag_if_empty(databag)
      s = Mixlib::ShellOut.new("#{@knife} data bag show #{databag}" + # steep:ignore
                               " --format json #{@knife_verb_option} " +
                               "-c #{@config}").run_command
      s.error!
      db = JSON.parse(s.stdout)
      if db.empty?
        exec!("#{@knife} data bag delete #{databag} --yes " +
          "#{@knife_verb_option} -c #{@config}", @logger)
      end
    end
  end
end
