class Miam::Driver
  include Miam::Logger::Helper

  MAX_POLICY_SIZE = 2048
  MAX_POLICY_VERSIONS = 5

  def initialize(iam, sts, options = {})
    @iam = iam
    @sts = sts
    @options = options
    @account_id = nil
  end

  def create_user(user_name, attrs)
    log(:info, "Create User `#{user_name}`", :color => :cyan)

    unless_dry_run do
      params = {:user_name => user_name}
      params[:path] = attrs[:path] if attrs[:path]
      @iam.create_user(params)
    end

    new_user_attrs = {:groups => [], :policies => {}, :attached_managed_policies => []}
    new_user_attrs[:path] = attrs[:path] if attrs[:path]
    new_user_attrs
  end

  def create_access_key(user_name)
    log(:info, "Create access key for User `#{user_name}`", :color => :cyan)
    access_key = nil

    unless_dry_run do
      resp = @iam.create_access_key(:user_name => user_name)

      access_key = {
        :access_key_id => resp.access_key.access_key_id,
        :secret_access_key => resp.access_key.secret_access_key,
      }
    end

    access_key
  end

  def delete_user(user_name, attrs)
    log(:info, "Delete User `#{user_name}`", :color => :red)

    unless_dry_run do
      if attrs[:login_profile]
        @iam.delete_login_profile(:user_name => user_name)
      end

      attrs[:policies].keys.each do |policy_name|
        @iam.delete_user_policy(:user_name => user_name, :policy_name => policy_name)
      end

      attrs[:groups].each do |group_name|
        @iam.remove_user_from_group(:group_name => group_name, :user_name => user_name)
      end

      attrs[:attached_managed_policies].each do |policy_arn|
        @iam.detach_user_policy(:user_name => user_name, :policy_arn => policy_arn)
      end

      list_access_key_ids(user_name).each do |access_key_id|
        @iam.delete_access_key(:user_name => user_name, :access_key_id => access_key_id)
      end

      list_signing_certificate_ids(user_name).each do |certificate_id|
        @iam.delete_signing_certificate(:user_name => user_name, :certificate_id => certificate_id)
      end

      mfa_devices = @iam.list_mfa_devices(:user_name => user_name).map {|resp|
        resp.mfa_devices
      }.flatten

      mfa_devices.each do |md|
        @iam.deactivate_mfa_device(:user_name => user_name, :serial_number => md.serial_number)
      end

      @iam.delete_user(:user_name => user_name)
    end
  end

  def create_login_profile(user_name, attrs)
    log_attrs = attrs.dup
    log_attrs.delete(:password)

    log(:info, "Update User `#{user_name}`", :color => :green)
    log(:info, "  create login profile: #{log_attrs.inspect}", :color => :green)

    unless_dry_run do
      @iam.create_login_profile(attrs.merge(:user_name => user_name))
    end
  end

  def delete_login_profile(user_name)
    log(:info, "Update User `#{user_name}`", :color => :green)
    log(:info, "  delete login profile", :color => :green)

    unless_dry_run do
      @iam.delete_login_profile(:user_name => user_name)
    end
  end

  def update_login_profile(user_name, attrs, old_attrs)
    log_attrs = attrs.dup
    log_attrs.delete(:password)

    log(:info, "Update User `#{user_name}`", :color => :green)
    log(:info, "  login profile:\n".green + Miam::Utils.diff(old_attrs, attrs, :color => @options[:color], :indent => '    '), :color => false)

    unless_dry_run do
      @iam.update_login_profile(attrs.merge(:user_name => user_name))
    end
  end

  def add_user_to_groups(user_name, group_names)
    log(:info, "Update User `#{user_name}`", :color => :green)
    log(:info, "  add groups=#{group_names.join(',')}", :color => :green)

    unless_dry_run do
      group_names.each do |group_name|
        @iam.add_user_to_group(:group_name => group_name, :user_name => user_name)
      end
    end
  end

  def remove_user_from_groups(user_name, group_names)
    log(:info, "Update User `#{user_name}`", :color => :green)
    log(:info, "  remove groups=#{group_names.join(',')}", :color => :green)

    unless_dry_run do
      group_names.each do |group_name|
        @iam.remove_user_from_group(:group_name => group_name, :user_name => user_name)
      end
    end
  end

  def create_group(group_name, attrs)
    log(:info, "Create Group `#{group_name}`", :color => :cyan)

    unless_dry_run do
      params = {:group_name => group_name}
      params[:path] = attrs[:path] if attrs[:path]
      @iam.create_group(params)
    end

    new_group_attrs = {:policies => {}, :attached_managed_policies => []}
    new_group_attrs[:path] = attrs[:path] if attrs[:path]
    new_group_attrs
  end

  def delete_group(group_name, attrs, users_in_group)
    log(:info, "Delete Group `#{group_name}`", :color => :red)

    unless_dry_run do
      attrs[:policies].keys.each do |policy_name|
        @iam.delete_group_policy(:group_name => group_name, :policy_name => policy_name)
      end

      users_in_group.each do |user_name|
        @iam.remove_user_from_group(:group_name => group_name, :user_name => user_name)
      end

      attrs[:attached_managed_policies].each do |policy_arn|
        @iam.detach_group_policy(:group_name => group_name, :policy_arn => policy_arn)
      end

      @iam.delete_group(:group_name => group_name)
    end
  end

  def create_role(role_name, attrs)
    log(:info, "Create Role `#{role_name}`", :color => :cyan)
    assume_role_policy_document = attrs.fetch(:assume_role_policy_document)

    unless_dry_run do
      params = {
        :role_name => role_name,
        :assume_role_policy_document => encode_document(assume_role_policy_document),
        :max_session_duration => attrs.fetch(:max_session_duration)
      }

      params[:path] = attrs[:path] if attrs[:path]
      @iam.create_role(params)
    end

    new_role_attrs = {
      :instance_profiles => [],
      :assume_role_policy_document => assume_role_policy_document,
      :policies => {},
      :attached_managed_policies => [],
      :max_session_duration => attrs.fetch(:max_session_duration),
    }

    new_role_attrs[:path] = attrs[:path] if attrs[:path]
    new_role_attrs
  end

  def delete_role(role_name, instance_profile_names, attrs)
    log(:info, "Delete Role `#{role_name}`", :color => :red)

    unless_dry_run do
      attrs[:policies].keys.each do |policy_name|
        @iam.delete_role_policy(:role_name => role_name, :policy_name => policy_name)
      end

      instance_profile_names.each do |instance_profile_name|
        @iam.remove_role_from_instance_profile(:instance_profile_name => instance_profile_name, :role_name => role_name)
      end

      attrs[:attached_managed_policies].each do |policy_arn|
        @iam.detach_role_policy(:role_name => role_name, :policy_arn => policy_arn)
      end

      @iam.delete_role(:role_name => role_name)
    end
  end

  def add_role_to_instance_profiles(role_name, instance_profile_names)
    log(:info, "Update Role `#{role_name}`", :color => :green)
    log(:info, "  add instance_profiles=#{instance_profile_names.join(',')}", :color => :green)

    unless_dry_run do
      instance_profile_names.each do |instance_profile_name|
        @iam.add_role_to_instance_profile(:instance_profile_name => instance_profile_name, :role_name => role_name)
      end
    end
  end

  def remove_role_from_instance_profiles(role_name, instance_profile_names)
    log(:info, "Update Role `#{role_name}`", :color => :green)
    log(:info, "  remove instance_profiles=#{instance_profile_names.join(',')}", :color => :green)

    unless_dry_run do
      instance_profile_names.each do |instance_profile_name|
        @iam.remove_role_from_instance_profile(:instance_profile_name => instance_profile_name, :role_name => role_name)
      end
    end
  end

  def update_role_settings(role_name, new_settings, old_settings)
    log(:info, "Update Role `#{role_name}` > Settings", :color => :green)
    log(:info, Miam::Utils.diff(old_settings, new_settings, :color => @options[:color]), :color => false)
    unless_dry_run do
      @iam.update_role(new_settings.merge(role_name: role_name))
    end
  end

  def update_assume_role_policy(role_name, policy_document, old_policy_document)
    log(:info, "Update Role `#{role_name}` > AssumeRolePolicy", :color => :green)
    log(:info, Miam::Utils.diff(old_policy_document, policy_document, :color => @options[:color]), :color => false)

    unless_dry_run do
      @iam.update_assume_role_policy(
        :role_name => role_name,
        :policy_document => encode_document(policy_document),
      )
    end
  end

  def create_instance_profile(instance_profile_name, attrs)
    log(:info, "Create InstanceProfile `#{instance_profile_name}`", :color => :cyan)

    unless_dry_run do
      params = {:instance_profile_name => instance_profile_name}
      params[:path] = attrs[:path] if attrs[:path]
      @iam.create_instance_profile(params)
    end

    new_instance_profile_attrs = {}
    new_instance_profile_attrs[:path] = attrs[:path] if attrs[:path]
    new_instance_profile_attrs
  end

  def delete_instance_profile(instance_profile_name, attrs, roles_in_instance_profile)
    log(:info, "Delete InstanceProfile `#{instance_profile_name}`", :color => :red)

    unless_dry_run do
      roles_in_instance_profile.each do |role_name|
        @iam.remove_role_from_instance_profile(:instance_profile_name => instance_profile_name, :role_name => role_name)
      end

      @iam.delete_instance_profile(:instance_profile_name => instance_profile_name)
    end
  end

  def update_name(type, user_or_group_name, new_name)
    log(:info, "Update #{Miam::Utils.camelize(type.to_s)} `#{user_or_group_name}`", :color => :green)
    log(:info, "  name:\n".green + Miam::Utils.diff(user_or_group_name, new_name, :color => @options[:color], :indent => '    '), :color => false)
    update_user_or_group(type, user_or_group_name, "new_#{type}_name".to_sym => new_name)
  end

  def update_path(type, user_or_group_name, new_path, old_path)
    log(:info, "Update #{Miam::Utils.camelize(type.to_s)} `#{user_or_group_name}`", :color => :green)
    log(:info, "  path:\n".green + Miam::Utils.diff(old_path, new_path, :color => @options[:color], :indent => '    '), :color => false)
    update_user_or_group(type, user_or_group_name, :new_path => new_path)
  end

  def update_user_or_group(type, user_or_group_name, params)
    unless_dry_run do
      params["#{type}_name".to_sym] = user_or_group_name
      @iam.send("update_#{type}", params)
    end
  end

  def create_policy(type, user_or_group_name, policy_name, policy_document)
    log(:info, "Create #{Miam::Utils.camelize(type.to_s)} `#{user_or_group_name}` > Policy `#{policy_name}`", :color => :cyan)
    log(:info, "  #{policy_document.pretty_inspect.gsub("\n", "\n  ").strip}", :color => :cyan)
    put_policy(type, user_or_group_name, policy_name, policy_document)
  end

  def update_policy(type, user_or_group_name, policy_name, policy_document, old_policy_document)
    log(:info, "Update #{Miam::Utils.camelize(type.to_s)} `#{user_or_group_name}` > Policy `#{policy_name}`", :color => :green)
    log(:info, Miam::Utils.diff(old_policy_document, policy_document, :color => @options[:color]), :color => false)
    put_policy(type, user_or_group_name, policy_name, policy_document)
  end

  def delete_policy(type, user_or_group_name, policy_name)
    logmsg = "Delete #{Miam::Utils.camelize(type.to_s)} `#{user_or_group_name}` > Policy `#{policy_name}`"
    log(:info, logmsg, :color => :red)

    unless_dry_run do
      params = {:policy_name => policy_name}
      params["#{type}_name".to_sym] = user_or_group_name
      @iam.send("delete_#{type}_policy", params)
    end
  end

  def put_policy(type, user_or_group_name, policy_name, policy_document)
    unless_dry_run do
      params = {
        :policy_name => policy_name,
        :policy_document => encode_document(policy_document),
      }

      params["#{type}_name".to_sym] = user_or_group_name
      @iam.send("put_#{type}_policy", params)
    end
  end

  def attach_policies(type, name, policies)
    type = type.to_s
    type_s = type.slice(0, 1).upcase + type.slice(1..-1)

    log(:info, "Update #{type_s} `#{name}`", :color => :green)
    log(:info, "  attach policies=#{policies.join(',')}", :color => :green)

    unless_dry_run do
      policies.each do |arn|
        @iam.send("attach_#{type}_policy", :"#{type}_name" => name, :policy_arn => arn)
      end
    end
  end

  def detach_policies(type, name, policies)
    type = type.to_s
    type_s = type.slice(0, 1).upcase + type.slice(1..-1)

    log(:info, "Update #{type_s} `#{name}`", :color => :green)
    log(:info, "  detach policies=#{policies.join(',')}", :color => :red)

    unless_dry_run do
      policies.each do |arn|
        @iam.send("detach_#{type}_policy", :"#{type}_name" => name, :policy_arn => arn)
      end
    end
  end

  def list_access_key_ids(user_name)
    @iam.list_access_keys(:user_name => user_name).map {|resp|
      resp.access_key_metadata.map do |metadata|
        metadata.access_key_id
      end
    }.flatten
  end

  def list_signing_certificate_ids(user_name)
    @iam.list_signing_certificates(:user_name => user_name).map {|resp|
      resp.certificates.map do |cert|
        cert.certificate_id
      end
    }.flatten
  end

  def create_managed_policy(policy_name, attrs)
    log(:info, "Create ManagedPolicy `#{policy_name}`", :color => :cyan)

    unless_dry_run do
      params = {
        :policy_name => policy_name,
        :path => attrs[:path],
        :policy_document => encode_document(attrs[:document]),
      }

      @iam.create_policy(params)
    end
  end

  def delete_managed_policy(policy_name, policy_path)
    log(:info, "Delete ManagedPolicy `#{policy_name}`", :color => :red)

    unless_dry_run do
      policy_versions = @iam.list_policy_versions(
        :policy_arn => policy_arn(policy_name, policy_path),
        :max_items => MAX_POLICY_VERSIONS
      )

      policy_versions.versions.reject {|pv|
        pv.is_default_version
      }.each {|pv|
        @iam.delete_policy_version(
          :policy_arn => policy_arn(policy_name, policy_path),
          :version_id => pv.version_id
        )
      }

      @iam.delete_policy(
        :policy_arn => policy_arn(policy_name, policy_path)
      )
    end
  end

  def update_managed_policy(policy_name, policy_path, policy_document, old_policy_document)
    log(:info, "Update ManagedPolicy `#{policy_name}`", :color => :green)
    log(:info, Miam::Utils.diff(old_policy_document, policy_document, :color => @options[:color]), :color => false)

    unless_dry_run do
      policy_versions = @iam.list_policy_versions(
        :policy_arn => policy_arn(policy_name, policy_path),
        :max_items => MAX_POLICY_VERSIONS
      )

      if policy_versions.versions.length >= MAX_POLICY_VERSIONS
        delete_policy_version = policy_versions.versions.reject {|pv|
          pv.is_default_version
        }.sort_by {|pv| pv.version_id[1..-1].to_i }.first

        @iam.delete_policy_version(
          :policy_arn => policy_arn(policy_name, policy_path),
          :version_id => delete_policy_version.version_id
        )
      end

      @iam.create_policy_version(
        :policy_arn => policy_arn(policy_name, policy_path),
        :policy_document => encode_document(policy_document),
        set_as_default: true
      )
    end
  end

  def password_policy
    return @password_policy if instance_variable_defined?(:@password_policy)

    @password_policy = @iam.get_account_password_policy.password_policy
  rescue Aws::IAM::Errors::NoSuchEntity
    @password_policy = nil
  end

  private

  def encode_document(policy_document)
    if @options[:disable_form_json]
      JSON.dump(policy_document)
    else
      encoded = JSON.pretty_generate(policy_document)

      if Miam::Utils.bytesize(encoded) > MAX_POLICY_SIZE
        encoded = JSON.pretty_generate(policy_document)
        encoded = encoded.gsub(/^\s+/m, '').strip
      end

      if Miam::Utils.bytesize(encoded) > MAX_POLICY_SIZE
        encoded = JSON.dump(policy_document)
      end

      encoded
    end
  end

  def unless_dry_run
    yield unless @options[:dry_run]
  end

  def account_id
    # https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
    # http://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
    @account_id ||= @sts.get_caller_identity.account
  end

  def policy_arn(policy_name, policy_path)
    File.join("arn:aws:iam::#{account_id}:policy", policy_path, policy_name)
  end
end