defmodule Bitcoinex.LightningNetwork.Invoice do
  @moduledoc """
  Includes BOLT#11 Invoice serialization.

  Reference: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
  """

  alias Bitcoinex.{Bech32, Network, Segwit}
  alias Bitcoinex.LightningNetwork.HopHint
  alias Decimal, as: D

  import Bitwise
  # consider using https://github.com/ejpcmac/typed_struct

  @default_min_final_cltv_expiry 18
  @default_expiry 3600

  @enforce_keys [:network, :destination, :payment_hash, :timestamp]
  defstruct [
    :network,
    :destination,
    :payment_hash,
    :amount_msat,
    :timestamp,
    :description,
    :description_hash,
    :fallback_address,
    route_hints: [],
    expiry: @default_expiry,
    min_final_cltv_expiry: @default_min_final_cltv_expiry
  ]

  @type t() :: %__MODULE__{
          network: Network.network_name(),
          destination: String.t(),
          payment_hash: String.t(),
          amount_msat: non_neg_integer | nil,
          timestamp: integer(),
          expiry: integer() | nil,
          # description and description_hash are either both non-nil or nil
          description: String.t() | nil,
          description_hash: String.t() | nil,
          fallback_address: String.t() | nil,
          min_final_cltv_expiry: non_neg_integer,
          route_hints: list(HopHint.t())
        }

  @prefix "ln"
  @valid_multipliers ~w(m u n p)
  # TODO move it to bitcoin asset?
  @milli_satoshi_per_bitcoin 100_000_000_000
  # the 512 bit signature + 8 bit recovery ID.
  @signature_base32_length 104
  @timestamp_base32_length 7
  @sha256_hash_base32_length 52
  @pubkey_base32_length 53
  @hop_hint_length 51
  @type error :: atom

  @doc """
   Decode accepts a Bech32 encoded string invoice and deserializes it.
  """
  @spec decode(String.t()) :: {:ok, t} | {:error, error}
  def decode(invoice) when is_binary(invoice) do
    with {:ok, {_encoding_type, hrp, data}} <- Bech32.decode(invoice, :infinity),
         {:ok, {network, amount_msat}} <- parse_hrp(hrp),
         {invoice_data, signature_data} = split_at(data, -@signature_base32_length),
         {:ok, parsed_data} <-
           parse_invoice_data(invoice_data, network),
         {:ok, destination} <-
           validate_and_parse_signature_data(
             Map.get(parsed_data, :destination),
             hrp,
             invoice_data,
             signature_data
           ) do
      __MODULE__
      |> struct(
        Map.merge(
          parsed_data,
          %{
            network: network,
            amount_msat: amount_msat,
            destination: destination
          }
        )
      )
      |> validate_invoice()
    end
  end

  def decode(invoice) when is_binary(invoice) do
    {:error, :no_ln_prefix}
  end

  @doc """
   Returns the expiry of the invoice.
  """
  @spec expires_at(Bitcoinex.LightningNetwork.Invoice.t()) :: DateTime.t()
  def expires_at(%__MODULE__{} = invoice) do
    expiry = invoice.expiry
    DateTime.from_unix!(invoice.timestamp + expiry)
  end

  # checking some invariant for invoice
  # TODO Could we use ecto(without SQL) for this?
  defp validate_invoice(%__MODULE__{} = invoice) do
    cond do
      is_nil(invoice.network) ->
        {:error, :network_missing}

      !is_nil(invoice.amount_msat) && invoice.amount_msat < 0 ->
        {:error, :negative_amount_msat}

      is_nil(invoice.payment_hash) ->
        {:error, :payment_hash_missing}

      is_nil(invoice.description) && is_nil(invoice.description_hash) ->
        {:error, :both_description_and_description_hash_present}

      !is_nil(invoice.description) && !is_nil(invoice.description_hash) ->
        {:error, :both_description_and_description_hash_missing}

      # lnd have this but not in Bolt11. do we need to enforce this?
      Enum.count(invoice.route_hints) > 20 ->
        {:error, :too_many_private_routes}

      String.length(invoice.payment_hash) != 64 ->
        {:error, :invalid_payment_hash_length}

      !is_nil(invoice.description_hash) && String.length(invoice.description_hash) != 64 ->
        {:error, :invalid_payment_hash}

      # String.length(invoice.destination) != 64 ->
      #   {:error, :invalid_destination_length}

      true ->
        {:ok, invoice}
    end
  end

  defp validate_and_parse_signature_data(destination, hrp, invoice_data, signature_data)
       when is_list(invoice_data) and is_list(signature_data) do
    with {:ok, signature_data_in_byte} <- Bech32.convert_bits(signature_data, 5, 8),
         {signature, [recoveryId]} = split_at(signature_data_in_byte, -1),
         {:ok, invoice_data_in_byte} <- Bech32.convert_bits(invoice_data, 5, 8) do
      to_sign = (hrp |> :erlang.binary_to_list()) ++ invoice_data_in_byte
      signature = signature |> byte_list_to_binary
      hash = to_sign |> Bitcoinex.Utils.sha256()

      # TODO if destination exist from tagged field, we dun need to recover but to verify it with signature
      # but that require convert lg sig before using secp256k1 to verify it
      # TODO refactor too nested
      case Bitcoinex.Secp256k1.Ecdsa.ecdsa_recover_compact(hash, signature, recoveryId) do
        {:ok, pubkey} ->
          if is_nil(destination) or destination == pubkey do
            {:ok, pubkey}
          else
            {:error, :invalid_invoice_signature}
          end

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

  defp parse_invoice_data(data, network) when is_list(data) do
    {timstamp_data, tagged_fields_data} = split_at(data, @timestamp_base32_length)

    with {:ok, timestamp} <- parse_timestamp(timstamp_data),
         {:ok, parsed_data} <-
           parse_tagged_fields(tagged_fields_data, network) do
      {:ok, Map.put(parsed_data, :timestamp, timestamp)}
    end
  end

  defp parse_tagged_fields(data, network) when is_list(data) do
    do_parse_tagged_fields(data, %{}, network)
  end

  defp do_parse_tagged_fields([type, data_length1, data_length2 | rest], acc, network) do
    data_length = data_length1 <<< 5 ||| data_length2

    if Enum.count(rest) < data_length do
      {:error, :invalid_field_length}
    else
      {data, new_rest} = split_at(rest, data_length)

      case(parse_tagged_field(type, data, acc, network)) do
        {:ok, acc} ->
          do_parse_tagged_fields(new_rest, acc, network)

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

  defp do_parse_tagged_fields(_, acc, _network) do
    {:ok, acc}
  end

  defp parse_tagged_field(type, data, acc, network) do
    case type do
      1 ->
        if Map.has_key?(acc, :payment_hash) do
          {:ok, acc}
        else
          case parse_payment_hash(data) do
            {:ok, payment_hash} ->
              {:ok, Map.put(acc, :payment_hash, payment_hash)}

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

      # r field HopHints
      3 ->
        if Map.has_key?(acc, :route_hints) do
          {:ok, acc}
        else
          case parse_hop_hints(data) do
            {:ok, hop_hints} ->
              {:ok, Map.put(acc, :route_hints, hop_hints)}

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

      # x field
      6 ->
        if Map.has_key?(acc, :expiry) do
          {:ok, acc}
        else
          expiry = parse_expiry(data)
          {:ok, Map.put(acc, :expiry, expiry)}
        end

      # f field fallback address
      9 ->
        if Map.has_key?(acc, :fallback_address) do
          {:ok, acc}
        else
          case parse_fallback_address(data, network) do
            {:ok, fallback_address} ->
              {:ok, Map.put(acc, :fallback_address, fallback_address)}

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

      # d field
      13 ->
        if Map.has_key?(acc, :description) do
          {:ok, acc}
        else
          case parse_description(data) do
            {:ok, description} ->
              {:ok, Map.put(acc, :description, description)}

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

      # n field destination
      19 ->
        case acc do
          %{destination: destination} when destination != nil ->
            {:ok, acc}

          _ ->
            case parse_destination(data) do
              {:ok, destination} ->
                {:ok, Map.put(acc, :destination, destination)}

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

      # h field description hash
      23 ->
        if Map.has_key?(acc, :description_hash) do
          {:ok, acc}
        else
          case parse_description_hash(data) do
            {:ok, description_hash} ->
              {:ok, Map.put(acc, :description_hash, description_hash)}

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

      # c field MINIMUM Fianl CLTV Expiry
      24 ->
        if Map.has_key?(acc, :min_final_cltv_expiry) do
          {:ok, acc}
        else
          min_final_cltv_expiry = parse_min_final_cltv_expiry(data)
          {:ok, Map.put(acc, :min_final_cltv_expiry, min_final_cltv_expiry)}
        end

      _ ->
        {:ok, acc}
    end
  end

  defp parse_timestamp(data) do
    {:ok, base32_to_integer(data)}
  end

  defp parse_payment_hash(data) when is_list(data) do
    if Enum.count(data) == @sha256_hash_base32_length do
      case Bech32.convert_bits(data, 5, 8, false) do
        {:ok, converted_data} ->
          {:ok, converted_data |> :binary.list_to_bin() |> Base.encode16(case: :lower)}

        {:error, error} ->
          {:error, error}
      end
    else
      {:error, :invalid_payment_hash_length}
    end
  end

  defp parse_description(data) do
    case Bech32.convert_bits(data, 5, 8, false) do
      {:ok, description} ->
        {:ok, :binary.list_to_bin(description)}

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

  defp parse_expiry(data) do
    base32_to_integer(data)
  end

  @spec base32_to_integer(maybe_improper_list()) :: any()
  def base32_to_integer(data) when is_list(data) do
    Enum.reduce(data, 0, fn val, acc ->
      acc <<< 5 ||| val
    end)
  end

  defp parse_destination(data) when is_list(data) do
    if Enum.count(data) == @pubkey_base32_length do
      case Bech32.convert_bits(data, 5, 8, false) do
        {:ok, data_in_bytes} ->
          {:ok, bytes_to_hex_string(data_in_bytes)}

        {:error, error} ->
          {:error, error}
      end
    else
      {:ok, nil}
    end
  end

  defp parse_description_hash(data) when is_list(data) do
    if Enum.count(data) == @sha256_hash_base32_length do
      case Bech32.convert_bits(data, 5, 8, false) do
        {:ok, data_in_bytes} ->
          {:ok, data_in_bytes |> bytes_to_hex_string}

        {:error, error} ->
          {:error, error}
      end
    else
      {:ok, nil}
    end
  end

  defp parse_fallback_address([], _network) do
    {:error, :empty_fallback_address}
  end

  defp parse_fallback_address([version | rest], network) do
    case version do
      0 ->
        case Bech32.convert_bits(rest, 5, 8, false) do
          {:ok, witness} ->
            case Enum.count(witness) do
              witness_program_lenghh when witness_program_lenghh in [20, 32] ->
                Segwit.encode_address(network, 0, witness)

              _ ->
                {:error, :invalid_witness_program_length}
            end

          err ->
            err
        end

      17 ->
        case Bech32.convert_bits(rest, 5, 8, false) do
          {:ok, pubKeyHash} ->
            {:ok,
             Bitcoinex.Address.encode(
               pubKeyHash |> :binary.list_to_bin(),
               network,
               :p2pkh
             )}

          err ->
            err
        end

      18 ->
        case Bech32.convert_bits(rest, 5, 8, false) do
          {:ok, scriptHash} ->
            {:ok,
             Bitcoinex.Address.encode(
               scriptHash |> :binary.list_to_bin(),
               network,
               :p2sh
             )}

          err ->
            err
        end

      # ignore unknown version
      _ ->
        {:ok, nil}
    end
  end

  defp parse_hop_hints(data) when is_list(data) do
    with {:ok, data_in_byte} <- Bech32.convert_bits(data, 5, 8, false),
         {_, true} <-
           {:validate_hop_hint_data_length, rem(Enum.count(data_in_byte), @hop_hint_length) == 0} do
      hop_hints =
        data_in_byte
        |> Enum.chunk_every(@hop_hint_length)
        |> Enum.map(&parse_hop_hint/1)

      {:ok, hop_hints}
    else
      {:validate_hop_hint_data_length, false} ->
        {:error, :invalid_hop_hint_data_length}

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

  defp parse_integer_from_hex_str!(hex_str) do
    {hex_str, ""} = Integer.parse(hex_str, 16)
    hex_str
  end

  # exoect they are list of integer in byte
  defp parse_hop_hint(data) when is_list(data) do
    # 64 bits
    {node_id_data, rest} = data |> split_at(33)
    node_id = node_id_data |> bytes_to_hex_string
    # 64 bits
    {channel_id_data, rest} = rest |> split_at(8)
    channel_id = channel_id_data |> bytes_to_hex_string |> parse_integer_from_hex_str!
    # 32 bits
    {fee_base_m_sat_data, rest} = rest |> split_at(4)
    fee_base_m_sat = fee_base_m_sat_data |> bytes_to_hex_string |> parse_integer_from_hex_str!
    # 32 bits
    {fee_proportional_millionths_data, rest} = rest |> split_at(4)

    fee_proportional_millionths =
      fee_proportional_millionths_data |> bytes_to_hex_string |> parse_integer_from_hex_str!

    cltv_expiry_delta = rest |> bytes_to_hex_string |> parse_integer_from_hex_str!

    %HopHint{
      node_id: node_id,
      channel_id: channel_id,
      fee_base_m_sat: fee_base_m_sat,
      fee_proportional_millionths: fee_proportional_millionths,
      cltv_expiry_delta: cltv_expiry_delta
    }
  end

  defp parse_min_final_cltv_expiry(data) when is_list(data) do
    base32_to_integer(data)
  end

  # defp get_pubkey_to_address_magicbyte(network, script_type) do
  #   case {network, script_type} do
  #     {:mainnet, :p2pkh} ->
  #       0x00

  #     {:mainnet, :p2sh} ->
  #       0x05

  #     {network, :p2pkh} when network in [:testnet, :regtest] ->
  #       0x6F

  #     {network, :p2sh} when network in [:testnet, :regtest] ->
  #       0xC4
  #   end
  # end

  defp parse_network(@prefix <> rest_hrp) do
    case Enum.find(Network.supported_networks(), fn %{hrp_segwit_prefix: hrp_segwit_prefix} ->
           if String.starts_with?(rest_hrp, hrp_segwit_prefix) do
             size = bit_size(hrp_segwit_prefix)

             case rest_hrp do
               # without amount
               ^hrp_segwit_prefix ->
                 true

               # with amount. a valid segwit_prefix must be following with base10 digit
               # ?0..?9 means range of codepoint of 0 - 9
               # it shoudln't include 0 but that's not responsiblity of passing network function here
               <<_::size(size), i, _::binary>> when i in ?0..?9 ->
                 true

               _ ->
                 false
             end
           end
         end) do
      nil ->
        {:error, :invalid_network}

      network ->
        {:ok, network}
    end
  end

  defp parse_hrp(hrp) do
    with {_, @prefix <> rest_hrp} <- {:strip_prefix, hrp},
         {_, {:ok, %{name: network_name, hrp_segwit_prefix: hrp_segwit_prefix}}} <-
           {:parse_network, parse_network(hrp)} do
      hrp_segwit_prefix_size = byte_size(hrp_segwit_prefix)

      case rest_hrp do
        ^hrp_segwit_prefix ->
          {:ok, {network_name, nil}}

        _ ->
          amount_str = String.slice(rest_hrp, hrp_segwit_prefix_size..-1//1)

          case calculate_milli_satoshi(amount_str) do
            {:ok, amount} ->
              {:ok, {network_name, amount}}

            {:error, error} ->
              {:error, error}
          end
      end
    else
      {:strip_prefix, _} ->
        {:error, :no_ln_prefix}

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

  defp calculate_milli_satoshi(amount_str) do
    if String.length(amount_str) > 1 and String.starts_with?(amount_str, "0") do
      {:error, :amount_with_leading_zero}
    else
      result =
        case Regex.run(~r/[munp]$/, amount_str) do
          [multiplier] when multiplier in @valid_multipliers ->
            case Integer.parse(String.slice(amount_str, 0..-2//1)) do
              {amount, ""} ->
                {:ok, to_bitcoin(amount, multiplier)}

              _ ->
                {:error, :invalid_amount}
            end

          _ ->
            case Integer.parse(amount_str) do
              {amount_in_bitcoin, ""} ->
                {:ok, amount_in_bitcoin}

              _ ->
                {:error, :invalid_amount}
            end
        end

      case result do
        {:ok, amount_in_bitcoin} ->
          amount_msat_dec = D.mult(amount_in_bitcoin, @milli_satoshi_per_bitcoin)
          rounded_amount_msat_dec = D.round(amount_msat_dec)

          case D.equal?(rounded_amount_msat_dec, amount_msat_dec) do
            true ->
              {:ok, D.to_integer(rounded_amount_msat_dec)}

            false ->
              {:error, :sub_msat_precision_amount}
          end

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

  defp to_bitcoin(amount, multiplier_str) when is_integer(amount) do
    multiplier =
      case multiplier_str do
        "m" ->
          0.001

        "u" ->
          0.000001

        "n" ->
          0.000000001

        "p" ->
          0.000000000001
      end

    D.mult(amount, D.from_float(multiplier))
  end

  defp bytes_to_hex_string(bytes) when is_list(bytes) do
    bytes |> :binary.list_to_bin() |> Base.encode16(case: :lower)
  end

  defp byte_list_to_binary(bytes) when is_list(bytes) do
    bytes |> :binary.list_to_bin()
  end

  @spec split_at(Enum.t(), integer()) :: {list(Enum.t()), list(Enum.t())}
  defp split_at(xs, index) when index >= 0 do
    {Enum.take(xs, index), Enum.drop(xs, index)}
  end

  defp split_at(xs, index) when index < 0 do
    {Enum.drop(xs, index), Enum.take(xs, index)}
  end
end
