module ActiveSambaLdap
  module Populate
    def self.included(base)
      base.extend(ClassMethods)
    end

    module ClassMethods
      def populate(options={})
        Private.new(self, options).populate
      end

      def purge(options={})
        self.delete_all(nil, {:scope => :sub}.merge(options))
      end

      class Private
        def initialize(base, options)
          @base = base
          @options = options.dup
        end

        def populate
          init_classes
          init_options

          entries = []
          entries.concat(ensure_base)
          entries.concat(ensure_group_base)
          entries.concat(ensure_user_base)
          entries.concat(ensure_computer_base)
          entries.concat(ensure_idmap_base)
          entries.concat(make_groups)
          entries.concat(make_users)
          entries.concat(make_pool)

          [entries, @options]
        end

        def init_classes
          @options[:user_class] = user_class = Class.new(User)
          @options[:group_class] = group_class = Class.new(Group)
          @options[:computer_class] = computer_class = Class.new(Computer)
          @options[:idmap_class] = idmap_class = Class.new(Idmap)
          @options[:unix_id_pool_class] = id_pool_class = Class.new(UnixIdPool)

          user_class.ldap_mapping
          group_class.ldap_mapping
          computer_class.ldap_mapping
          idmap_class.ldap_mapping
          id_pool_class.ldap_mapping

          user_class.set_associated_class(:primary_group, group_class)
          computer_class.set_associated_class(:primary_group, group_class)
          user_class.set_associated_class(:groups, group_class)
          computer_class.set_associated_class(:groups, group_class)

          group_class.set_associated_class(:users, user_class)
          group_class.set_associated_class(:computers, computer_class)
          group_class.set_associated_class(:primary_users, user_class)
          group_class.set_associated_class(:primary_computers, computer_class)
        end

        def user_class
          @options[:user_class]
        end

        def group_class
          @options[:group_class]
        end

        def computer_class
          @options[:computer_class]
        end

        def idmap_class
          @options[:idmap_class]
        end

        def init_options
          config = @base.configuration
          @options[:start_uid] ||= Integer(config[:start_uid])
          @options[:start_gid] ||= Integer(config[:start_gid])
          @options[:administrator] ||= user_class::DOMAIN_ADMIN_NAME
          @options[:administrator_uid] ||=
            user_class.rid2uid(user_class::DOMAIN_ADMIN_RID)
          @options[:administrator_gid] ||=
            group_class.rid2gid(group_class::DOMAIN_ADMINS_RID)
          @options[:guest] ||= user_class::DOMAIN_GUEST_NAME
          @options[:guest_uid] ||=
            user_class.rid2uid(user_class::DOMAIN_GUEST_RID)
          @options[:guest_gid] ||=
            group_class.rid2gid(group_class::DOMAIN_GUESTS_RID)
          @options[:default_user_gid] ||= config[:default_user_gid]
          @options[:default_computer_gid] ||= config[:default_computer_gid]
        end

        def ensure_container_base(dn, target_name, klass, ignore_base=false)
          dn = dn.to_s if dn.is_a?(ActiveLdap::DN)
          entries = []
          suffixes = []
          dn.split(/,/).reverse_each do |suffix|
            name, value = suffix.split(/=/, 2)
            next unless name == target_name
            container_class = Class.new(klass)
            prefix = suffixes.reverse.join(",")
            suffixes << suffix
            if ignore_base
              container_class.ldap_mapping :prefix => "", :scope => :base
              container_class.base = prefix
            else
              container_class.ldap_mapping :prefix => prefix, :scope => :base
            end
            next if container_class.exists?(value, :prefix => suffix)
            container = container_class.new(value)
            yield(container) if block_given?
            begin
              container.save!
            rescue ActiveLdap::OperationNotPermitted
            end
            entries << container
          end
          entries
        end

        def ensure_base
          ensure_container_base(@base.base, "dc", Dc, true) do |dc|
            dc.o = dc.dc
          end
        end

        def ensure_ou_base(dn)
          ensure_container_base(dn, "ou", Ou)
        end

        def ensure_user_base
          ensure_ou_base(user_class.prefix)
        end

        def ensure_group_base
          ensure_ou_base(group_class.prefix)
        end

        def ensure_computer_base
          ensure_ou_base(computer_class.prefix)
        end

        def ensure_idmap_base
          ensure_ou_base(idmap_class.prefix)
        end

        def make_user(user_class, name, uid, group)
          if user_class.exists?(name)
            user = user_class.find(name)
            group = nil
          else
            user = user_class.new(name)
            user.fill_default_values("uid_number" => uid, "group" => group)
            user.save!
            group.users << user
          end
          [user, group]
        end

        def make_users
          user_class = @options[:user_class]
          group_class = @options[:group_class]
          entries = []
          [
           [@options[:administrator], @options[:administrator_uid],
            @options[:administrator_gid]],
           [@options[:guest], @options[:guest_uid], @options[:guest_gid]],
          ].each do |name, uid, gid|
            user, group = make_user(user_class, name, uid,
                                    group_class.find_by_gid_number(gid))
            entries << user
            if group
              old_group = entries.find do |entry|
                entry.is_a?(group_class) and entry.cn == group.cn
              end
              index = entries.index(old_group)
              if index
                entries[index] = group
              else
                entries << group
              end
            end
          end
          entries
        end

        def make_group(group_class, name, gid, description=nil, type=nil)
          if group_class.exists?(name)
            group = group_class.find(name)
          else
            group = group_class.new(name)
            group.change_type(type || "domain")
            group.display_name = name
            group.description = name || description
            group.change_gid_number(gid)

            group.save!
          end
          group
        end

        def make_groups
          entries = []
          [
           ["Domain Admins", @options[:administrator_gid],
            "Netbios Domain Administrators"],
           ["Domain Users", @options[:default_user_gid],
            "Netbios Domain Users"],
           ["Domain Guests", @options[:guest_gid],
            "Netbios Domain Guest Users"],
           ["Domain Computers", @options[:default_computer_gid],
            "Netbios Domain Computers"],
           ["Administrators", nil, nil, "builtin",
            group_class::LOCAL_ADMINS_RID],
           ["Users", nil, nil, "builtin", group_class::LOCAL_USERS_RID],
           ["Guests", nil, nil, "builtin", group_class::LOCAL_GUESTS_RID],
           ["Power Users", nil, nil, "builtin",
            group_class::LOCAL_POWER_USERS_RID],
           ["Account Operators", nil, nil, "builtin",
            group_class::LOCAL_ACCOUNT_OPERATORS_RID],
           ["System Operators", nil, nil, "builtin",
            group_class::LOCAL_SYSTEM_OPERATORS_RID],
           ["Print Operators", nil, nil, "builtin",
            group_class::LOCAL_PRINT_OPERATORS_RID],
           ["Backup Operators", nil, nil, "builtin",
            group_class::LOCAL_BACKUP_OPERATORS_RID],
           ["Replicators", nil, nil, "builtin",
            group_class::LOCAL_REPLICATORS_RID],
          ].each do |name, gid, description, type, rid|
            gid ||= group_class.rid2gid(rid)
            entries << make_group(group_class, name, gid, description, type)
          end
          entries
        end

        def make_pool
          config = @base.configuration
          klass = @options[:unix_id_pool_class]
          name = config[:samba_domain]
          if klass.exists?(name)
            pool = klass.find(name)
          else
            pool = klass.new(name)
            pool.samba_sid = config[:sid]
            pool.uid_number = @options[:start_uid]
            pool.gid_number = @options[:start_gid]
            pool.save!
          end
          [pool]
        end
      end
    end
  end
end
