# -*- encoding: utf-8 -*-
#
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
#
# Copyright (C) 2013, Fletcher Nichol
#
# 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 "shellwords"
require "rbconfig"

require "kitchen/errors"
require "kitchen/logging"
require "kitchen/shell_out"

module Kitchen
  module Provisioner
    module Chef
      # Chef cookbook resolver that uses Policyfiles to calculate dependencies.
      #
      # @author Fletcher Nichol <fnichol@nichol.ca>
      class Policyfile
        include Logging
        include ShellOut

        # Creates a new cookbook resolver.
        #
        # @param policyfile [String] path to a Policyfile
        # @param path [String] path in which to vendor the resulting
        #   cookbooks
        # @param logger [Kitchen::Logger] a logger to use for output, defaults
        #   to `Kitchen.logger`
        def initialize(policyfile, path, logger: Kitchen.logger, always_update: false)
          @policyfile = policyfile
          @path       = path
          @logger     = logger
          @always_update = always_update
        end

        # Loads the library code required to use the resolver.
        #
        # @param logger [Kitchen::Logger] a logger to use for output, defaults
        #   to `Kitchen.logger`
        def self.load!(logger: Kitchen.logger)
          detect_chef_command!(logger)
        end

        # Performs the cookbook resolution and vendors the resulting cookbooks
        # in the desired path.
        def resolve
          info("Exporting cookbook dependencies from Policyfile #{path}...")
          run_command("chef export #{escape_path(policyfile)} #{escape_path(path)} --force")
        end

        # Runs `chef install` to determine the correct cookbook set and
        # generate the policyfile lock.
        def compile
          if always_update
            info("Updating policy lock using `chef update`")
            run_command("chef update #{escape_path(policyfile)}")
          end
          if File.exist?(lockfile)
            info("Installing cookbooks for Policyfile #{policyfile} using `chef install`")
          else
            info("Policy lock file doesn't exist, running `chef install` for "\
                 "Policyfile #{policyfile}...")
          end
          run_command("chef install #{escape_path(policyfile)}")
        end

        # Return the path to the lockfile corresponding to this policyfile.
        #
        # @return [String]
        def lockfile
          policyfile.gsub(/\.rb\Z/, ".lock.json")
        end

        private

        # @return [String] path to a Berksfile
        # @api private
        attr_reader :policyfile

        # @return [String] path in which to vendor the resulting cookbooks
        # @api private
        attr_reader :path

        # @return [Kitchen::Logger] a logger to use for output
        # @api private
        attr_reader :logger

        # @return [Boolean] If true, always update cookbooks in the policy.
        # @api private
        attr_reader :always_update

        # Escape spaces in a path in way that works with both Sh (Unix) and
        # Windows.
        #
        # @param path [String] Path to escape
        # @return [String]
        # @api private
        def escape_path(path)
          if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
            # I know what you're thinking: "just use Shellwords.escape". That
            # method produces incorrect results on Windows with certain input
            # which would be a metacharacter in Sh but is not for one or more of
            # Windows command line parsing libraries. This covers the 99% case of
            # spaces in the path without breaking other stuff.
            if path =~ /[ \t\n\v"]/
              "\"#{path.gsub(/[ \t\n\v\"\\]/) { |m| '\\' + m[0] }}\""
            else
              path
            end
          else
            Shellwords.escape(path)
          end
        end

        class << self
          private

          # Ensure the `chef` command is in the path.
          #
          # @param logger [Kitchen::Logger] the logger to use
          # @raise [UserError] if the `chef` command is not in the PATH
          # @api private
          def detect_chef_command!(logger)
            unless ENV["PATH"].split(File::PATH_SEPARATOR).any? do |p|
              File.exist?(File.join(p, "chef"))
            end
              logger.fatal("The `chef` executable cannot be found in your " \
                          "PATH. Ensure you have installed ChefDK from " \
                          "https://downloads.chef.io and that your PATH " \
                          "setting includes the path to the `chef` comand.")
              raise UserError,
                    "Could not find the chef executable in your PATH."
            end
          end
        end

      end
    end
  end
end
