require 'openssl'
require 'tmpdir'

require 'chef/cookbook_loader'
require 'chef/cookbook_uploader'
require 'chef/server_api'

require_relative '../examples'

module Culinaryrobot
  module Internal # :nodoc: all
    module Examples
      # Example that starts a “hello world” web server, configures it with Chef,
      # and tests it from another machine
      class Webserver < Example
        def run_example
          [
              Thread.new { start_chef_server },
              Thread.new { @server, @server_ssh = server("server") },
              Thread.new { @client, @client_ssh = server("client") },
              Thread.new { @client2, @client2_ssh = server("client2") },
              Thread.new { @client3, @client3_ssh = server("client3") }
          ].map(&:join)
          # FIXME: create nodes + clients as soon as org is created
          # Create test org
          p(chef_api.get('/organizations'))
          chef_api.post "/organizations", {
              name: "test", full_name: "test"}
          # Upload cookbooks
          upload_cookbooks
          # Bootstrap
          bootstrap(@server_ssh, "server", ["recipe[server::server]"])
          bootstrap(@client_ssh, "client", ["recipe[client::test_server]"])
          bootstrap(@client2_ssh, "client2", ["recipe[client::test_server]"])
          bootstrap(@client3_ssh, "client3", ["recipe[client::test_server]"])
          [@server_ssh, @client_ssh, @client2_ssh, @client3_ssh].map do |ssh|
            Thread.new do
              name = \
                case ssh
                when @server_ssh
                  "server"
                when @client_ssh
                  "client"
                when @client2_ssh
                  "client2"
                when @client3_ssh
                  "client3"
                else
                  raise "Unknown SSH client #{ssh}"
                end
              ssh.run [
                  "sudo", "bash", "-ec",
                  "\"echo 'chef_server_url \
                  \\\"https://#{chef_ip_address}/organizations/test\\\"
node_name \\\"#{name}\\\"
' > /etc/chef/client.rb\""]
            end
          end.map(&:join)
          # Server has to run before client for test to succeed
          @server_ssh.run ["sudo", "chef-client"]
          [@client_ssh, @client2_ssh, @client3_ssh].map do |ssh|
            Thread.new { ssh.run ["sudo", "chef-client"] }
          end.map(&:join)

          [@server, @client, @client2, @client3].map do |vm|
            Thread.new { vm.snapshot("first-run done") }
          end.map(&:join)

          prev_time = nil
          [81, 88, 888, 8088, 8888, 8808].each do |port|
            ([@server, @client, @client2, @client3].map do |vm|
              Thread.new { vm.revert_to("first-run done") }
            end + [
                Thread.new do
                  server_node = chef_api('test').get '/nodes/server'
                  server_node['normal'] ||= {}
                  server_node['normal']['webserver'] ||= {}
                  server_node['normal']['webserver']['port'] = port.to_s
                  chef_api('test').put '/nodes/server', server_node
                end
            ]).map(&:join)
            [@server_ssh, @client_ssh, @client2_ssh, @client3_ssh].map do |ssh|
              Thread.new { ssh.run ["date"] }
            end.map(&:join)
            unless prev_time.nil?
              cur_time = Time.now.to_f
              printf "\nRound-trip time: %.03f\n\n", cur_time - prev_time
            end
            @server_ssh.run ["sudo", "chef-client"]
            [@client_ssh, @client2_ssh, @client3_ssh].map do |ssh|
              Thread.new { ssh.run ["sudo", "chef-client"] }
            end.map(&:join)
            prev_time = Time.now.to_f
          end

          if ARGV.include?("--interactive")
            puts "server: #{@server_ssh.ip_addr}"
            puts "client: #{@client_ssh.ip_addr}"

            loop do
              print "Rollback/eXit> "
              input = STDIN.gets
              unless input.nil?
                input = input.strip
              end
              case input
              when 'R'
                ([@server, @client].map do |vm|
                  Thread.new { vm.revert_to("first-run done") }
                end + [
                    Thread.new { upload_cookbooks }
                ]).map(&:join)
              when 'X', nil
                break
              else
                puts '?'
              end
            end
          end
        ensure
          stop_chef_server
          @server.power_off
          @server.delete
          @client.power_off
          @client.delete
          @client2.power_off
          @client2.delete
          @client3.power_off
          @client3.delete
        end

        def upload_cookbooks
          repo = Chef::CookbookLoader.new(
              Pathname(__FILE__).dirname.join("webserver", "cookbooks"))
          cookbooks = repo.load_cookbooks.values
          verbose_chef_rest_exceptions("Uploading #{cookbooks}") do
            Chef::CookbookUploader.new(
                cookbooks, rest: chef_api('test')).upload_cookbooks
          end
        end

        def verbose_chef_rest_exceptions(msg = "")
          yield
        rescue Net::HTTPServerException => e
          puts "Error #{msg}: #{e.chef_rest_request.class}" \
            " #{e.chef_rest_request.path} returned #{e.response.body}"
          raise
        end

        def server(name)
          clone_path = vm_path "culinaryrobot-webserver-#{name}"
          base_vm.clone(clone_path, "Base")

          child_vm = @host.vm_from_path(clone_path)
          child_vm.power_on
          child_vm.wait_for_tools
          guest = child_vm.login('root', 'test')
          ssh = guest.wait_for_ssh
          [child_vm, ssh]
        end

        def bootstrap(ssh, name, run_list = [])
          server_cert = cert_dir.join("#{chef_ip_address}.crt").read
          ssh.run [
              "sudo", "bash", "-ec",
              "\"mkdir -p /etc/chef/trusted_certs
echo '#{server_cert}' > /etc/chef/trusted_certs/#{chef_ip_address}.crt\""
          ]

          chef_key = OpenSSL::PKey.read(ssh.chef_client_private_key)
          verbose_chef_rest_exceptions("Creating client #{name}") do
            chef_api('test').post "/clients", {
                name: name,
                chef_type: "client",
                json_class: "Chef::Client",
                public_key: chef_key.public_key.to_pem
            }
          end
          verbose_chef_rest_exceptions("Creating node #{name}") do
            # use the client key to create the node so that it has save perms
            Dir.mktmpdir do |tmpdir|
              key_file = Pathname(tmpdir).join("client.pem")
              IO.write(key_file, chef_key.to_pem)
              client_api = Chef::ServerAPI.new(
                  "https://#{chef_ip_address}/organizations/test",
                  client_name: name,
                  signing_key_filename: key_file)
              client_api.post "/nodes", {
                  name: name,
                  chef_type: "node",
                  json_class: "Chef::Node",
                  run_list: run_list
              }
            end
          end
        end

        def cert_dir
          chef_server_access_dir.join 'trusted_certs'
        end

        def pivotal_key_file
          chef_server_access_dir.join "pivotal.pem"
        end

        def ip_address_file
          chef_server_access_dir.join "ip_address"
        end

        def chef_ip_address
          @chef_ip_address ||= IO.read(ip_address_file)
        end

        def chef_api(org = nil)
          Chef::Config.trusted_certs_dir = cert_dir.to_s
          Chef::ServerAPI.new(
              "https://#{chef_ip_address}" +
                  (org.nil? ? "" : "/organizations/#{org}"),
              client_name: "pivotal",
              signing_key_filename: pivotal_key_file)
        end

        def chef_server_path
          vm_path "culinaryrobot-chef-server"
        end

        def chef_server_access_dir
          chef_server_path.dirname.join '.chef'
        end

        def start_chef_server
          if File.exist?(chef_server_path)
            @chef_server = @host.vm_from_path(chef_server_path)
            @chef_server.revert_to("Ready")
          else
            @chef_server = create_chef_server
          end
        end

        def stop_chef_server
          host.vm_from_path(chef_server_path, &:power_off)
        end

        def create_chef_server
          host.vm_from_path(base_vmx_path) do |base_vm|
            base_vm.clone(chef_server_path, "Base")

            File.open(chef_server_path, 'r+') do |vmx_file|
              editor = Culinaryrobot::Vix::VmxEditor.new vmx_file
              editor[:memsize] = 2048
              editor.save
            end

            host.vm_from_path(chef_server_path) do |chef_server_vm|
              @chef_server_vm = chef_server_vm

              chef_server_vm.power_on
              chef_server_vm.wait_for_tools

              # @type [Culinaryrobot::VixGuest]
              guest = chef_server_vm.login('root', 'test')
              # @type [Culinaryrobot::SSHableVM]
              chef_server_ssh = guest.wait_for_ssh
              puts chef_server_ssh.ip_addr

              cache = Culinaryrobot::Vix::Internal::Cache.new
              local_server_rpm = cache.get_local(
                  'https://packages.chef.io/stable/el/7/chef-server-core-12.6.0-1.el7.x86_64.rpm',
                  'sha256:5a36d0ffa692bf788e90315171582a758d4c5d8033a892dca9a81d3c03c44d14'
              )
              rpm_basename = File.basename local_server_rpm
              p local_server_rpm
              chef_server_ssh.scp(
                  local_server_rpm, "/tmp/#{rpm_basename}")
              chef_server_ssh.run [
                  "sudo", "rpm", "-ihp", "/tmp/#{rpm_basename}"]

              # FIXME: allow sanely running commands with spaces over SSH
              chef_server_ssh.run [
                  "sudo", "bash", "-ec",
                  "\"umask 0077
mkdir -p /etc/chef
echo 'api_fqdn \\\"#{chef_server_ssh.ip_addr}\\\"'" \
 " > /etc/opscode/chef-server.rb\""]

              chef_server_ssh.run [
                  "sudo", "chef-server-ctl", "reconfigure"]

              chef_server_ssh.run [
                  "sudo", "firewall-cmd", "--add-port", "443/tcp"]
              chef_server_ssh.run [
                  "sudo", "firewall-cmd", "--permanent",
                  "--add-port", "443/tcp"]

              ssl_cert = chef_server_ssh.run(
                  [
                      "sudo", "cat",
                      "/var/opt/opscode/nginx/ca/" +
                          chef_server_ssh.ip_addr + ".crt"
                  ],
                  capture_output: true)
              pivotal_key = chef_server_ssh.run(
                  ["sudo", "cat", "/etc/opscode/pivotal.pem"],
                  capture_output: true)

              # FIXME: lock down directory permissions
              chef_server_access_dir.mkpath
              IO.write(ip_address_file, chef_server_ssh.ip_addr)
              cert_dir.mkpath
              cert_file = cert_dir + "#{chef_server_ssh.ip_addr}.crt"
              IO.write(cert_file, ssl_cert)
              IO.write(pivotal_key_file, pivotal_key)

              p chef_api.get '/organizations'

              chef_server_vm.snapshot("Ready")
            end
          end
        end
      end
    end
  end
end

if __FILE__ == $PROGRAM_NAME
  Culinaryrobot::Internal::Examples::Webserver.main
end
