defmodule Bitcoinex.ExtendedKey do
  @moduledoc """
  	Contains an an extended key as documented in BIP 32.
  """
  alias Bitcoinex.Secp256k1.{Params, Point, PrivateKey, Math}
  alias Bitcoinex.Base58

  import Bitwise

  defmodule DerivationPath do
    @moduledoc """
    Contains a list of integers (or the :any atom) representing a bip32 derivation path.
    The :any atom represents a wildcard in the derivation path. DerivationPath structs can
    be used by ExtendedKey.derive_extended_key to derive a child key at the given path.
    """

    @min_non_hardened_child_num 0
    # 2^31 - 1
    @max_non_hardened_child_num 0x80000000 - 1
    @min_hardened_child_num 0x80000000
    # 2^32 - 1
    @max_hardened_child_num 0x100000000 - 1

    @type hardened_child_num ::
            unquote(@min_hardened_child_num)..unquote(@max_hardened_child_num)
    @type non_hardened_child_num ::
            unquote(@min_non_hardened_child_num)..unquote(@max_non_hardened_child_num)

    @type child_num :: hardened_child_num | non_hardened_child_num

    @type t :: %__MODULE__{
            child_nums: list(child_num)
          }

    @enforce_keys [
      :child_nums
    ]
    defstruct [:child_nums]

    def min_hardened_child_num(), do: @min_hardened_child_num
    def max_hardened_child_num(), do: @max_hardened_child_num

    def new(), do: %__MODULE__{child_nums: []}

    @spec to_string(t()) :: {:ok, String.t()} | {:error, String.t()}
    def to_string(%__MODULE__{child_nums: path}), do: tto_string(path, "")

    defp tto_string([], path_acc), do: {:ok, path_acc}

    defp tto_string([l | rest], path_acc) do
      cond do
        l == :any ->
          tto_string(rest, path_acc <> "*/")

        l == :anyh ->
          tto_string(rest, path_acc <> "*'/")

        l > @max_hardened_child_num ->
          {:error, "index cannot be greater than #{@max_hardened_child_num}"}

        l < @min_non_hardened_child_num ->
          {:error, "index cannot be less than #{@min_non_hardened_child_num}"}

        # hardened
        l >= @min_hardened_child_num ->
          tto_string(
            rest,
            path_acc <>
              (l
               |> Math.modulo(@min_hardened_child_num)
               |> Integer.to_string()
               |> Kernel.<>("'/"))
          )

        # unhardened
        true ->
          tto_string(rest, path_acc <> Integer.to_string(l) <> "/")
      end
    end

    @spec from_string(String.t()) :: {:ok, t()} | {:error, String.t()}
    def from_string(pathstr) do
      try do
        {:ok, %__MODULE__{child_nums: tfrom_string(String.split(pathstr, "/"))}}
      rescue
        e in ArgumentError -> {:error, e.message}
      end
    end

    defp tfrom_string(path_list) do
      case path_list do
        [] -> []
        [""] -> []
        ["m" | rest] -> tfrom_string(rest)
        ["*" | rest] -> [:any | tfrom_string(rest)]
        ["*'" | rest] -> [:anyh | tfrom_string(rest)]
        ["*h" | rest] -> [:anyh | tfrom_string(rest)]
        [i | rest] -> [str_to_level(i) | tfrom_string(rest)]
      end
    end

    defp str_to_level(level) do
      {num, is_hardened} =
        case String.split(level, ["'", "h"]) do
          [num] ->
            {num, false}

          [num, ""] ->
            {num, true}
        end

      nnum = String.to_integer(num)

      if nnum in @min_non_hardened_child_num..@max_non_hardened_child_num do
        if is_hardened do
          nnum + @min_hardened_child_num
        else
          nnum
        end
      else
        raise(ArgumentError, message: "invalid derivation path")
      end
    end

    def add(%__MODULE__{child_nums: path1}, %__MODULE__{child_nums: path2}),
      do: %__MODULE__{child_nums: path1 ++ path2}
  end

  @type t :: %__MODULE__{
          prefix: binary,
          depth: binary,
          parent_fingerprint: binary,
          child_num: binary,
          chaincode: binary,
          key: binary,
          checksum: binary
        }

  @enforce_keys [
    :prefix,
    :depth,
    :parent_fingerprint,
    :child_num,
    :chaincode,
    :key,
    :checksum
  ]

  defstruct [
    :prefix,
    :depth,
    :parent_fingerprint,
    :child_num,
    :chaincode,
    :key,
    :checksum
  ]

  @xpub_pfx <<0x04, 0x88, 0xB2, 0x1E>>
  @xprv_pfx <<0x04, 0x88, 0xAD, 0xE4>>
  @tpub_pfx <<0x04, 0x35, 0x87, 0xCF>>
  @tprv_pfx <<0x04, 0x35, 0x83, 0x94>>

  @prv_prefixes [
    @xprv_pfx,
    @tprv_pfx
  ]

  @pub_prefixes [
    @xpub_pfx,
    @tpub_pfx
  ]

  @all_prefixes @prv_prefixes ++ @pub_prefixes

  defp pfx_atom_to_bin(pfx) do
    case pfx do
      :xpub -> @xpub_pfx
      :xprv -> @xprv_pfx
      :tpub -> @tpub_pfx
      :tprv -> @tprv_pfx
    end
  end

  defp prv_to_pub_prefix(prv_pfx) do
    case prv_pfx do
      @xprv_pfx -> @xpub_pfx
      @tprv_pfx -> @tpub_pfx
    end
  end

  defp mainnet_prefixes do
    [
      @xpub_pfx,
      @xprv_pfx
    ]
  end

  @spec network_from_prefix(binary) :: atom
  defp network_from_prefix(prefix) do
    if prefix in mainnet_prefixes(), do: :mainnet, else: :testnet
  end

  @doc """
    network_from_extended_key returns :testnet or :mainnet
    depending on the network prefix of the key.
  """
  @spec network_from_extended_key(t()) :: atom
  def network_from_extended_key(%__MODULE__{prefix: prefix}), do: network_from_prefix(prefix)

  # GETTERS

  @spec get_prefix(t()) :: binary
  def get_prefix(%__MODULE__{prefix: prefix}), do: prefix

  @spec get_depth(t()) :: binary
  def get_depth(%__MODULE__{depth: depth}), do: depth

  @spec get_parent_fingerprint(t()) :: binary
  def get_parent_fingerprint(%__MODULE__{parent_fingerprint: pfp}), do: pfp

  @spec get_fingerprint(t()) :: binary
  def get_fingerprint(xkey = %__MODULE__{}) do
    if xkey.prefix in @prv_prefixes do
      {:ok, prvkey} =
        xkey.key
        |> :binary.decode_unsigned()
        |> PrivateKey.new()

      prvkey
      |> PrivateKey.to_point()
      |> Point.sec()
      |> Bitcoinex.Utils.hash160()
      |> :binary.part(0, 4)
    else
      xkey.key
      |> Bitcoinex.Utils.hash160()
      |> :binary.part(0, 4)
    end
  end

  @spec get_child_num(t()) :: binary
  def get_child_num(%__MODULE__{child_num: child_num}), do: child_num

  # PARSE & SERIALIZE

  @doc """
    parse_extended_key takes binary or string representation
    of an extended key and parses it to an extended key object
  """
  @spec parse_extended_key(binary) :: {:ok, t()} | {:error, String.t()}
  def parse_extended_key(
        xkey =
          <<prefix::binary-size(4), depth::binary-size(1), parent_fingerprint::binary-size(4),
            child_num::binary-size(4), chaincode::binary-size(32), key::binary-size(33),
            checksum::binary-size(4)>>
      ) do
    cond do
      prefix not in @all_prefixes ->
        {:error, "invalid prefix"}

      # BIP 32 instructs to check that public key is valid upon import
      prefix not in @prv_prefixes and not check_point(key) ->
        {:error, "invalid public key"}

      true ->
        case Base58.validate_checksum(xkey) do
          {:error, msg} ->
            {:error, msg}

          _ ->
            {:ok,
             %__MODULE__{
               prefix: prefix,
               depth: depth,
               parent_fingerprint: parent_fingerprint,
               child_num: child_num,
               chaincode: chaincode,
               key: key,
               checksum: checksum
             }}
        end
    end
  end

  # parse from string
  def parse_extended_key(xkey) do
    case Base58.decode(xkey) do
      {:error, _} ->
        {:error, "error parsing key"}

      {:ok, xkey} ->
        xkey
        |> Base58.append_checksum()
        |> parse_extended_key()
    end
  end

  # verify if point is valid on secp256k1
  defp check_point(key) do
    {:ok, pubkey} = Point.parse_public_key(key)
    Bitcoinex.Secp256k1.verify_point(pubkey)
  end

  @doc """
    serialize_extended_key takes an extended key
    and returns the binary
  """
  @spec serialize_extended_key(t()) :: binary
  def serialize_extended_key(xkey) do
    (xkey.prefix <>
       xkey.depth <> xkey.parent_fingerprint <> xkey.child_num <> xkey.chaincode <> xkey.key)
    |> Base58.append_checksum()
  end

  @doc """
    display returns the extended key as a string
  """
  @spec display_extended_key(t()) :: String.t()
  def display_extended_key(xkey) do
    xkey
    |> serialize_extended_key()
    |> Base58.encode_base()
  end

  @doc """
    seed_to_master_private_key transforms a bip32 seed
    into a master extended private key
  """
  @spec seed_to_master_private_key(binary, atom) :: {:ok, t()} | {:error, String.t()}
  def seed_to_master_private_key(<<seed::binary>>, pfx \\ :xprv) do
    prefix = pfx_atom_to_bin(pfx)

    if prefix in @prv_prefixes do
      <<key::binary-size(32), chaincode::binary-size(32)>> =
        :crypto.mac(:hmac, :sha512, "Bitcoin seed", seed)

      depth_fingerprint_childnum = <<0, 0, 0, 0, 0, 0, 0, 0, 0>>

      (prefix <> depth_fingerprint_childnum <> chaincode <> <<0>> <> key)
      |> Base58.append_checksum()
      |> parse_extended_key()
    else
      {:error, "invalid extended private key prefix"}
    end
  end

  @doc """
    to_extended_public_key takes an extended private key
    and returns an extended public key
  """
  @spec to_extended_public_key(t()) :: {:ok, t()} | {:error, String.t()}
  def to_extended_public_key(xprv) do
    if xprv.prefix in @prv_prefixes do
      try do
        {:ok, privkey} = PrivateKey.new(:binary.decode_unsigned(xprv.key, :big))

        pubkey =
          privkey
          |> PrivateKey.to_point()
          |> Point.sec()

        xprv.prefix
        |> prv_to_pub_prefix()
        |> Kernel.<>(xprv.depth)
        |> Kernel.<>(xprv.parent_fingerprint)
        |> Kernel.<>(xprv.child_num)
        |> Kernel.<>(xprv.chaincode)
        |> Kernel.<>(pubkey)
        |> Base58.append_checksum()
        |> parse_extended_key()
      rescue
        _ in MatchError -> {:error, "invalid private key"}
      end
    else
      # it is an xpub already
      xprv
    end
  end

  @doc """
    to_private_key takes an extended private key
    and returns the contained private key.
  """
  @spec to_private_key(t()) :: {:ok, PrivateKey.t()} | {:error, String.t()}
  def to_private_key(xprv) do
    if xprv.prefix in @prv_prefixes do
      secret = :binary.decode_unsigned(xprv.key, :big)
      PrivateKey.new(secret)
    else
      {:error, "key is not a extended private key."}
    end
  end

  @doc """
    to_public_key takes an extended key xkey and
    returns the public key.
  """
  @spec to_public_key(t()) :: {:ok, Point.t()} | {:error, String.t()}
  def to_public_key(xkey) do
    if xkey.prefix in @prv_prefixes do
      case to_private_key(xkey) do
        {:ok, prvkey} -> {:ok, PrivateKey.to_point(prvkey)}
        x -> x
      end
    else
      Point.parse_public_key(xkey.key)
    end
  end

  @doc """
    derive_child uses a public or private key xkey to
    derive the public or private key at index idx.
    public key -> public child
    private key -> private child
  """
  @spec derive_child_key(t(), non_neg_integer) :: {:ok, t()} | {:error, String.t()}
  def derive_child_key(xkey, idx) do
    if xkey.prefix in @prv_prefixes do
      derive_private_child(xkey, idx)
    else
      derive_public_child(xkey, idx)
    end
  end

  @doc """
    derive_public_child uses a public or private key xkey to
    derive the public key at index idx
  """
  @spec derive_public_child(t(), non_neg_integer) :: {:ok, t()} | {:error, String.t()}
  def derive_public_child(xkey, idx) do
    cond do
      xkey.prefix in @prv_prefixes ->
        {:ok, child_xprv} = derive_private_child(xkey, idx)
        to_extended_public_key(child_xprv)

      idx >= DerivationPath.min_hardened_child_num() or idx < 0 ->
        {:error, "idx must be in 0..2**31-1"}

      true ->
        child_depth = incr(xkey.depth)

        i =
          idx
          |> :binary.encode_unsigned()
          |> Bitcoinex.Utils.pad(4, :leading)

        fingerprint =
          xkey.key
          |> Bitcoinex.Utils.hash160()
          |> :binary.part(0, 4)

        ent = get_pub_child_entropy(xkey, idx)
        child_chaincode = :binary.part(ent, byte_size(ent), -32)

        key_secret =
          ent
          |> :binary.part(0, 32)
          |> :binary.decode_unsigned()

        if key_secret >= Params.curve().n do
          {:error, "invalid key derived. Bad luck!"}
        else
          {:ok, parent_pubkey} = Point.parse_public_key(xkey.key)
          {:ok, prvkey} = PrivateKey.new(key_secret)

          pubkey =
            prvkey
            |> PrivateKey.to_point()
            |> Math.add(parent_pubkey)

          if Point.is_inf(pubkey) do
            {:error, "pubkey is point at infinity, bad luck!"}
          else
            (xkey.prefix <>
               child_depth <> fingerprint <> i <> child_chaincode <> Point.sec(pubkey))
            |> Base58.append_checksum()
            |> parse_extended_key()
          end
        end
    end
  end

  @doc """
    derive_private_child uses a private key xkey to
    derive the private key at index idx
  """
  @spec derive_private_child(t(), non_neg_integer()) :: {:ok, t()} | {:error, String.t()}
  def derive_private_child(_, idx) when idx >>> 32 != 0, do: {:error, "idx must be in 0..2**32-1"}

  def derive_private_child(%{prefix: prefix}, _) when prefix not in @prv_prefixes,
    do: {:error, "public key cannot derive private child"}

  def derive_private_child(xkey, idx) do
    child_depth = incr(xkey.depth)

    i =
      idx
      |> :binary.encode_unsigned()
      |> Bitcoinex.Utils.pad(4, :leading)

    key_secret =
      xkey.key
      |> :binary.decode_unsigned()

    try do
      {:ok, prvkey} = PrivateKey.new(key_secret)

      fingerprint =
        prvkey
        |> PrivateKey.to_point()
        |> Point.sec()
        |> Bitcoinex.Utils.hash160()
        |> :binary.part(0, 4)

      ent = get_prv_child_entropy(xkey, idx)
      child_chaincode = :binary.part(ent, byte_size(ent), -32)

      child_key =
        ent
        |> :binary.part(0, 32)
        |> :binary.decode_unsigned()
        |> Kernel.+(key_secret)
        |> Bitcoinex.Secp256k1.Math.modulo(Params.curve().n)
        |> :binary.encode_unsigned()

      (xkey.prefix <> child_depth <> fingerprint <> i <> child_chaincode <> <<0>> <> child_key)
      |> Base58.append_checksum()
      |> parse_extended_key()
    rescue
      _ in MatchError -> {:error, "invalid private key in extended private key"}
    end
  end

  # increment byte by 1
  defp incr(byte) do
    byte
    |> :binary.decode_unsigned()
    |> Kernel.+(1)
    |> :binary.encode_unsigned()
  end

  # BIP32 spec
  defp get_prv_child_entropy(xprv, idx) do
    i =
      idx
      |> :binary.encode_unsigned()
      |> Bitcoinex.Utils.pad(4, :leading)

    if idx >= DerivationPath.min_hardened_child_num() do
      # hardened child from priv key
      :crypto.mac(:hmac, :sha512, xprv.chaincode, xprv.key <> i)
    else
      # unhardened child from privkey
      {:ok, prvkey} = PrivateKey.new(:binary.decode_unsigned(xprv.key))

      pubkey =
        prvkey
        |> PrivateKey.to_point()
        |> Point.sec()

      :crypto.mac(:hmac, :sha512, xprv.chaincode, pubkey <> i)
    end
  end

  defp get_pub_child_entropy(xpub, idx) do
    i =
      idx
      |> :binary.encode_unsigned()
      |> Bitcoinex.Utils.pad(4, :leading)

    :crypto.mac(:hmac, :sha512, xpub.chaincode, xpub.key <> i)
  end

  @doc """
    derive_extended_key uses an extended xkey and a derivation
    path to derive the extended key at that path
  """
  @spec derive_extended_key(t() | binary, DerivationPath.t()) :: {:ok, t()} | {:error, String.t()}
  def derive_extended_key(xkey = %__MODULE__{}, %DerivationPath{child_nums: path}),
    do: rderive_extended_key(xkey, path)

  def derive_extended_key(seed, %DerivationPath{child_nums: path}) do
    {:ok, xkey} = seed_to_master_private_key(seed)
    rderive_extended_key(xkey, path)
  end

  defp rderive_extended_key(xkey = %__MODULE__{}, []), do: {:ok, xkey}

  defp rderive_extended_key(xkey = %__MODULE__{}, [p | rest]) do
    try do
      case p do
        # if asterisk (:any) is in path, return the immediate parent xkey
        :any ->
          {:ok, xkey}

        # otherwise it is an integer, so derive child at that index.
        _ ->
          case derive_child_key(xkey, p) do
            {:ok, child_key} -> rderive_extended_key(child_key, rest)
            {:error, msg} -> {:error, msg}
          end
      end
    rescue
      e in ArgumentError -> {:error, e.message}
    end
  end
end
