defmodule TeslaMate.Api do
  use GenServer

  require Logger

  alias TeslaMate.Auth.Tokens
  alias TeslaMate.{Vehicles, Convert}
  alias TeslaApi.Auth

  alias Finch.Response

  import Core.Dependency, only: [call: 3, call: 2]

  defmodule State do
    defstruct name: nil, deps: %{}, refresh_timer: nil
  end

  @timeout :timer.minutes(2)
  @name __MODULE__

  # API

  def start_link(opts) do
    opts = Keyword.put_new(opts, :name, @name)
    name = Keyword.fetch!(opts, :name)
    GenServer.start_link(__MODULE__, opts, name: name)
  end

  ## State

  def list_vehicles(name \\ @name) do
    with {:ok, auth} <- fetch_auth(name) do
      TeslaApi.Vehicle.list(auth)
      |> handle_result(auth, name)
    end
  end

  def get_vehicle(name \\ @name, id) do
    with {:ok, auth} <- fetch_auth(name) do
      TeslaApi.Vehicle.get(auth, id)
      |> handle_result(auth, name)
    end
  end

  def get_vehicle_with_state(name \\ @name, id) do
    with {:ok, auth} <- fetch_auth(name) do
      TeslaApi.Vehicle.get_with_state(auth, id)
      |> handle_result(auth, name)
    end
  end

  def stream(name \\ @name, vid, receiver) do
    with {:ok, %Auth{} = auth} <- fetch_auth(name) do
      TeslaApi.Stream.start_link(auth: auth, vehicle_id: vid, receiver: receiver)
    end
  end

  ## Internals

  def signed_in?(name \\ @name) do
    case fetch_auth(name) do
      {:error, :not_signed_in} -> false
      {:ok, _} -> true
    end
  end

  def sign_in(name \\ @name, args)

  def sign_in(name, %Tokens{} = tokens) do
    case fetch_auth(name) do
      {:error, :not_signed_in} -> GenServer.call(name, {:sign_in, [tokens]}, @timeout)
      {:ok, %Auth{}} -> {:error, :already_signed_in}
    end
  end

  def sign_in(name, {email, password}) do
    case fetch_auth(name) do
      {:error, :not_signed_in} -> GenServer.call(name, {:sign_in, [email, password]}, @timeout)
      {:ok, %Auth{}} -> {:error, :already_signed_in}
    end
  end

  def sign_out(name \\ @name) do
    true = :ets.delete(name, :auth)
    :ok
  rescue
    _ in ArgumentError -> {:error, :not_signed_in}
  end

  # Callbacks

  @impl true
  def init(opts) do
    name = Keyword.fetch!(opts, :name)

    deps = %{
      auth: Keyword.get(opts, :auth, TeslaMate.Auth),
      vehicles: Keyword.get(opts, :vehicles, Vehicles)
    }

    :ok =
      :fuse.install(
        fuse_name(name),
        {{:standard, 5, :timer.minutes(10)}, {:reset, :timer.hours(9999)}}
      )

    ^name = :ets.new(name, [:named_table, :set, :public, read_concurrency: true])
    state = %State{name: name, deps: deps}

    state =
      case call(deps.auth, :get_tokens) do
        %Tokens{access: at, refresh: rt} when is_binary(at) and is_binary(rt) ->
          restored_tokens = %Auth{token: at, refresh_token: rt, expires_in: 10 * 60}

          {:ok, state} =
            case refresh_tokens(restored_tokens) do
              {:ok, refreshed_tokens} ->
                :ok = call(deps.auth, :save, [refreshed_tokens])
                true = insert_auth(name, refreshed_tokens)
                schedule_refresh(refreshed_tokens, state)

              {:error, reason} ->
                Logger.warning("Token refresh failed: #{inspect(reason, pretty: true)}")
                true = insert_auth(name, restored_tokens)
                schedule_refresh(restored_tokens, state)
            end

          state

        %Tokens{access: :error, refresh: :error} ->
          Logger.warning("Could not decrypt API tokens!")
          state

        _ ->
          state
      end

    {:ok, state}
  end

  @impl true
  def handle_call({:sign_in, args}, _, %State{} = state) do
    case args do
      [args, callback] when is_function(callback) -> apply(callback, args)
      [%Tokens{} = t] -> Auth.refresh(%Auth{token: t.access, refresh_token: t.refresh})
    end
    |> case do
      {:ok, %Auth{} = auth} ->
        true = insert_auth(state.name, auth)
        :ok = call(state.deps.auth, :save, [auth])
        :ok = call(state.deps.vehicles, :restart)
        {:ok, state} = schedule_refresh(auth, state)
        :ok = :fuse.reset(fuse_name(state.name))

        {:reply, :ok, state}

      {:ok, {:captcha, captcha, callback}} ->
        wrapped_callback = fn captcha_code ->
          GenServer.call(state.name, {:sign_in, [[captcha_code], callback]}, @timeout)
        end

        {:reply, {:ok, {:captcha, captcha, wrapped_callback}}, state}

      {:ok, {:mfa, devices, callback}} ->
        wrapped_callback = fn device_id, mfa_passcode ->
          GenServer.call(state.name, {:sign_in, [[device_id, mfa_passcode], callback]}, @timeout)
        end

        {:reply, {:ok, {:mfa, devices, wrapped_callback}}, state}

      {:error, %TeslaApi.Error{} = e} ->
        {:reply, {:error, e}, state}
    end
  end

  @impl true
  def handle_info(:refresh_auth, %State{name: name} = state) do
    case fetch_auth(name) do
      {:ok, tokens} ->
        Logger.info("Refreshing access token ...")

        case Auth.refresh(tokens) do
          {:ok, refreshed_tokens} ->
            true = insert_auth(name, refreshed_tokens)
            :ok = call(state.deps.auth, :save, [refreshed_tokens])
            {:ok, state} = schedule_refresh(refreshed_tokens, state)
            :ok = :fuse.reset(fuse_name(name))
            {:noreply, state}

          {:error, reason} ->
            Logger.warning("Token refresh failed: #{inspect(reason, pretty: true)}")
            Logger.warning("Retrying in 5 minutes...")

            if is_reference(state.refresh_timer), do: Process.cancel_timer(state.refresh_timer)
            refresh_timer = Process.send_after(self(), :refresh_auth, :timer.minutes(5))

            {:noreply, %State{state | refresh_timer: refresh_timer}}
        end

      {:error, reason} ->
        Logger.warning("Cannot refresh access token: #{inspect(reason)}")
        {:noreply, state}
    end
  end

  def handle_info(msg, state) do
    Logger.info("#{__MODULE__} / unhandled message: #{inspect(msg, pretty: true)}")
    {:noreply, state}
  end

  ## Private

  defp refresh_tokens(%Auth{} = tokens) do
    case Application.get_env(:teslamate, :disable_token_refresh, false) do
      true ->
        Logger.info("Token refresh is disabled")
        {:ok, tokens}

      false ->
        with {:ok, %Auth{} = refresh_tokens} <- Auth.refresh(tokens) do
          Logger.info("Refreshed api tokens")
          {:ok, refresh_tokens}
        end
    end
  end

  defp schedule_refresh(%Auth{} = auth, %State{} = state) do
    ms =
      auth.expires_in
      |> Kernel.*(0.75)
      |> round()
      |> :timer.seconds()

    duration =
      ms
      |> div(1000)
      |> Convert.sec_to_str()
      |> Enum.reject(&String.ends_with?(&1, "s"))
      |> Enum.join(" ")

    Logger.info("Scheduling token refresh in #{duration}")

    if is_reference(state.refresh_timer), do: Process.cancel_timer(state.refresh_timer)
    refresh_timer = Process.send_after(self(), :refresh_auth, ms)

    {:ok, %State{state | refresh_timer: refresh_timer}}
  end

  defp insert_auth(name, %Auth{} = auth) do
    :ets.insert(name, auth: auth)
  end

  defp fetch_auth(name) do
    case :ets.lookup(name, :auth) do
      [auth: %Auth{} = auth] -> {:ok, auth}
      [] -> {:error, :not_signed_in}
    end
  rescue
    _ in ArgumentError -> {:error, :not_signed_in}
  end

  defp handle_result(result, auth, name) do
    case result do
      {:error, %TeslaApi.Error{reason: :unauthorized}} ->
        :ok = :fuse.melt(fuse_name(name))

        case :fuse.ask(fuse_name(name), :sync) do
          :blown ->
            true = :ets.delete(name, :auth)
            {:error, :not_signed_in}

          :ok ->
            send(name, :refresh_auth)
            {:error, :unauthorized}
        end

      {:error, %TeslaApi.Error{reason: reason, env: %Response{status: status, body: body}}} ->
        Logger.error("TeslaApi.Error / #{status} – #{inspect(body, pretty: true)}")
        {:error, reason}

      {:error, %TeslaApi.Error{reason: :too_many_request, message: retry_after}} ->
        Logger.warning("TeslaApi.Error / :too_many_request #{retry_after}")
        {:error, :too_many_request, retry_after}

      {:error, %TeslaApi.Error{reason: reason, message: msg}} ->
        if is_binary(msg) and msg != "", do: Logger.warning("TeslaApi.Error / #{msg}")
        {:error, reason}

      {:ok, vehicles} when is_list(vehicles) ->
        vehicles =
          vehicles
          |> Task.async_stream(&preload_vehicle(&1, auth), timeout: 32_500)
          |> Enum.map(fn {:ok, vehicle} -> vehicle end)

        {:ok, vehicles}

      {:ok, %TeslaApi.Vehicle{} = vehicle} ->
        {:ok, vehicle}
    end
  end

  defp preload_vehicle(%TeslaApi.Vehicle{state: "online", id: id} = vehicle, auth) do
    case TeslaApi.Vehicle.get_with_state(auth, id) do
      {:ok, %TeslaApi.Vehicle{} = vehicle} ->
        vehicle

      {:error, reason} ->
        Logger.warning("TeslaApi.Error / #{inspect(reason, pretty: true)}")
        vehicle
    end
  end

  defp preload_vehicle(%TeslaApi.Vehicle{} = vehicle, _state), do: vehicle

  defp fuse_name(name), do: :"#{name}.unauthorized"
end
