module Berkshelf
  # All tasks that operate on the Berkshelf shelf.
  class Shelf < Thor
    desc "list", "List all cookbooks and their versions"
    def list
      cookbooks = store.cookbooks.inject({}) do |hash, cookbook|
        (hash[cookbook.cookbook_name] ||= []).push(cookbook.version)
        hash
      end

      if cookbooks.empty?
        Berkshelf.formatter.msg "There are no cookbooks in the Berkshelf shelf"
      else
        Berkshelf.formatter.msg "Cookbooks in the Berkshelf shelf:"
        cookbooks.sort.each do |cookbook, versions|
          Berkshelf.formatter.msg("  * #{cookbook} (#{versions.sort.join(", ")})")
        end
      end
    end

    method_option :version, aliases: "-v", type: :string, desc: "Version to show"
    desc "show", "Display information about a cookbook in the Berkshelf shelf"
    def show(name)
      cookbooks = find(name, options[:version])

      if options[:version]
        Berkshelf.formatter.msg "Displaying '#{name}' (#{options[:version]}) in the Berkshelf shelf:"
      else
        Berkshelf.formatter.msg "Displaying all versions of '#{name}' in the Berkshelf shelf:"
      end

      cookbooks.each do |cookbook|
        Berkshelf.formatter.info(cookbook)
        Berkshelf.formatter.msg("\n")
      end
    end

    method_option :version, aliases: "-v", type: :string,  desc: "Version to remove"
    method_option :force,   aliases: "-f", type: :boolean, desc: "Force removal, even if other cookbooks are contingent", default: false
    desc "uninstall", "Remove a cookbook from the Berkshelf shelf"
    def uninstall(name)
      cookbooks = find(name, options[:version])
      cookbooks.each { |c| uninstall_cookbook(c, options[:force]) }
    end

    no_tasks do
      # Shortcut helper to the CookbookStore
      #
      # @return [Berkshelf::CookbookStore]
      def store
        Berkshelf.cookbook_store
      end

      # Find a cookbook in the store by name and version. If the no version
      # is given, all cookbooks with the given name are returned. Otherwise,
      # only the cookbook matching the given version is returned.
      #
      # @param [String] name
      #   the name of the cookbook to find
      # @param [String, nil] version
      #   the version of the cookbook to find
      #
      # @raise [CookbookNotFound]
      #   if the cookbook does not exist
      #
      # @return [Array<CachedCookbook>]
      #   the list of cookbooks that match the parameters - this is always an
      #   array!
      def find(name, version = nil)
        cookbooks =
          if version
            [store.cookbook(name, version)].compact
          else
            store.cookbooks(name).sort
          end

        if cookbooks.empty?
          raise CookbookNotFound.new(name, version, "in the Berkshelf shelf")
        end

        cookbooks
      end

      # Uninstall a cookbook from the CookbookStore. This method assumes the
      # cookbook exists, so perform that validation elsewhere.
      #
      # By default, this method will request confirmation from the user to
      # delete a cookbook that is a dependency on another (contingent). This
      # behavior can be overridden by setting the second parameter `force` to
      # true.
      #
      # @param [Berkshelf::CachedCookbook] cookbook
      #   the cookbook to uninstall
      # @param [Boolean] force
      #   if false, the user will need to confirm before uninstalling
      #   if contingencies exist
      def uninstall_cookbook(cookbook, force = false)
        unless options[:force] || (contingent = contingencies(cookbook)).empty?
          contingent = contingent.map { |c| "#{c.cookbook_name} (#{c.version})" }.join(", ")
          confirm = Berkshelf.ui.ask("[#{contingent}] depend on #{cookbook.cookbook_name}.\n\nAre you sure you want to continue? (y/N)")

          exit unless confirm.to_s.upcase[0] == "Y"
        end

        FileUtils.rm_rf(cookbook.path)
        Berkshelf.formatter.msg("Successfully uninstalled #{cookbook.cookbook_name} (#{cookbook.version})")
      end

      # Return a list of all cookbooks which are contingent upon the given
      # cookbook.
      #
      # @param [Berkshelf::CachedCookbook] cookbook
      #   the cached cookbook to search for dependencies against
      #
      # @return [Array<Berkshelf::CachedCookbook>]
      #   the list of cookbooks which depend on the parameter
      def contingencies(cookbook)
        store.cookbooks.select { |c| c.dependencies.include?(cookbook.cookbook_name) }
      end
    end
  end

  class Cli < Thor
    desc "shelf SUBCOMMAND", "Interact with the cookbook store"
    subcommand "shelf", Berkshelf::Shelf
  end
end
