require 'json/pure'

module LeapCli
  module Config

    #
    # A class to manage all the objects in all the configuration files.
    #
    class Manager

      attr_reader :services, :tags, :nodes, :provider, :common, :secrets

      ##
      ## IMPORT EXPORT
      ##

      #
      # load .json configuration files
      #
      def load
        @provider_dir = Path.provider

        # load base
        base_services = load_all_json(Path.named_path([:service_config, '*'], Path.provider_base))
        base_tags     = load_all_json(Path.named_path([:tag_config, '*'], Path.provider_base))
        base_common   = load_json(Path.named_path(:common_config, Path.provider_base))
        base_provider = load_json(Path.named_path(:provider_config, Path.provider_base))

        # load provider
        provider_path = Path.named_path(:provider_config, @provider_dir)
        common_path = Path.named_path(:common_config, @provider_dir)
        Util::assert_files_exist!(provider_path, common_path)
        @services = load_all_json(Path.named_path([:service_config, '*'], @provider_dir))
        @tags     = load_all_json(Path.named_path([:tag_config, '*'],     @provider_dir))
        @nodes    = load_all_json(Path.named_path([:node_config, '*'],    @provider_dir))
        @common   = load_json(common_path)
        @provider = load_json(provider_path)
        @secrets  = load_json(Path.named_path(:secrets_config,  @provider_dir))

        # inherit
        @services.inherit_from! base_services
        @tags.inherit_from!     base_tags
        @common.inherit_from!   base_common
        @provider.inherit_from! base_provider
        @nodes.each do |name, node|
          @nodes[name] = apply_inheritance(node)
        end

        # validate
        validate_provider(@provider)
      end

      #
      # save compiled hiera .yaml files
      #
      def export_nodes
        existing_hiera = Dir.glob(Path.named_path([:hiera, '*'], @provider_dir))
        existing_files = Dir.glob(Path.named_path([:node_files_dir, '*'], @provider_dir))
        updated_hiera = []
        updated_files = []
        self.each_node do |node|
          filepath = Path.named_path([:node_files_dir, node.name], @provider_dir)
          updated_files << filepath
          hierapath = Path.named_path([:hiera, node.name], @provider_dir)
          updated_hiera << hierapath
          Util::write_file!(hierapath, node.dump)
        end
        (existing_hiera - updated_hiera).each do |filepath|
          Util::remove_file!(filepath)
        end
        (existing_files - updated_files).each do |filepath|
          Util::remove_directory!(filepath)
        end
      end

      def export_secrets(destination_file = nil)
        if @secrets.any?
          file_path = destination_file || Path.named_path(:secrets_config, @provider_dir)
          Util.write_file!(file_path, @secrets.dump_json + "\n")
        end
      end

      ##
      ## FILTERING
      ##

      #
      # returns a node list consisting only of nodes that satisfy the filter criteria.
      #
      # filter: condition [condition] [condition] [+condition]
      # condition: [node_name | service_name | tag_name]
      #
      # if conditions is prefixed with +, then it works like an AND. Otherwise, it works like an OR.
      #
      def filter(filters)
        if filters.empty?
          return nodes
        end
        if filters[0] =~ /^\+/
          # don't let the first filter have a + prefix
          filters[0] = filters[0][1..-1]
        end

        node_list = Config::ObjectList.new
        filters.each do |filter|
          if filter =~ /^\+/
            keep_list = nodes_for_name(filter[1..-1])
            node_list.delete_if do |name, node|
              if keep_list[name]
                false
              else
                true
              end
            end
          else
            node_list.merge!(nodes_for_name(filter))
          end
        end
        return node_list
      end

      #
      # same as filter(), but exits if there is no matching nodes
      #
      def filter!(filters)
        node_list = filter(filters)
        Util::assert! node_list.any?, "Could not match any nodes from '#{filters}'"
        return node_list
      end

      #
      # returns a single Config::Object that corresponds to a Node.
      #
      def node(name)
        nodes[name]
      end

      #
      # yields each node, in sorted order
      #
      def each_node(&block)
        nodes.each_node &block
      end

      private

      def load_all_json(pattern)
        results = Config::ObjectList.new
        Dir.glob(pattern).each do |filename|
          obj = load_json(filename)
          if obj
            name = File.basename(filename).sub(/\.json$/,'')
            obj['name'] ||= name
            results[name] = obj
          end
        end
        results
      end

      def load_json(filename)
        if !File.exists?(filename)
          return Config::Object.new(self)
        end

        log :loading, filename, 2

        #
        # read file, strip out comments
        # (File.read(filename) would be faster, but we like ability to have comments)
        #
        buffer = StringIO.new
        File.open(filename) do |f|
          while (line = f.gets)
            next if line =~ /^\s*#/
            buffer << line
          end
        end

        # parse json
        begin
          hash = JSON.parse(buffer.string, :object_class => Hash, :array_class => Array) || {}
        rescue SyntaxError => exc
          log 0, :error, 'in file "%s":' % filename
          log 0, exc.to_s, :indent => 1
          return nil
        end
        object = Config::Object.new(self)
        object.deep_merge!(hash)
        return object
      end

      #
      # remove all the nesting from a hash.
      #
      # def flatten_hash(input = {}, output = {}, options = {})
      #   input.each do |key, value|
      #     key = options[:prefix].nil? ? "#{key}" : "#{options[:prefix]}#{options[:delimiter]||"_"}#{key}"
      #     if value.is_a? Hash
      #       flatten_hash(value, output, :prefix => key, :delimiter => options[:delimiter])
      #     else
      #       output[key]  = value
      #     end
      #   end
      #   output.replace(input)
      #   output
      # end

      #
      # makes a node inherit options from appropriate the common, service, and tag json files.
      #
      def apply_inheritance(node)
        new_node = Config::Object.new(self)
        name = node.name

        # inherit from common
        new_node.deep_merge!(@common)

        # inherit from services
        if node['services']
          node['services'].to_a.sort.each do |node_service|
            service = @services[node_service]
            if service.nil?
              log 0, :error, 'in node "%s": the service "%s" does not exist.' % [node['name'], node_service]
            else
              new_node.deep_merge!(service)
              service.node_list.add(name, new_node)
            end
          end
        end

        # inherit from tags
        if node.vagrant?
          node['tags'] = (node['tags'] || []).to_a + ['local']
        end
        if node['tags']
          node['tags'].to_a.sort.each do |node_tag|
            tag = @tags[node_tag]
            if tag.nil?
              log 0, :error, 'in node "%s": the tag "%s" does not exist.' % [node['name'], node_tag]
            else
              new_node.deep_merge!(tag)
              tag.node_list.add(name, new_node)
            end
          end
        end

        # inherit from node
        new_node.deep_merge!(node)
        return new_node
      end

      #
      # returns a set of nodes corresponding to a single name, where name could be a node name, service name, or tag name.
      #
      def nodes_for_name(name)
        if node = self.nodes[name]
          Config::ObjectList.new(node)
        elsif service = self.services[name]
          service.node_list
        elsif tag = self.tags[name]
          tag.node_list
        else
          {}
        end
      end

      #
      # TODO: apply JSON spec
      #
      PRIVATE_IP_RANGES = /(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/
      def validate_provider(provider)
        Util::assert! provider.vagrant.network =~ PRIVATE_IP_RANGES do
          log 0, :error, 'in provider.json: vagrant.network is not a local private network'
        end
      end

    end
  end
end