# Copyright 2023 Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "time"
require "googleauth/external_account/base_credentials"
require "googleauth/external_account/external_account_utils"

module Google
  # Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
  module Auth
    module ExternalAccount
      # This module handles the retrieval of credentials from Google Cloud by utilizing the any 3PI
      # provider then exchanging the credentials for a short-lived Google Cloud access token.
      class IdentityPoolCredentials
        include Google::Auth::ExternalAccount::BaseCredentials
        include Google::Auth::ExternalAccount::ExternalAccountUtils
        extend CredentialsLoader

        # Will always be nil, but method still gets used.
        attr_reader :client_id

        # Initialize from options map.
        #
        # @param [string] audience
        # @param [hash{symbol => value}] credential_source
        #     credential_source is a hash that contains either source file or url.
        #     credential_source_format is either text or json. To define how we parse the credential response.
        #
        def initialize options = {}
          base_setup options

          @audience = options[:audience]
          @credential_source = options[:credential_source] || {}
          @credential_source_file = @credential_source[:file]
          @credential_source_url = @credential_source[:url]
          @credential_source_headers = @credential_source[:headers] || {}
          @credential_source_format = @credential_source[:format] || {}
          @credential_source_format_type = @credential_source_format[:type] || "text"
          validate_credential_source
        end

        # Implementation of BaseCredentials retrieve_subject_token!
        def retrieve_subject_token!
          content, resource_name = token_data
          if @credential_source_format_type == "text"
            token = content
          else
            begin
              response_data = MultiJson.load content, symbolize_keys: true
              token = response_data[@credential_source_field_name.to_sym]
            rescue StandardError
              raise "Unable to parse subject_token from JSON resource #{resource_name} " \
                    "using key #{@credential_source_field_name}"
            end
          end
          raise "Missing subject_token in the credential_source file/response." unless token
          token
        end

        private

        def validate_credential_source
          # `environment_id` is only supported in AWS or dedicated future external account credentials.
          unless @credential_source[:environment_id].nil?
            raise "Invalid Identity Pool credential_source field 'environment_id'"
          end
          unless ["json", "text"].include? @credential_source_format_type
            raise "Invalid credential_source format #{@credential_source_format_type}"
          end
          # for JSON types, get the required subject_token field name.
          @credential_source_field_name = @credential_source_format[:subject_token_field_name]
          if @credential_source_format_type == "json" && @credential_source_field_name.nil?
            raise "Missing subject_token_field_name for JSON credential_source format"
          end
          # check file or url must be fulfilled and mutually exclusiveness.
          if @credential_source_file && @credential_source_url
            raise "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
          end
          return unless (@credential_source_file || @credential_source_url).nil?
          raise "Missing credential_source. A 'file' or 'url' must be provided."
        end

        def token_data
          @credential_source_file.nil? ? url_data : file_data
        end

        def file_data
          raise "File #{@credential_source_file} was not found." unless File.exist? @credential_source_file
          content = File.read @credential_source_file, encoding: "utf-8"
          [content, @credential_source_file]
        end

        def url_data
          begin
            response = connection.get @credential_source_url do |req|
              req.headers.merge! @credential_source_headers
            end
          rescue Faraday::Error => e
            raise "Error retrieving from credential url: #{e}"
          end
          raise "Unable to retrieve Identity Pool subject token #{response.body}" unless response.success?
          [response.body, @credential_source_url]
        end
      end
    end
  end
end