module Awsborn
  module Chef #:nodoc:
    # Just add a `Rakefile` in the same directory as your server definition file.
    #
    #     require 'awsborn'
    #     include Awsborn::Chef::Rake
    #     require './servers'
    #
    # You are now able to run `rake` to start all servers and run Chef on each of them.
    # Other rake tasks include:
    #
    # * `rake chef` - Run chef on all servers, or the ones specified with `host=name1,name2`.
    # * `rake chef:debug` - Ditto, but with chef's log level set to `debug`.
    # * `rake start` - Start all servers (or host=name1,name2) but don't run `chef`.
    #
    # You can use `server=name1,name2` as a synonym for `host=...`
    #
    module Rake
      # Compatibility with 0.8.7 & 0.9.2.2
      extend ::Rake::DSL rescue nil

      desc "Default: Start all servers (if needed) and deploy with chef."
      task :all => [:start, "chef:run"]
      task :default => :all

      desc "Like 'all' but with chef debugging on."
      task :debug => ["chef:set_chef_debug", :all]

      desc "Start all servers (or host=name1,name2) but don't run chef."
      task :start do |t,args|
        c = cluster(args)
        c.launch get_hosts(args)
        info = c.load_balancer_info
        print_required_dns_setting(info) if info
      end

      def print_required_dns_setting (info)
        puts "Make sure those NS records are present in your DNS settings:"
        puts(info.map do |dns_entry|
          dns_entry[:name_servers].map do |ns|
            "#{dns_entry[:name]}\tIN\tNS\t#{ns}."
          end
        end.join("\n"))
      end

      desc "Update .ssh/known_hosts with data from all servers (or host=host1,host2)"
      task :update_known_hosts do |t,args|
        hosts = get_hosts(args)
        cluster(args).each do |server|
          server.running? && server.update_known_hosts if hosts.nil? || hosts.include?(server.name.to_s)
        end
      end

      desc "Run chef on all servers (or host=name1,name2)."
      task :chef => "chef:run"

      namespace :chef do
        task :run => [:check_syntax] do |t,args|
          hosts = get_hosts(args)
          cluster(args).each do |server|
            next unless hosts.nil? || hosts.include?(server.name.to_s)
            puts framed("Running chef on '#{server.name}'")
            server.cook
          end
        end

        desc "Run chef on all servers with log_level debug."
        task :debug => [:set_chef_debug, :run]
        task :set_chef_debug do
          Awsborn.chef_log_level = :debug
        end

        desc "Check your cookbooks and config files for syntax errors."
        task :check_syntax do
          Dir["**/*.rb"].each do |recipe|
            RakeFileUtils.verbose(false) do
              sh %{ruby -c #{recipe} > /dev/null} do |ok, res|
                raise "Syntax error in #{recipe}" if not ok
              end
            end
          end
        end

        desc "Create a new cookbook (with cookbook=name)."
        task :new_cookbook do
          create_cookbook("cookbooks")
        end
      end

      desc "List running servers"
      task :list do |t,args|
        # Disable annoying "Waiting for console output.."
        Awsborn.verbose = false

        cluster_name = cluster_param(args)
        if cluster_name
          clusters = Awsborn::ServerCluster.clusters.select {|cluster| cluster.name == cluster_name}
        else
          clusters = Awsborn::ServerCluster.clusters
        end

        clusters.each do |cluster|
          servers = cluster.select { |server| server.running? }
          balancers = cluster.load_balancers.select { |balancer| balancer.running? }

          next if servers.empty? && balancers.empty?

          cluster_header = "Cluster: #{cluster.name}"
          puts cluster_header
          puts '=' * cluster_header.length

          balancers.each do |balancer|
            desc = balancer.description
            instances = desc[:instances].any? ? desc[:instances].join(', ') : 'None'
            puts "Balancer: %s -- %s -- %s -- Instances: %s" % [balancer.name, balancer.dns_alias, desc[:availability_zones].join(', '), instances]
          end

          max_name_length = servers.map { |server| server.name.to_s.size }.max
          servers.each do |server|
            h = server.describe_instance
            puts "Server:   %-#{max_name_length}s -- %s -- %s -- %s (%s) %s" %
              [server.name, server.host_name, h[:aws_availability_zone], h[:aws_instance_type], h[:architecture], h[:aws_instance_id]]
          end

          puts
        end
      end

      desc "Update chef on the server"
      task :update_chef do |t,args|
        hosts = get_hosts(args)
        cluster(args).each do |server|
          next if hosts && ! hosts.include?(server.name.to_s)
          puts framed("Updating chef on server #{server.name}")
          # Include excplicit path to avoid rvm
          sh "ssh root@#{server.host_name} 'PATH=/usr/sbin:/usr/bin:/sbin:/bin gem install chef --no-ri --no-rdoc'"
        end
      end

      def create_cookbook(dir) #:nodoc:
        raise "Must provide a cookbook=" unless ENV["cookbook"]
        puts "** Creating cookbook #{ENV["cookbook"]}"
        sh "mkdir -p #{File.join(dir, ENV["cookbook"], "attributes")}"
        sh "mkdir -p #{File.join(dir, ENV["cookbook"], "recipes")}"
        sh "mkdir -p #{File.join(dir, ENV["cookbook"], "definitions")}"
        sh "mkdir -p #{File.join(dir, ENV["cookbook"], "libraries")}"
        sh "mkdir -p #{File.join(dir, ENV["cookbook"], "files", "default")}"
        sh "mkdir -p #{File.join(dir, ENV["cookbook"], "templates", "default")}"

        unless File.exists?(File.join(dir, ENV["cookbook"], "recipes", "default.rb"))
          open(File.join(dir, ENV["cookbook"], "recipes", "default.rb"), "w") do |file|
            file.puts <<-EOH
#
# Cookbook Name:: #{ENV["cookbook"]}
# Recipe:: default
#
EOH
          end
        end
      end

      def get_hosts (args) #:nodoc:
        host_param(args) && host_param(args).split(',')
      end

      def framed (message) #:nodoc:
        '*' * (4 + message.length) + "\n* #{message} *\n" + '*' * (4 + message.length)
      end

      def cluster (args) #:nodoc:
        name = cluster_param(args) || 'cluster1'
        Awsborn::ServerCluster.clusters.detect { |c| c.name == name } || raise("Could not find cluster named '#{name}'")
      end

      def host_param(args)
        args[:host] || args[:server] || ENV['host'] || ENV['server']
      end

      def cluster_param(args)
        args[:c] || args[:cluster] || ENV['c'] || ENV['cluster']
      end

    end
  end
end
