require "chef/json_compat"
require "providers/abstract_provider_connector"
require "providers/openstack/openstack_provider_account"
require_relative "openstack_accounts_factory"

module Provider
  # Provider for 'openstack'
  class OpenstackConnector < AbstractProviderConnector

    PROVIDER = "openstack"

    def initialize config
      self.connection_options = {
        :provider => PROVIDER,
        :openstack_username => config[:openstack_username],
        :openstack_api_key => config[:openstack_api_key],
        :openstack_auth_url => config[:openstack_auth_url],
        :openstack_tenant => config[:openstack_tenant]
      }
      self.run_list = config[:openstack_integration_run_list] || []
    end

    # Returns 'true' if all parameters defined
    def configured?
      o = self.connection_options
      super and !(empty_param?(o[:openstack_username]) or empty_param?(o[:openstack_api_key]) or empty_param?(o[:openstack_auth_url]) or empty_param?(o[:openstack_tenant]))
    end

    def name
      PROVIDER
    end

    def security_groups filters={}
      convert_groups(compute.list_security_groups.body["security_groups"])
    end

    def flavors
      self.compute.list_flavors_detail.body["flavors"].map do |f|
        {
          "id" => f["name"],
          "v_cpus" => f["vcpus"],
          "ram" => f["ram"],
          "disk" => f["disk"]
        }
      end
    end

    def images filters
      self.compute.list_images_detail.body["images"].select{|i| filters.include?(i["id"]) and i["status"] == "ACTIVE"}.map do |i|
        {
          "id" => i["id"],
          "name" => i["name"],
          "status" => i["status"]
        }
      end
    end

    def networks_detail filters={}
      net = self.network(filters)
      subnets = net.list_subnets.body["subnets"].select{|s| net.current_tenant["id"] == s["tenant_id"]}
      net.list_networks.body["networks"].select{|n| n["router:external"] == false and n["status"] == "ACTIVE" and net.current_tenant["id"] == n["tenant_id"]}.map{|n|
        sn = subnets.detect{|s| n["subnets"][0] == s["id"]}
        {
          "cidr" => sn["cidr"],
          "name" => n["name"],
          "id" => n["id"]
        }
      }
    end

    def networks filters={}
      net = self.network
      net.list_networks.body["networks"].select{|n| n["router:external"] == false and n["status"] == "ACTIVE" and net.current_tenant["id"] == n["tenant_id"]}.map{|n|
        {
          "name" => n["name"],
          "id" => n["id"]
        }
      }
    end

    def servers
      list = self.compute.list_servers_detail.body["servers"]
      list.map do |s|
        o = {"state" => s["status"], "name" => s["name"], "image" => s["image"]["id"], "flavor" => s["flavor"]["name"], "keypair" => s["key_name"], "instance_id" => s["id"]}
        s["addresses"].each_value do |a|
          a.each do |addr|
            o["private_ip"] = addr["addr"] if addr["OS-EXT-IPS:type"] == "fixed"
          end
        end
        o
      end
    end

    def create_server s, image, flavor, subnets, groups, out, options={}
      out << "Creating server for project '#{s.project} - #{s.environment}'\n"
      if s.name.nil?
        out << "Generate new instance name: "
        out << s.name = create_default_server_name(s)
        out << "\n"
      end
      networks = self.networks.select{|n| subnets.include?(n["name"])}
      buf = subnets - networks.map{|n| n["name"]}
      unless buf.empty?
        out << "No networks with names '#{buf.join("', '")}' found"
        return false
      end
      flavor = self.compute.list_flavors_detail.body["flavors"].detect{|f| f["name"] == flavor}["id"]
      out << "Creating server with name '#{s.name}', image '#{image}', flavor '#{flavor}', key '#{s.key}' and networks '#{networks.map{|n| n["name"]}.join("', '")}'...\n\n"
      compute = self.compute
      begin
        o_server = compute.create_server(s.name, image, flavor,
                           "nics" => networks.map{|n| {"net_id" => n["id"]}},
                           "security_groups" => groups,
                           "key_name" => s.key)
      rescue Excon::Errors::BadRequest => e
        response = ::Chef::JSONCompat.from_json(e.response.body)
        if response['badRequest']['code'] == 400
          if response['badRequest']['message'] =~ /Invalid flavorRef/
            out << "\nERROR: Bad request (400): Invalid flavor id specified: #{flavor}"
          elsif response['badRequest']['message'] =~ /Invalid imageRef/
            out << "\nERROR: Bad request (400): Invalid image specified: #{image}"
          else
            out << "\nERROR: Bad request (400): #{response['badRequest']['message']}"
          end
          out << "\n"
          return false
        else
          out << "\nERROR: Unknown server error (#{response['badRequest']['code']}): #{response['badRequest']['message']}"
          out << "\n"
          return false
        end
      rescue Excon::Errors::InternalServerError => ise
        out << "\nError: openstack internal server error " + ise.message
        out << "\n"
        return false
      rescue => e2
        out << "\nError: Unknown error: " + e2.message
        out << "\n"
        return false
      end
      sbody = o_server.body
      s.id = sbody["server"]["id"]
      return true
    end

    def waiting_server server, out
      details, status = nil, nil
      until status == "ACTIVE"
        sleep(5)
        details = compute.get_server_details(s.id).body
        status = details["server"]["status"].upcase
        if status == "ERROR"
          out << " error\nServer returns status 'ERROR'"
          out << details["server"]
          return false
        end
        out << "."
        out.flush
      end
      network = networks[0]["name"]
      s.private_ip = details["server"]["addresses"][network][0]["addr"]
      out << " Done\n\n"
      out.flush
      true
    end

    def delete_server s
      r = self.compute.delete_server(s.id)
      return r.status == 204 ? "Server with id '#{s.id}' terminated" : r.body
    end

    def pause_server s
      begin
        self.compute.pause_server s.id
      rescue Excon::Errors::Conflict => e
        return "pause"
      end
      return nil
    end

    def unpause_server s
      begin
        self.compute.unpause_server s.id
      rescue Excon::Errors::Conflict => e
        return "unpause"
      end
      return nil
    end

    def compute
      @compute ||= connection_compute(connection_options)
    end

    def network
      connection_network(self.connection_options)
    end

    def create_stack(stack, out)
      begin
        out << "Creating stack for project '#{stack.project}' and environment '#{stack.environment}'...\n"
        stack.name = create_default_stack_name(stack) unless stack.name
        out << "Stack name: #{stack.name}\n"
        out << "Stack template: #{stack.stack_template}\n"
        out << "Stack parameters: #{stack.parameters}\n"
        out.flush
        response = orchestration.create_stack(
          stack_name: stack.name,
          template: stack.stack_template_model.template_body,
          parameters: stack.parameters || {}
        )
        stack.id = response[:body]['stack']['id']
        out << "Stack id: #{stack.id}\n"
        out.flush
      rescue Excon::Errors::Conflict => e
        raise ProviderErrors::NameConflict
      rescue Excon::Errors::BadRequest => br
        response = ::Chef::JSONCompat.from_json(br.response.body)
        if response['code'] == 400
          out << "\nERROR: Bad request (400): #{response['explanation']}"
          out << "\n"
          raise InvalidRecord.new(response['explanation'])
        else
          out << "\nERROR: Unknown server error (#{response['code']}): #{response['explanation']}"
          out << "\n"
          raise InvalidRecord.new(response['explanation'])
        end
      end
    end

    def validate_stack_template template
      r = orchestration.validate_template({'template' => template})
      pp r.body
    end

    def delete_stack(stack)
      begin
        orchestration.delete_stack(Fog::Orchestration::OpenStack::Stack.new({'id' => stack.id, 'stack_name' => stack.name}))
      rescue Fog::Compute::OpenStack::NotFound
        puts 'already deleted'
      end
    end

    def stack_details(stack)
      details = orchestration.show_stack_details(stack.name, stack.id).body['stack']
      {
        stack_status: details[:stack_status]
      }
    end

    def stack_resources(stack)
      orchestration.list_resources(Fog::Orchestration::OpenStack::Stack.new({'id' => stack.id, 'stack_name' => stack.name})).body['resources']
    end

    def stack_resource(stack, resource_id)
      fog_stack = orchestration.stacks.get(stack.name, stack.id)
      physical_id = fog_stack(stack).resources.get(resource_id).physical_resource_id
      compute.servers.get(physical_id)
    end

    def stack_servers(stack)
      stack_resources(stack).map do |r|
        server = compute.servers.get(r['physical_resource_id'])
        net = server.addresses.first
        ip = net.last.first['addr']
        {
          'name' => server.name,
          'id' => server.id,
          'key_name' => server.key_name,
          'private_ip' => ip,
          'public_ip' => ip
        }
      end
    end

  private
    def convert_groups list
      res = {}
      list.map do |g|
        res[g["name"]] = {
          "description" => g["description"]
        }
        rules = []
        g["rules"].each do |r|
          rules.push({
            "protocol" => r["ip_protocol"],
            "from" => r["from_port"],
            "to" => r["to_port"],
            "cidr" => r["ip_range"]["cidr"]
          })
        end
        res[g["name"]]["rules"] = rules
      end
      res
    end

    def orchestration
      @orchestration ||= Fog::Orchestration.new(connection_options)
    end

  end
end
