// Copyright 2021 The Pigweed Authors
//
// Licensed 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
//
//     https://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 "pw_assert/check.h"
#include "pw_log/log.h"
#include "pw_tls_client/entropy.h"
#include "pw_tls_client/session.h"
#include "pw_tls_client_mbedtls/backend_types.h"

namespace pw::tls_client {
namespace backend {

int SessionImplementation::MbedTlsWrite(void* ctx,
                                        const uint8_t* buf,
                                        size_t len) {
  PW_CHECK_NOTNULL(ctx);
  PW_CHECK_NOTNULL(buf);
  auto writer =
      static_cast<SessionImplementation*>(ctx)->session_options_.transport();
  PW_CHECK_NOTNULL(writer);
  return writer->Write(buf, len).ok() ? len : -1;
}

int SessionImplementation::MbedTlsRead(void* ctx,
                                       unsigned char* buf,
                                       size_t len) {
  PW_CHECK_NOTNULL(ctx);
  PW_CHECK_NOTNULL(buf);
  auto reader =
      static_cast<SessionImplementation*>(ctx)->session_options_.transport();
  PW_CHECK_NOTNULL(reader);
  auto res = reader->Read(buf, len);
  if (!res.ok()) {
    return -1;
  }
  return res.value().empty() ? MBEDTLS_ERR_SSL_WANT_READ : res.value().size();
}

Status SessionImplementation::entropy_source_status_ = OkStatus();

void SessionImplementation::SetEntropySourceStatus(Status status) {
  entropy_source_status_ = status;
}

// Entropy source callback
int SessionImplementation::MbedTlsEntropySource(void* ctx,
                                                unsigned char* out,
                                                size_t len,
                                                size_t* output_length) {
  Status status;
  if (entropy_source_status_ != OkStatus()) {
    status = entropy_source_status_;
  } else {
    status = GetRandomBytes({out, len});
  }

  if (!status.ok()) {
    PW_LOG_DEBUG("Failed to generate random bytes");
    auto session_impl = static_cast<SessionImplementation*>(ctx);
    session_impl->SetTlsStatus(pw::tls_client::TLSStatus::kEntropySourceFailed);
    return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
  }
  *output_length = len;
  return 0;
}

SessionImplementation::SessionImplementation(SessionOptions options)
    : session_options_(options) {
  mbedtls_ssl_init(&ssl_ctx_);
  mbedtls_ssl_config_init(&ssl_config_);
  mbedtls_ctr_drbg_init(&drbg_ctx_);
  mbedtls_entropy_init(&entropy_ctx_);
}

SessionImplementation::~SessionImplementation() {
  mbedtls_ssl_free(&ssl_ctx_);
  mbedtls_ssl_config_free(&ssl_config_);
  mbedtls_ctr_drbg_free(&drbg_ctx_);
  mbedtls_entropy_free(&entropy_ctx_);
}

Status SessionImplementation::Setup() {
  int ret = 0;

  // Set up default configuration.
  ret = mbedtls_ssl_config_defaults(
      &ssl_config_,
      // Configured as client.
      MBEDTLS_SSL_IS_CLIENT,
      // Statndard TLS. The other option is MBEDTLS_SSL_TRANSPORT_DATAGRAM
      // for DTLS, which we'll consider later.
      MBEDTLS_SSL_TRANSPORT_STREAM,
      // This option is used in all MbedTLS native examples.
      // The other option is MBEDTLS_SSL_PRESET_SUITEB.
      // However, there is no document/comment availalbe on what they do.
      // Base on the source code, these options will restrict the version
      // of TLS protocol. MBEDTLS_SSL_PRESET_SUITEB forces TLS 1.2.
      // MBEDTLS_SSL_PRESET_DEFAULT is more relaxed. But since we
      // define MBEDTLS_SSL_PROTO_TLS1_2 for all configs. There shouldn't be
      // any difference.
      MBEDTLS_SSL_PRESET_DEFAULT);
  if (ret) {
    return Status::Internal();
  }

  // Set up an entropy source.
  ret = mbedtls_entropy_add_source(&entropy_ctx_,
                                   MbedTlsEntropySource,
                                   this,
                                   1,
                                   MBEDTLS_ENTROPY_SOURCE_STRONG);
  if (ret) {
    return Status::Internal();
  }

  // Set up drbg.
  unsigned char personalized_bytes[] = "pw_tls_client";
  ret = mbedtls_ctr_drbg_seed(&drbg_ctx_,
                              mbedtls_entropy_func,
                              &entropy_ctx_,
                              personalized_bytes,
                              sizeof(personalized_bytes));
  if (ret) {
    if (ret == MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED) {
      tls_status_ = TLSStatus::kEntropySourceFailed;
    }
    return Status::Internal();
  }

  // The API does not fail.
  mbedtls_ssl_conf_rng(&ssl_config_, mbedtls_ctr_drbg_random, &drbg_ctx_);

  // The API does not fail.
  mbedtls_ssl_conf_authmode(&ssl_config_, MBEDTLS_SSL_VERIFY_REQUIRED);

  // TODO: b/235289501 - Add logic for loading trust anchors.

  // Load configuration to SSL.
  ret = mbedtls_ssl_setup(&ssl_ctx_, &ssl_config_);
  if (ret) {
    return Status::Internal();
  }

  // Set up transport.
  // The API does not fail.
  mbedtls_ssl_set_bio(&ssl_ctx_, this, MbedTlsWrite, MbedTlsRead, nullptr);

  ret = mbedtls_ssl_set_hostname(&ssl_ctx_,
                                 session_options_.server_name().data());
  if (ret) {
    return Status::Internal();
  }

  return OkStatus();
}

}  // namespace backend

Session::Session(const SessionOptions& options) : session_impl_(options) {}

Session::~Session() = default;

Result<Session*> Session::Create(const SessionOptions& options) {
  if (!options.transport()) {
    PW_LOG_DEBUG("Must provide a transport");
    return Status::Internal();
  }

  auto sess = new Session(options);
  if (!sess) {
    return Status::ResourceExhausted();
  }

  // Set up the client.
  auto setup_status = sess->session_impl_.Setup();
  if (!setup_status.ok()) {
    PW_LOG_DEBUG("Failed to setup");
    // TODO: b/235289501 - `tls_status_` may be set, but the session object will
    // be released. Map `tls_stauts_` to string and print out here so that
    // the information can be catched.
    delete sess;
    return setup_status;
  }

  return sess;
}

Status Session::Open() {
  // TODO: b/235289501 - To implement
  return Status::Unimplemented();
}

Status Session::Close() {
  // TODO: b/235289501 - To implement
  return Status::Unimplemented();
}

StatusWithSize Session::DoRead(ByteSpan) {
  // TODO: b/235289501 - To implement
  return StatusWithSize(Status::Unimplemented(), 0);
}

Status Session::DoWrite(ConstByteSpan) {
  // TODO: b/235289501 - To implement
  return Status::Unimplemented();
}

TLSStatus Session::GetLastTLSStatus() { return session_impl_.GetTlsStatus(); }

}  // namespace pw::tls_client
