defmodule Gringotts.Gateways.GlobalCollect do
  @moduledoc """
  [GlobalCollect][home] gateway implementation.

  For further details, please refer [GlobalCollect API documentation][docs].

  Following are the features that have been implemented for GlobalCollect:

  | Action                       | Method        |
  | ------                       | ------        |
  | Authorize                    | `authorize/3` |
  | Purchase                     | `purchase/3`  |
  | Capture                      | `capture/3`   |
  | Refund                       | `refund/3`    |
  | Void                         | `void/2`      |

  ## Optional parameters

  Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply
  optional arguments for transactions with the gateway.

  | Key                      | Remark          |
  | ----                     | ---             |
  | `merchantCustomerId`     |  Identifier for the consumer that can be used as a search criteria in the Global Collect Payment Console |
  | `description`            |  Descriptive text that is used towards to consumer, either during an online checkout at a third party and/or on the statement of the consumer |
  | `dob`                    |  The date of birth of the consumer Format: YYYYMMDD   |
  | `company`                |  Name of company, as a consumer                       |
  | `email`                  |  Email address of the consumer                        |
  | `phone`                  |  Phone number of the consumer                         |
  | `invoice`                |  Object containing additional invoice data            |
  | `billingAddress`         |  Object containing billing address details            |
  | `shippingAddress`        |  Object containing shipping address details           |
  | `name`                   |  Object containing the name details of the consumer   |
  | `skipAuthentication`     |  3D Secure Authentication will be skipped for this transaction if set to true |

  For more details of the required keys refer [this.][options]
  ## Registering your GlobalCollect account at `Gringotts`

  After creating your account successfully on [GlobalCollect][home] open the
  [dashboard][dashboard] to fetch the `secret_api_key`, `api_key_id` and
  `merchant_id` from the menu.

  Here's how the secrets map to the required configuration parameters for
  GlobalCollect:

  | Config parameter | GlobalCollect secret |
  | -------          | ----                 |
  | `:secret_api_key`| **SecretApiKey**     |
  | `:api_key_id`    | **ApiKeyId**         |
  | `:merchant_id`   | **MerchantId**       |

  Your Application config **must include the `:secret_api_key`, `:api_key_id`,
  `:merchant_id` field(s)** and would look something like this:

       config :gringotts, Gringotts.Gateways.GlobalCollect,
           secret_api_key: "your_secret_secret_api_key"
           api_key_id: "your_secret_api_key_id"
           merchant_id: "your_secret_merchant_id"

   ## Scope of this module

  * [All amount fields in globalCollect are in cents with each amount having 2 decimals.][amountReference]

  ## Supported currencies and countries

  The GlobalCollect platform supports payments in [over 150 currencies][currencies].

  ## Following the examples

  1. First, set up a sample application and configure it to work with GlobalCollect.
      - You could do that from scratch by following our [Getting Started][gs] guide.
      - To save you time, we recommend [cloning our example
      repo][example] that gives you a pre-configured sample app ready-to-go.
          + You could use the same config or update it the with your "secrets"
          as described [above](#module-registering-your-globalcollect-account-at-gringotts).

  2. Run an `iex` session with `iex -S mix` and add some variable bindings and
     aliases to it (to save some time):
  ```
  iex> alias Gringotts.{Response, CreditCard, Gateways.GlobalCollect}
  iex> shippingAddress = %{
  street: "Desertroad",
  houseNumber: "1",
  additionalInfo: "Suite II",
  zip: "84536",
  city: "Monument Valley",
  state: "Utah",
  countryCode: "US"
  }

  iex> billingAddress = %{
  street: "Desertroad",
  houseNumber: "13",
  additionalInfo: "b",
  zip: "84536",
  city: "Monument Valley",
  state: "Utah",
  countryCode: "US"
  }

  iex> invoice = %{
  invoiceNumber: "000000123",
  invoiceDate: "20140306191500"
  }

  iex> name = %{
  title: "Miss",
  firstName: "Road",
  surname: "Runner"
  }

  iex> card = %CreditCard{
  number: "4567350000427977",
  month: 12,
  year: 43,
  first_name: "John",
  last_name: "Doe",
  verification_code: "123",
  brand: "VISA"
  }

  iex> opts = [
  description: "Store Purchase 1437598192",
  merchantCustomerId: "234", dob: "19490917",
  company: "asma", email: "johndoe@gmail.com",
  phone: "7765746563", invoice: invoice,
  billingAddress: billingAddress,
  shippingAddress: shippingAddress,
  name: name, skipAuthentication: "true"
  ]
  ```

  We'll be using these in the examples below.

  [home]: http://www.globalcollect.com/
  [docs]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/index.html
  [dashboard]: https://sandbox.account.ingenico.com/#/dashboard
  [gs]: #
  [options]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/java/payments/create.html#payments-create-payload
  [currencies]: https://epayments.developer-ingenico.com/best-practices/services/currency-conversion
  [example]: https://github.com/aviabird/gringotts_example
  [amountReference]: https://epayments-api.developer-ingenico.com/c2sapi/v1/en_US/swift/services/convertAmount.html
  """
  @base_url "https://api-sandbox.globalcollect.com/v1/"

  use Gringotts.Gateways.Base

  # The Adapter module provides the `validate_config/1`
  # Add the keys that must be present in the Application config in the
  # `required_config` list
  use Gringotts.Adapter, required_config: [:secret_api_key, :api_key_id, :merchant_id]

  alias Gringotts.{CreditCard, Money, Response}

  @brand_map %{
    VISA: "1",
    AMERICAN_EXPRESS: "2",
    MASTER: "3",
    DISCOVER: "128",
    JCB: "125",
    DINERS_CLUB: "132"
  }

  @doc """
  Performs a (pre) Authorize operation.

  The authorization validates the `card` details with the banking network,
  places a hold on the transaction `amount` in the customer’s issuing bank and
  also triggers risk management. Funds are not transferred.

  GlobalCollect returns a payment id which can be further used to:
  * `capture/3` an amount.
  * `refund/3` an amount
  * `void/2` a pre_authorization

  ## Example

  The following example shows how one would (pre) authorize a payment of $100 on
  a sample `card`.
  ```
  iex> card = %CreditCard{
      number: "4567350000427977",
      month: 12,
      year: 43,
      first_name: "John",
      last_name: "Doe",
      verification_code: "123",
      brand: "VISA"
  }

  iex> amount = Money.new(100, :USD)

  iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.GlobalCollect, amount, card, opts)
  ```
  """
  @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
  def authorize(amount, %CreditCard{} = card, opts) do
    params = %{
      order: add_order(amount, opts),
      cardPaymentMethodSpecificInput: add_card(card, opts)
    }

    commit(:post, "payments", params, opts)
  end

  @doc """
  Captures a pre-authorized `amount`.

  `amount` used in the pre-authorization referenced by `payment_id` is
  transferred to the merchant account by GlobalCollect.

  ## Note

  Authorized payment with PENDING_APPROVAL status only allow a single capture whereas
  the one with PENDING_CAPTURE status is used for payments that allow multiple captures.

  ## Example

  The following example shows how one would (partially) capture a previously
  authorized a payment worth $100 by referencing the obtained authorization `id`.

  ```
  iex> amount = Money.new(100, :USD)

  iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.GlobalCollect, auth_result.authorization, amount, opts)

  ```

  """
  @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response}
  def capture(payment_id, amount, opts) do
    params = %{
      order: add_order(amount, opts)
    }

    commit(:post, "payments/#{payment_id}/approve", params, opts)
  end

  @doc """
  Transfers `amount` from the customer to the merchant.

  GlobalCollect attempts to process a purchase on behalf of the customer, by
  debiting `amount` from the customer's account by charging the customer's
  `card`.

  ## Example

  The following example shows how one would process a payment in one-shot,
  without (pre) authorization.

  ```
  iex> card = %CreditCard{
      number: "4567350000427977",
      month: 12,
      year: 43,
      first_name: "John",
      last_name: "Doe",
      verification_code: "123",
      brand: "VISA"
    }

  iex> amount = Money.new(100, :USD)

  iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.GlobalCollect, amount, card, opts)

  ```
  """
  @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
  def purchase(amount, %CreditCard{} = card, opts) do
    case authorize(amount, card, opts) do
      {:ok, results} ->
        payment_id = results.raw["payment"]["id"]
        capture(payment_id, amount, opts)

      {:error, results} ->
        {:error, results}
    end
  end

  @doc """
  Voids the referenced payment.

  This makes it impossible to process the payment any further and will also try
  to reverse an authorization on a card.
  Reversing an authorization that you will not be utilizing will prevent you
  from having to [pay a fee/penalty][void] for unused authorization requests.

  [void]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/java/payments/cancel.html#payments-cancel-request
  ## Example

  The following example shows how one would void a previous (pre)
  authorization. Remember that our `capture/3` example only did a complete
  capture.

  ```
  iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.GlobalCollect, auth_result.authorization, opts)

  ```
  """
  @spec void(String.t(), keyword) :: {:ok | :error, Response}
  def void(payment_id, opts) do
    commit(:post, "payments/#{payment_id}/cancel", [], opts)
  end

  @doc """
  Refunds the `amount` to the customer's account with reference to a prior transfer.

  You always have the option to refund just a portion of the payment amount.
  It is also possible to submit multiple refund requests on one payment as long
  as the total amount to be refunded does not exceed the total amount that was paid.

  ## Example

  The following example shows how one would refund a previous purchase (and
  similarily for captures).

  ```
  iex> amount = Money.new(100, :USD)

  iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.authorization, amount)
  ```
  """
  @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response}
  def refund(amount, payment_id, opts) do
    params = %{
      amountOfMoney: add_money(amount),
      customer: add_customer(opts)
    }

    commit(:post, "payments/#{payment_id}/refund", params, opts)
  end

  ###############################################################################
  #                                PRIVATE METHODS                              #
  ###############################################################################

  defp add_order(money, options) do
    %{
      amountOfMoney: add_money(money),
      customer: add_customer(options),
      references: %{
        descriptor: options[:description],
        invoiceData: options[:invoice]
      }
    }
  end

  defp add_money(amount) do
    {currency, amount, _} = Money.to_integer(amount)

    %{
      amount: amount,
      currencyCode: currency
    }
  end

  defp add_customer(options) do
    %{
      merchantCustomerId: options[:merchantCustomerId],
      personalInformation: %{
        name: options[:name]
      },
      dateOfBirth: options[:dob],
      companyInformation: %{
        name: options[:company]
      },
      billingAddress: options[:billingAddress],
      shippingAddress: options[:shippingAddress],
      contactDetails: %{
        emailAddress: options[:email],
        phoneNumber: options[:phone]
      }
    }
  end

  defp add_card(card, opts) do
    %{
      paymentProductId: Map.fetch!(@brand_map, String.to_atom(card.brand)),
      skipAuthentication: opts[:skipAuthentication],
      card: %{
        cvv: card.verification_code,
        cardNumber: card.number,
        expiryDate: "#{card.month}#{card.year}",
        cardholderName: CreditCard.full_name(card)
      }
    }
  end

  defp commit(method, path, params, opts) do
    headers = create_headers(path, opts)
    data = Jason.encode!(params)
    merchant_id = opts[:config][:merchant_id]
    url = "#{@base_url}#{merchant_id}/#{path}"

    gateway_response = HTTPoison.request(method, url, data, headers)
    gateway_response |> respond
  end

  defp create_headers(path, opts) do
    datetime = Timex.now() |> Timex.local()

    date_string =
      "#{Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S", :strftime)} #{datetime.zone_abbr}"

    api_key_id = opts[:config][:api_key_id]

    sha_signature = auth_digest(path, date_string, opts)

    auth_token = "GCS v1HMAC:#{api_key_id}:#{Base.encode64(sha_signature)}"
    [{"Content-Type", "application/json"}, {"Authorization", auth_token}, {"Date", date_string}]
  end

  defp auth_digest(path, date_string, opts) do
    secret_api_key = opts[:config][:secret_api_key]
    merchant_id = opts[:config][:merchant_id]

    data = "POST\napplication/json\n#{date_string}\n/v1/#{merchant_id}/#{path}\n"
    :crypto.mac(:hmac, :sha256, secret_api_key, data)
  end

  # Parses GlobalCollect's response and returns a `Gringotts.Response` struct
  # in a `:ok`, `:error` tuple.
  @spec respond(term) :: {:ok | :error, Response}
  defp respond(global_collect_response)

  defp respond({:ok, %{status_code: code, body: body}}) when code in [200, 201] do
    case Jason.decode(body) do
      {:ok, results} ->
        {
          :ok,
          Response.success(
            authorization: results["payment"]["id"],
            raw: results,
            status_code: code,
            avs_result:
              results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][
                "fraudResults"
              ]["avsResult"],
            cvc_result:
              results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][
                "fraudResults"
              ]["cvcResult"],
            message: results["payment"]["status"],
            fraud_review:
              results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][
                "fraudResults"
              ]["fraudServiceResult"]
          )
        }

      {:error, _} ->
        {:error, Response.error(raw: body, message: "undefined response from GlobalCollect")}
    end
  end

  defp respond({:ok, %{status_code: status_code, body: body}}) do
    {:ok, results} = Jason.decode(body)
    message = Enum.map(results["errors"], fn x -> x["message"] end)
    detail = List.to_string(message)
    {:error, Response.error(status_code: status_code, message: detail, raw: results)}
  end

  defp respond({:error, %HTTPoison.Error{} = error}) do
    {
      :error,
      Response.error(
        code: error.id,
        reason: :network_fail?,
        description: "HTTPoison says '#{error.reason}'"
      )
    }
  end
end
