defmodule MyApp.Data.User do
  use Ecto.Schema
  import Ecto.Query
  import Ecto.Changeset
  alias MyApp.Repo
  alias MyApp.Data

  @derive {Poison.Encoder, except: [:__meta__, :__struct__]}
  schema "user" do
    field :battle_net_id, :integer
    field :battletag, :string
    field :permissions, :string, default: "user" # admin, mod, user
    field :character_guild, :string
    field :character_name, :string
    field :character_class, :string
    field :character_realm, :string
    field :character_avatar, :string

    # for admin purposes
    field :username, :string
    field :password, :string
    timestamps(type: :utc_datetime)
  end

  defp changeset(user, params \\ %{}) do
    user
    |> cast(params, [:battle_net_id, :battletag])
    |> validate_required([:battle_net_id, :battletag])
    |> unique_constraint(:battle_net_id)
  end

  defp update_char_changeset(user, params \\ %{}) do
    user
    |> cast(params, [:character_guild, :character_name, :character_class, :character_realm, :character_avatar])
    |> validate_required([:character_name, :character_class, :character_realm, :character_avatar])
  end

  def update_character(params) do
    {:ok, data} = Repo.transaction(fn ->
      user = Repo.get(__MODULE__, Map.get(params, "id"))
      # remove columns from data because we need to update all of them
      |> Map.drop([:character_realm, :character_name, :character_guild, :character_class, :character_avatar])
      output = user
      |> update_char_changeset(params)
      |> Repo.update
      |> Data.Util.process_insert_or_update
    end)
    data
  end

  @spec get_user(integer) :: nil | map
  defp get_user(battle_net_id) do
    query = from u in "user",
      where: u.battle_net_id == ^battle_net_id,
      select: [:id, :permissions, :battle_net_id, :battletag, :character_guild, :character_name, :character_class, :character_realm, :character_avatar]
    Repo.one(query)
  end

  # insert user info in database - if not exists - update battletag if it has changed
  @spec upsert_user(%{"battle_net_id": integer, "battletag": String.t, "access_token": String.t} | tuple) :: {:ok, map} | {:error, any}
  def upsert_user(params) when is_map(params) do
    # check for current user in database
    user = get_user(Map.get(params, "battle_net_id"))

    output = cond do
      is_nil(user) ->
        insert_battlenet_user(params)
      true ->
        if Map.get(user, :battletag) != Map.get(params, "battletag") do
          update_battletag(user, params)
        else
          {:ok, user}
        end
    end

    output
    |> add_extra_params(params)
  end
  def upsert_user({:ok, params}), do: upsert_user(params)
  def upsert_user({:error, error}), do: {:error, error}

  # need to add token back to map because we don't store it in the database
  defp add_extra_params({:error, error}, _), do: {:error, error}
  defp add_extra_params({:ok, user}, params) do
    {:ok, Map.merge(user, %{access_token: params["access_token"], expires_in: params["expires_in"]})}
  end

  defp insert_battlenet_user(params) do
    changeset(%Data.User{}, params)
    |> Repo.insert
    |> Data.Util.process_insert_or_update
    |> filter_values
  end

  # it's possible for a user's battle tag to change - if so update it
  defp update_battletag(user, params) do
    changeset(Map.merge(%Data.User{}, user), %{battletag: Map.get(params, "battletag")})
    |> Repo.update
    |> Data.Util.process_insert_or_update
    |> filter_values
  end

  # take certain values after insertion
  defp filter_values({:error, error}), do: {:error, error}
  defp filter_values({:ok, user}), do: {:ok, Map.take(user, [:id, :permissions, :battle_net_id, :battletag])}

  def insert_admin_user(params) do
    params = params
    |> Map.put("password", Comeonin.Argon2.hashpwsalt(Map.get(params, "password")))

    %Data.User{}
    |> cast(params, [:username, :password, :permissions, :character_name, :character_avatar])
    |> Repo.insert
  end

  def login(params) do
    user = Repo.get_by(Data.User, username: Map.get(params, "username"))

    if user do
      case Comeonin.Argon2.checkpw(Map.get(params, "password"), user.password) do
        false -> {:error, "invalid login"}
        _ ->
          user = user
          |> Map.from_struct
          |> Map.drop([:password, :__meta__])
          {:ok, user}
      end
    else
      {:error, "invalid login"}
    end
  end

end
