defmodule Gringotts.Gateways.Openpay do
  @moduledoc """
  [openpay][home] gateway implementation.
  For reference see [openpay documentation][docs].
  The following features of openpay are implemented:

  | Action                       | Method        |
  | ------                       | ------        |
  | Pre-authorize                | `authorize/3` |
  | Capture                      | `capture/3`   |
  | Purchase                     | `purchase/3`  |
  | Reversal                     | `void/2`      |
  | Refund                       | `refund/3`    |
  [home]: https://www.openpay.mx/
  [docs]: https://www.openpay.mx/en/docs/api/

  ## The `opts` argument
  Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply
  optional arguments for transactions with the mercadopago
  gateway. The following keys are supported:

  | Key                      | Remark                                                                                             |
  | ----                     | ---                                                                                                |
  | `email`                  | Email of the customer. Type - string                                                               |
  | `device_session_id`      | Identifier of the device generated by the antifraud tool. Type- string                             |
  | `method`                 | It must contain the card value in order to specify the charge will be made from card. Type- string |
  | `address`                | Address of the card owner. Type object                                                             |
  | `phone_number`           | Customer phone number. Type- integer                                                               |

  --------------------------------------------------------------------------------

  ## Registering your openpay account at `Gringotts`

  After [making an account on openpay][dashboard], find
  your account "secrets" in the `home page`
  [dashboard]: https://sandbox-dashboard.openpay.mx/home.opay

  > Here's how the secrets map to the required configuration parameters for openpay:
  >
  > | Config parameter | openpay secret   |
  > | -------          | ----             |
  > | `:merchant_id`   |  **MerchantId**  |
  > | `:public_key`    | **PublicKey**    |

  > Your Application config **must include the `[:merchant_id, :public_key]` field(s)** and would look
  > something like this:
  >
  >     config :gringotts, Gringotts.Gateways.Openpay,
  >         merchant_id: "your_secret_merchant_id"
  >         public_key: "your_secret_public_key"

  ## Note
  mercadopago processes money with upto two decimal places.

  ## Supported currencies and countries

  openpay support mexico country and :MXN currency.

  ## Following the examples

  1. First, set up a sample application and configure it to work with openpay.
  - 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-openpay-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):

  [gs]: https://github.com/aviabird/gringotts/wiki/
  [home]: https://www.openpay.com
  [example]: https://github.com/aviabird/gringotts_example
  """

  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: [:merchant_id, :public_key]
  @base_url "https://sandbox-api.openpay.mx"

  alias Gringotts.{CreditCard, Money, Response}

  @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.

  openpay returns an ID string which can be used to:

  * `capture/3` _an_ amount.
  * `void/2` a pre-authorization.

  ## Example

  The following example shows how one would (pre) authorize a payment of $42 on
  a sample `card`.

      iex> amount = Money.new(42, :MXN)
      iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code:  "123", brand: "VISA"}
      iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Openpay, amount, card, opts)
      iex> auth_result.id # This is the authorization ID
  """
  @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
  def authorize(amount, %CreditCard{} = card, opts) do
    with {:ok, token_id} <- create_token(card, opts) do
      {_, value, _} = Money.to_integer(amount)

      body =
        card
        |> auth_params(token_id, opts, value, false)
        |> Jason.encode!()

      commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges", body, opts)
    end
  end

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

  `amount` is transferred to the merchant account by openpay when it is smaller or
  equal to the amount used in the pre-authorization referenced by `payment_id`.

  ## Note

  openpay allows partial captures and unlike many other gateways, does not release
  the remaining amount back to the payment source. Thus, the same
  pre-authorisation ID can be used to perform multiple captures, till:
  * all the pre-authorized amount is captured or,
  * the remaining amount is explicitly "reversed" via `void/2`.

  ## Example

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

      iex> amount = Money.new(35, :MXN)
      iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Openpay, amount, auth_result.id, opts)
  """
  @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response}
  def capture(payment_id, amount, opts) do
    {_, value, _} = Money.to_integer(amount)
    body = %{amount: value} |> Jason.encode!()
    commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges/#{payment_id}/capture", body, opts)
  end

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

  openpay 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 worth $42 in
  one-shot, without (pre) authorization.

      iex> amount = Money.new(42, :MXN)
      iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code:  "123", brand: "VISA"}
      iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Openpay, amount, card, opts)
  """
  @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
  def purchase(amount, %CreditCard{} = card, opts) do
    with {:ok, token_id} <- create_token(card, opts) do
      {_, value, _} = Money.to_integer(amount)

      body =
        card
        |> auth_params(token_id, opts, value, true)
        |> Jason.encode!()

      commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges", body, opts)
    end
  end

  @doc """
  Voids the referenced payment.

  This method attempts a reversal of a previous transaction referenced by
  `payment_id`.

  > As a consequence, the customer will never see any booking on his statement.

  ## Note

  > Only pending or in_process payments can be cancelled.
  > Cancelled coupon payments, deposits and/or transfers will be deposited in the buyer’s Openpay account.

  ## Example

  The following example shows how one would void

  iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Openpay, auth_result.id, opts)
  """
  @spec void(String.t(), keyword) :: {:ok | :error, Response}
  def void(payment_id, opts) do
    body = %{} |> Jason.encode!()
    commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges/#{payment_id}/refund", body, opts)
  end

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

  openpay processes a full or partial refund worth `amount`, referencing a
  previous `purchase/3` or `capture/3`.

  ## Example

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

      iex> amount = Money.new(42, :MXN)
      iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Openpay, purchase_result.id, amount)
  """
  @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response}
  def refund(amount, payment_id, opts) do
    {_, value, _} = Money.to_integer(amount)
    body = %{amount: value} |> Jason.encode!()
    commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges/#{payment_id}/refund", body, opts)
  end

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

  # Makes the request to openpay's network.
  # For consistency with other gateway implementations, make your (final)
  # network request in here, and parse it using another private method called
  # `respond`.
  # @spec commit(_) :: {:ok | :error, Response}
  defp commit(method, path, body, opts) do
    headers = [
      {"content-type", "application/json"},
      {"Authorization", "Basic c2tfNjk1YjcxN2VhNmE1NGZlOTg2ZjRlNTJiZTJmZTdlNjE6"}
    ]

    res = HTTPoison.request(method, "#{@base_url}#{path}", body, headers)

    respond(res, opts)
  end

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

  defp create_token(%CreditCard{} = card, opts) do
    body =
      card
      |> token_params(opts)
      |> Jason.encode!()

    {state, res} = commit(:post, "/v1/#{opts[:config][:merchant_id]}/tokens", body, opts)

    case state do
      :ok -> {:ok, res.id}
      :error -> {:error, res}
    end
  end

  defp token_params(%CreditCard{} = card, opts) do
    %{
      card_number: card.number,
      holder_name: CreditCard.full_name(card),
      expiration_year: card.year,
      expiration_month: card.month,
      cvv2: card.verification_code |> String.to_integer(),
      address: %{
        city: opts[:address][:city],
        country_code: opts[:address][:country_code],
        postal_code: opts[:address][:postal_code],
        line1: opts[:address][:line1],
        line2: opts[:address][:line2],
        line3: opts[:address][:line3],
        state: opts[:address][:state]
      }
    }
  end

  defp auth_params(%CreditCard{} = card, token_id, opts, value, capture) do
    %{
      source_id: token_id,
      method: opts[:method],
      amount: value,
      device_session_id: opts[:device_session_id],
      capture: capture,
      customer: %{
        name: card.first_name,
        last_name: card.last_name,
        phone_number: opts[:phone_number],
        email: opts[:email]
      }
    }
  end

  defp success_body(body, status_code, _opts) do
    %Response{
      success: true,
      id: body["id"],
      status_code: status_code,
      message: body["status"]
    }
  end

  defp error_body(body, status_code, _opts) do
    %Response{
      success: false,
      status_code: status_code,
      message: body["description"]
    }
  end

  defp respond({:ok, %HTTPoison.Response{body: body, status_code: status_code}}, opts) do
    body = body |> Jason.decode!()

    case body["description"] do
      nil -> {:ok, success_body(body, status_code, opts)}
      _ -> {:error, error_body(body, status_code, opts)}
    end
  end

  defp respond({:error, %HTTPoison.Error{} = error}, _) do
    {
      :error,
      Response.error(
        reason: "network related failure",
        message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]"
      )
    }
  end
end
