/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include "Authentication.h"
#include <future>

static const std::string PARAM_TLS_CERT = "certificatePath";
static const std::string PARAM_TLS_KEY = "privateKeyPath";
static const std::string PARAM_TOKEN = "token";
static const std::string PARAM_USERNAME = "username";
static const std::string PARAM_PASSWORD = "password";

void FinalizeTokenSupplierCallback(Napi::Env env, TokenSupplierCallback *cb, void *) { delete cb; }

struct TokenSupplierProxyData {
  std::function<void(void)> callback;
  std::string token;

  TokenSupplierProxyData(std::function<void(void)> callback) : callback(callback), token(std::string()) {}
};

void TokenSupplierProxy(Napi::Env env, Napi::Function jsCallback, TokenSupplierProxyData *data) {
  Napi::Value ret = jsCallback.Call({});
  if (ret.IsPromise()) {
    Napi::Promise promise = ret.As<Napi::Promise>();
    Napi::Value thenValue = promise.Get("then");
    if (thenValue.IsFunction()) {
      Napi::Function then = thenValue.As<Napi::Function>();
      Napi::Function callback = Napi::Function::New(env, [data](const Napi::CallbackInfo &info) {
        Napi::Value value = info[0];
        if (value.IsString()) {
          data->token = value.ToString().Utf8Value();
        }
        data->callback();
      });
      then.Call(promise, {callback});
      return;
    }
  }
  if (ret.IsString()) {
    data->token = ret.ToString().Utf8Value();
  }
  data->callback();
}

char *TokenSupplier(void *ctx) {
  TokenSupplierCallback *tokenSupplierCallback = (TokenSupplierCallback *)ctx;
  if (tokenSupplierCallback->callback.Acquire() != napi_ok) {
    char *empty = (char *)malloc(0);
    return empty;
  }

  std::promise<void> promise;
  std::future<void> future = promise.get_future();

  std::unique_ptr<TokenSupplierProxyData> dataPtr(
      new TokenSupplierProxyData([&promise]() { promise.set_value(); }));

  tokenSupplierCallback->callback.BlockingCall(dataPtr.get(), TokenSupplierProxy);
  tokenSupplierCallback->callback.Release();

  future.wait();

  char *token = (char *)malloc(dataPtr->token.size() + 1);
  strcpy(token, dataPtr->token.c_str());
  return token;
}

Napi::FunctionReference Authentication::constructor;

Napi::Object Authentication::Init(Napi::Env env, Napi::Object exports) {
  Napi::HandleScope scope(env);

  Napi::Function func = DefineClass(env, "Authentication", {});

  constructor = Napi::Persistent(func);
  constructor.SuppressDestruct();

  exports.Set("Authentication", func);
  return exports;
}

Authentication::Authentication(const Napi::CallbackInfo &info)
    : Napi::ObjectWrap<Authentication>(info), cAuthentication(nullptr), tokenSupplier(nullptr) {
  Napi::Env env = info.Env();
  Napi::HandleScope scope(env);

  if (info.Length() < 1 || !info[0].IsString() || info[0].ToString().Utf8Value().empty()) {
    Napi::Error::New(env, "Authentication method is not specified").ThrowAsJavaScriptException();
    return;
  }

  std::string authMethod = info[0].ToString().Utf8Value();

  if (authMethod == "tls" || authMethod == "token" || authMethod == "basic") {
    if (info.Length() < 2 || !info[1].IsObject()) {
      Napi::Error::New(env, "Authentication parameter must be a object").ThrowAsJavaScriptException();
      return;
    }

    Napi::Object obj = info[1].ToObject();

    if (authMethod == "tls") {
      if (!obj.Has(PARAM_TLS_CERT) || !obj.Get(PARAM_TLS_CERT).IsString() || !obj.Has(PARAM_TLS_KEY) ||
          !obj.Get(PARAM_TLS_KEY).IsString()) {
        Napi::Error::New(env, "Missing required parameter").ThrowAsJavaScriptException();
        return;
      }
      this->cAuthentication =
          pulsar_authentication_tls_create(obj.Get(PARAM_TLS_CERT).ToString().Utf8Value().c_str(),
                                           obj.Get(PARAM_TLS_KEY).ToString().Utf8Value().c_str());
    } else if (authMethod == "token") {
      if (obj.Has(PARAM_TOKEN)) {
        if (obj.Get(PARAM_TOKEN).IsString()) {
          this->cAuthentication =
              pulsar_authentication_token_create(obj.Get(PARAM_TOKEN).ToString().Utf8Value().c_str());
          return;
        }

        if (obj.Get(PARAM_TOKEN).IsFunction()) {
          this->tokenSupplier = new TokenSupplierCallback();
          Napi::ThreadSafeFunction callback = Napi::ThreadSafeFunction::New(
              obj.Env(), obj.Get(PARAM_TOKEN).As<Napi::Function>(), "Token Supplier Callback", 1, 1,
              (void *)NULL, FinalizeTokenSupplierCallback, this->tokenSupplier);
          this->tokenSupplier->callback = std::move(callback);

          this->cAuthentication =
              pulsar_authentication_token_create_with_supplier(&TokenSupplier, this->tokenSupplier);
          return;
        }
      }

      Napi::Error::New(env, "Missing required parameter").ThrowAsJavaScriptException();
      return;
    } else if (authMethod == "basic") {
      if (!obj.Has(PARAM_USERNAME) || !obj.Get(PARAM_USERNAME).IsString() || !obj.Has(PARAM_PASSWORD) ||
          !obj.Get(PARAM_PASSWORD).IsString()) {
        Napi::Error::New(env, "Missing required parameter").ThrowAsJavaScriptException();
        return;
      }
      this->cAuthentication =
          pulsar_authentication_basic_create(obj.Get(PARAM_USERNAME).ToString().Utf8Value().c_str(),
                                             obj.Get(PARAM_PASSWORD).ToString().Utf8Value().c_str());
    }
  } else if (authMethod == "athenz") {
    if (info.Length() < 2 || !info[1].IsString()) {
      Napi::Error::New(env, "Authentication parameter must be a JSON string").ThrowAsJavaScriptException();
      return;
    }
    this->cAuthentication = pulsar_authentication_athenz_create(info[1].ToString().Utf8Value().c_str());
  } else if (authMethod == "oauth2") {
    if (info.Length() < 2 || !info[1].IsString()) {
      Napi::Error::New(env, "Authentication parameter must be a JSON string for oauth2")
          .ThrowAsJavaScriptException();
      return;
    }
    this->cAuthentication = pulsar_authentication_oauth2_create(info[1].ToString().Utf8Value().c_str());
  } else {
    Napi::Error::New(env, "Unsupported authentication method").ThrowAsJavaScriptException();
    return;
  }
}

Authentication::~Authentication() {
  if (this->tokenSupplier != nullptr) {
    this->tokenSupplier->callback.Release();
  }
  if (this->cAuthentication != nullptr) {
    pulsar_authentication_free(this->cAuthentication);
  }
}

pulsar_authentication_t *Authentication::GetCAuthentication() { return this->cAuthentication; }
