defmodule ExAws.InstanceMeta do
  @moduledoc false

  # Provides access to the AWS Instance MetaData
  # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html

  # AWS InstanceMetaData URL
  @meta_path_root "http://169.254.169.254/latest/meta-data"

  # Endpoint for ECS tasks role credentials
  # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
  @task_role_root "http://169.254.170.2"

  def request(config, url, fallback \\ false) do
    # If we're using IMDSv2, we will need to pass in session token headers.
    headers = get_request_headers(config, fallback)

    case config.http_client.request(:get, url, "", headers, http_opts())
         |> ExAws.Request.maybe_transform_response() do
      {:ok, %{status_code: 200, body: body}} ->
        body

      {:ok, %{status_code: status_code}} ->
        retry_or_raise(config, url, status_code, fallback)

      error ->
        raise """
        Instance Meta Error: #{inspect(error)}

        You tried to access the AWS EC2 instance meta, but it could not be reached.
        This happens most often when trying to access it from your local computer,
        which happens when environment variables are not set correctly prompting
        ExAws to fallback to the Instance Meta.

        Please check your key config and make sure they're configured correctly:

        For Example:
        ```
        ExAws.Config.new(:s3)
        ExAws.Config.new(:dynamodb)
        ```
        """
    end
  end

  defp retry_or_raise(config, url, 401, false) do
    request(config, url, true)
  end

  defp retry_or_raise(_config, _url, status_code, _fallback) do
    raise """
    Instance Meta Error: HTTP response status code #{inspect(status_code)}

    Please check AWS EC2 IAM role.
    """
  end

  def get_request_headers(config, fallback) do
    if fallback || Map.get(config, :require_imds_v2) do
      ExAws.InstanceMetaTokenProvider.get_headers(config)
    else
      []
    end
  end

  def instance_role(config) do
    ExAws.InstanceMeta.request(config, @meta_path_root <> "/iam/security-credentials/")
  end

  def task_role_credentials(config) do
    case System.get_env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") do
      nil ->
        nil

      uri ->
        ExAws.InstanceMeta.request(config, @task_role_root <> uri)
        |> config.json_codec.decode!
    end
  end

  def instance_role_credentials(config) do
    ExAws.InstanceMeta.request(
      config,
      @meta_path_root <> "/iam/security-credentials/#{instance_role(config)}"
    )
    |> config.json_codec.decode!
  end

  def security_credentials(config) do
    result =
      case task_role_credentials(config) do
        nil -> instance_role_credentials(config)
        credentials -> credentials
      end

    %{
      access_key_id: result["AccessKeyId"],
      secret_access_key: result["SecretAccessKey"],
      security_token: result["Token"],
      expiration: result["Expiration"]
    }
  end

  defp http_opts do
    # Certain solutions like kube2iam will redirect instance meta requests,
    # we need to follow those redirects.
    defaults = [follow_redirect: true]

    overrides =
      Application.get_env(:ex_aws, :metadata, [])
      |> Keyword.get(:http_opts, [])

    Keyword.merge(defaults, overrides)
  end
end
