require_relative "acl_path"

module ChefZero
  module ChefData
    #
    # The DefaultCreator creates default values when you ask for them.
    # - It relies on created and deleted being called when things get
    #   created and deleted, so that it knows the owners of said objects
    #   and knows to eliminate default values on delete.
    # - get, list and exists? get data.
    #
    class DefaultCreator
      def initialize(data, single_org, osc_compat, superusers = nil)
        @data = data
        @single_org = single_org
        @osc_compat = osc_compat
        @superusers = superusers || DEFAULT_SUPERUSERS
        clear
      end

      attr_reader :data
      attr_reader :single_org
      attr_reader :osc_compat
      attr_reader :creators
      attr_writer :deleted

      PERMISSIONS = %w{create read update delete grant}.freeze
      DEFAULT_SUPERUSERS = %w{pivotal}.freeze

      def clear
        @creators = { [] => @superusers }
        @deleted = {}
      end

      def deleted(path)
        # acl deletes mean nothing, they are entirely subservient to their
        # parent object
        if path[0] == "acls" || (path[0] == "organizations" && path[2] == "acls")
          return false
        end

        result = exists?(path)
        @deleted[path] = true
        result
      end

      def deleted?(path)
        1.upto(path.size) do |index|
          return true if @deleted[path[0..-index]]
        end
        false
      end

      def created(path, creator, create_parents)
        # If a parent has been deleted, we will need to clear that.
        deleted_index = nil
        0.upto(path.size - 1) do |index|
          deleted_index = index if @deleted[path[0..index]]
        end

        # Walk up the tree, setting the creator on anything that doesn't exist
        # (anything that is either deleted or was never created)
        while (deleted_index && path.size > deleted_index) || !@creators[path]
          @creators[path] = creator ? [ creator ] : []
          @deleted.delete(path)
          # Only do this once if create_parents is false
          break if !create_parents || path.size == 0

          path = path[0..-2]
        end
      end

      def superusers
        @creators[[]]
      end

      def get(path)
        return nil if deleted?(path)

        result = case path[0]
                 when "acls"
                   # /acls/*
                   object_path = AclPath.get_object_path(path)
                   if data_exists?(object_path)
                     default_acl(path)
                   end

                 when "containers"
                   if path.size == 2 && exists?(path)
                     {}
                   end

                 when "users"
                   if path.size == 2 && data.exists?(path)
                     # User is empty user
                     {}
                   end

                 when "organizations"
                   if path.size >= 2
                     # /organizations/*/**
                     if data.exists_dir?(path[0..1])
                       get_org_default(path)
                     end
                   end
                 end

        result
      end

      def list(path)
        return nil if deleted?(path)

        if path.size == 0
          return %w{containers users organizations acls}
        end

        case path[0]
        when "acls"
          if path.size == 1
            [ "root" ] + (data.list(path + [ "containers" ]) - [ "organizations" ])
          else
            data.list(AclPath.get_object_path(path))
          end

        when "containers"
          %w{containers users organizations}

        when "users"
          superusers

        when "organizations"
          if path.size == 1
            single_org ? [ single_org ] : []
          elsif path.size >= 2 && data.exists_dir?(path[0..1])
            list_org_default(path)
          end
        end
      end

      def exists?(path)
        return true if path.size == 0

        parent_list = list(path[0..-2])
        parent_list && parent_list.include?(path[-1])
      end

      protected

      DEFAULT_ORG_SPINE = {
        "clients" => {},
        "cookbook_artifacts" => {},
        "cookbooks" => {},
        "data" => {},
        "environments" => %w{_default},
        "file_store" => {
          "checksums" => {},
        },
        "nodes" => {},
        "policies" => {},
        "policy_groups" => {},
        "roles" => {},
        "sandboxes" => {},
        "users" => {},

        "org" => {},
        "containers" => %w{clients containers cookbook_artifacts cookbooks data environments groups nodes policies policy_groups roles sandboxes},
        "groups" => %w{admins billing-admins clients users},
        "association_requests" => {},
      }.freeze

      def list_org_default(path)
        if path.size >= 3 && path[2] == "acls"
          if path.size == 3
            # /organizations/ORG/acls
            return [ "root" ] + data.list(path[0..1] + [ "containers" ])
          elsif path.size == 4
            # /organizations/ORG/acls/TYPE
            return data.list(path[0..1] + [ path[3] ])
          else
            return nil
          end
        end

        value = DEFAULT_ORG_SPINE
        2.upto(path.size - 1) do |index|
          value = nil if @deleted[path[0..index]]
          break unless value

          value = value[path[index]]
        end

        result = if value.is_a?(Hash)
                   value.keys
                 elsif value
                   value
                 end

        if path.size == 3
          if path[2] == "clients"
            result << "#{path[1]}-validator"
            if osc_compat
              result << "#{path[1]}-webui"
            end
          elsif path[2] == "users"
            if osc_compat
              result << "admin"
            end
          end
        end

        result
      end

      def get_org_default(path)
        if path[2] == "acls"
          get_org_acl_default(path)

        elsif path.size >= 4
          if path[2] == "containers" && path.size == 4
            if exists?(path)
              return {}
            else
              return nil
            end
          end

          # /organizations/(*)/clients/\1-validator
          # /organizations/*/environments/_default
          # /organizations/*/groups/{admins,billing-admins,clients,users}
          case path[2..-1].join("/")
          when "clients/#{path[1]}-validator"
            { "validator" => "true" }

          when "clients/#{path[1]}-webui", "users/admin"
            if osc_compat
              { "admin" => "true" }
            end

          when "environments/_default"
            { "description" => "The default Chef environment" }

          when "groups/admins"
            admins = data.list(path[0..1] + [ "users" ]).select do |name|
              user = FFI_Yajl::Parser.parse(data.get(path[0..1] + [ "users", name ]))
              user["admin"]
            end
            admins += data.list(path[0..1] + [ "clients" ]).select do |name|
              client = FFI_Yajl::Parser.parse(data.get(path[0..1] + [ "clients", name ]))
              client["admin"]
            end
            admins += @creators[path[0..1]] if @creators[path[0..1]]
            { "actors" => admins.uniq }

          when "groups/billing-admins"
            {}

          when "groups/clients"
            { "clients" => data.list(path[0..1] + [ "clients" ]) }

          when "groups/users"
            users = data.list(path[0..1] + [ "users" ])
            users |= @creators[path[0..1]] if @creators[path[0..1]]
            { "users" => users }

          when "org"
            {}

          end
        end
      end

      def get_org_acl_default(path)
        object_path = AclPath.get_object_path(path)
        # The actual things containers correspond to don't have to exist, as
        # long as the container does
        return nil unless data_exists?(object_path)

        basic_acl =
          case path[3..-1].join("/")
          when "root", "containers/containers", "containers/groups"
            {
              "create" => { "groups" => %w{admins} },
              "read" => { "groups" => %w{admins users} },
              "update" => { "groups" => %w{admins} },
              "delete" => { "groups" => %w{admins} },
              "grant" => { "groups" => %w{admins} },
            }
          when "containers/environments", "containers/roles",
            "containers/policy_groups", "containers/policies",
            "containers/cookbooks", "containers/cookbook_artifacts",
            "containers/data"
            {
              "create" => { "groups" => %w{admins users} },
              "read" => { "groups" => %w{admins users clients} },
              "update" => { "groups" => %w{admins users} },
              "delete" => { "groups" => %w{admins users} },
              "grant" => { "groups" => %w{admins} },
            }
          when "containers/nodes"
            {
              "create" => { "groups" => %w{admins users clients} },
              "read" => { "groups" => %w{admins users clients} },
              "update" => { "groups" => %w{admins users} },
              "delete" => { "groups" => %w{admins users} },
              "grant" => { "groups" => %w{admins} },
            }
          when "containers/clients"
            {
              "create" => { "groups" => %w{admins} },
              "read" => { "groups" => %w{admins users} },
              "update" => { "groups" => %w{admins} },
              "delete" => { "groups" => %w{admins users} },
              "grant" => { "groups" => %w{admins} },
            }
          when "containers/sandboxes"
            {
              "create" => { "groups" => %w{admins users} },
              "read" => { "groups" => %w{admins} },
              "update" => { "groups" => %w{admins} },
              "delete" => { "groups" => %w{admins} },
              "grant" => { "groups" => %w{admins} },
            }
          when "groups/admins", "groups/clients", "groups/users"
            {
              "create" => { "groups" => %w{admins} },
              "read" => { "groups" => %w{admins} },
              "update" => { "groups" => %w{admins} },
              "delete" => { "groups" => %w{admins} },
              "grant" => { "groups" => %w{admins} },
            }
          when "groups/billing-admins"
            {
              "create" => { "groups" => %w{} },
              "read" => { "groups" => %w{billing-admins} },
              "update" => { "groups" => %w{billing-admins} },
              "delete" => { "groups" => %w{} },
              "grant" => { "groups" => %w{} },
            }
          else
            {}
          end

        default_acl(path, basic_acl)
      end

      def get_owners(acl_path)
        unknown_owners = []
        path = AclPath.get_object_path(acl_path)
        if path

          # Non-validator clients own themselves.
          if path.size == 4 && path[0] == "organizations" && path[2] == "clients"
            begin
              client = FFI_Yajl::Parser.parse(data.get(path))
              unless client["validator"]
                unknown_owners |= [ path[3] ]
              end
            rescue
              unknown_owners |= [ path[3] ]
            end

            # Add creators as owners (except any validator clients).
            if @creators[path]
              @creators[path].each do |creator|
                begin
                  client = FFI_Yajl::Parser.parse(data.get(path[0..2] + [ creator ]))
                  next if client["validator"]
                rescue
                end
                unknown_owners |= [ creator ]
              end
            end
          else
            unknown_owners |= @creators[path] if @creators[path]
          end
          owners = filter_owners(path, unknown_owners)

          # ANGRY
          # Non-default containers do not get superusers added to them,
          # because reasons.
          unless path.size == 4 && path[0] == "organizations" && path[2] == "containers" && !exists?(path)
            owners[:users] += superusers
          end
        else
          owners = { clients: [], users: [] }
        end

        owners[:users].uniq!
        owners[:clients].uniq!
        owners
      end

      # Figures out if an  object was created by a user or client.
      # If the object does not exist in the context
      # of an organization, it can only be a user
      #
      # This isn't perfect, because we are never explicitly told
      # if a requestor creating an object is a user or client -
      # but it gets us reasonably close
      def filter_owners(path, unknown_owners)
        owners = { clients: [], users: [] }
        unknown_owners.each do |entity|
          if path[0] == "organizations" && path.length > 2
            begin
              data.get(["organizations", path[1], "clients", entity])
              owners[:clients] |= [ entity ]
            rescue
              owners[:users] |= [ entity ]
            end
          else
            owners[:users] |= [ entity ]
          end
        end
        owners
      end

      def default_acl(acl_path, acl = {})
        owners = get_owners(acl_path)
        container_acl = nil
        PERMISSIONS.each do |perm|
          acl[perm] ||= {}
          acl[perm]["users"] = owners[:users]
          acl[perm]["clients"] = owners[:clients]
          acl[perm]["groups"] ||= begin
            # When we create containers, we don't merge groups (not sure why).
            if acl_path[0] == "organizations" && acl_path[3] == "containers"
              []
            else
              container_acl ||= get_container_acl(acl_path) || {}
              (container_acl[perm] ? container_acl[perm]["groups"] : []) || []
            end
          end
          acl[perm]["actors"] = acl[perm]["clients"] + acl[perm]["users"]
        end
        acl
      end

      def get_container_acl(acl_path)
        parent_path = AclPath.parent_acl_data_path(acl_path)
        if parent_path
          FFI_Yajl::Parser.parse(data.get(parent_path))
        else
          nil
        end
      end

      def data_exists?(path)
        if is_dir?(path)
          data.exists_dir?(path)
        else
          data.exists?(path)
        end
      end

      def is_dir?(path)
        case path.size
        when 0, 1
          true
        when 2
          path[0] == "organizations" || (path[0] == "acls" && path[1] != "root")
        when 3
          # If it has a container, it is a directory.
          path[0] == "organizations" &&
            (path[2] == "acls" || data.exists?(path[0..1] + [ "containers", path[2] ]))
        when 4
          path[0] == "organizations" && (
            (path[2] == "acls" && path[1] != "root") ||
            %w{cookbooks cookbook_artifacts data policies policy_groups}.include?(path[2]))
        else
          false
        end
      end
    end
  end
end
