require "muchkeys/version"
require "muchkeys/errors"
require "muchkeys/configuration"
require "muchkeys/secret"
require "muchkeys/key_validator"
require "muchkeys/cli"
require "muchkeys/cli/validator"

require "net/http"

module MuchKeys

  # revealing intention
  module BlankKey; false; end


  class << self
    attr_accessor :configuration

    def configure
      self.configuration ||= MuchKeys::Configuration.new
      if block_given?
        yield configuration
      end
    end

    # this is the entry point to the ENV-like object that devs will use
    def find_first(key_name)
      unless MuchKeys.configuration.search_paths
        raise MuchKeys::UnknownApplication, "Can't detect app name and application_name isn't set." if !application_name
      end

      consul_paths_to_search = search_order(application_name, key_name)

      # search consul in a specific order until we find something
      response = nil
      consul_paths_to_search.detect do |consul_path|
        response = fetch_key(consul_path)
        response if response != MuchKeys::BlankKey
      end

      if response == BlankKey
        raise MuchKeys::NoKeysSet, "Bailing.  Consul isn't set with any keys for #{key_name}."
      end

      response
    end

    def fetch_key(key_name, public_key:nil, private_key:nil)
      return ENV[key_name] unless ENV[key_name].nil?

      if secret_adapter.is_secret?(key_name)
        raise InvalidKey unless validate_key_name(key_name)

        # configure automatic certificates
        public_key  = find_certfile_for_key(key_name) if public_key.nil?
        private_key = find_certfile_for_key(key_name) if private_key.nil?

        response = fetch_secret_key(key_name, public_key, private_key)
      else
        response = fetch_plain_key(key_name)
      end

      # otherwise, we got consul data so return that
      response
    end

    def search_order(application_name, key_name)
      if MuchKeys.configuration.search_paths
        search_paths = MuchKeys.configuration.search_paths.collect do |path|
          "#{path}/#{key_name}"
        end
      else
        search_paths = [
          "git/#{application_name}/secrets/#{key_name}",
          "git/#{application_name}/config/#{key_name}",
          "git/shared/secrets/#{key_name}",
          "git/shared/config/#{key_name}"
        ]
      end

      search_paths
    end


    private
    def consul_url(key_name)
      URI("#{configuration.consul_url}/v1/kv/#{key_name}?raw")
    end

    def fetch_plain_key(key_name)
      url = consul_url(key_name)
      response = Net::HTTP.get_response url

      return MuchKeys::BlankKey if !response.body || response.body.empty?
      response.body
    end

    def fetch_secret_key(key_name, public_pem=nil, private_pem=nil)
      result = fetch_plain_key(key_name)

      # we hit a key that doesn't exist, so don't try to decrypt it
      return MuchKeys::BlankKey if result == MuchKeys::BlankKey

      secret_adapter.decrypt_string(result, public_pem, private_pem)
    end

    def find_certfile_for_key(key_name)
      secret_adapter.certfile_name key_name
    end

    def secret_adapter
      MuchKeys::Secret
    end

    def validate_key_name(key_name)
      MuchKeys::KeyValidator.valid? key_name
    end

    # Detecting Rails app names is a known quantity.
    # Rack apps need to set the name through config.
    def application_name
      return configuration.application_name if configuration.application_name

      if defined?(Rails)
        # Rails.application looks something like "Monorail::Application"
        application_name = Rails.application.class.to_s.split("::").first
      else
        return false
      end

      snakecase(application_name)
    end

    # MyApp should become my_app
    def snakecase(string)
      string.gsub(/::/, '/').
        gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
        gsub(/([a-z\d])([A-Z])/,'\1_\2').
        tr("-", "_").
        downcase
    end

  end
end

# default configure the gem on gem load
MuchKeys.configuration ||= MuchKeys::Configuration.new


# This interface looks like ENV which is more friendly to devs
class MUCHKEYS

  def self.[](key_name)
    # if an ENV key is set, use that first
    return ENV[key_name] unless ENV[key_name].nil?

    MuchKeys.find_first(key_name)
  end

end