/*
 * Copyright (C) AC SOFTWARE SP. Z O.O
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#include "esp_idf_client.h"

#include <SuplaDevice.h>
#include <esp_netif.h>
#include <esp_tls.h>
#include <fcntl.h>
#include <lwip/netif.h>
#include <lwip/sockets.h>
#include <mbedtls/pk.h>
#include <stdio.h>
#include <string.h>
#include <supla/auto_lock.h>
#include <supla/log_wrapper.h>
#include <supla/mutex.h>
#include <supla/time.h>

#include "supla/network/client.h"

#ifndef SUPLA_DEVICE_ESP32
// delete name variant is deprecated in ESP-IDF, however ESP8266 RTOS still
// use it.
#define esp_tls_conn_destroy esp_tls_conn_delete

// Latest ESP-IDF moved definition of esp_tls_t to private section and added
// methods to access members. This change is missing in esp8266, so below
// method is added to keep the same functionality
void esp_tls_get_error_handle(esp_tls_t *client,
                              esp_tls_error_handle_t *errorHandle) {
  *errorHandle = client->error_handle;
}

#endif

Supla::EspIdfClient::EspIdfClient() {
  mutex = Supla::Mutex::Create();
}

Supla::EspIdfClient::~EspIdfClient() {
}

#if !defined(MBEDTLS_PK_HAVE_PRIVATE_HEADER)
[[maybe_unused]]
static const char *pkTypeStr(mbedtls_pk_type_t t) {
  switch (t) {
    case MBEDTLS_PK_RSA:
      return "RSA";
    case MBEDTLS_PK_ECKEY:
      return "EC (ECKEY)";
    case MBEDTLS_PK_ECDSA:
      return "ECDSA";
    default:
      return "OTHER";
  }
}
#endif
int Supla::EspIdfClient::connectImp(const char *host, uint16_t port) {
  Supla::AutoLock autoLock(mutex);
  if (client != nullptr) {
    SUPLA_LOG_ERROR("client ptr should be null when trying to connect");
    return 0;
  }
  srcIp = 0;

  esp_tls_cfg_t cfg = {};
  if (rootCACert) {
    cfg.cacert_pem_buf = reinterpret_cast<const unsigned char *>(rootCACert);
    int len = strlen(rootCACert);
    cfg.cacert_pem_bytes = len + 1;
  }
  cfg.timeout_ms = timeoutMs;
  cfg.non_block = false;

  bool isFirstConnectAfterInit = firstConnectAfterInit;
  firstConnectAfterInit = false;

  client = esp_tls_init();
  if (!client) {
    SUPLA_LOG_ERROR("ESP TLS INIT FAILED");
    return 0;
  }
  int result = esp_tls_conn_new_sync(host, strlen(host), port, &cfg, client);
  if (result == 1) {
    isConnected = true;
    int socketFd = 0;
    if (esp_tls_get_conn_sockfd(client, &socketFd) == ESP_OK) {
      fcntl(socketFd, F_SETFL, O_NONBLOCK);

      // store connection source IP address
      struct sockaddr_in addr = {};
      socklen_t addrLen = sizeof(addr);
      getsockname(socketFd, (struct sockaddr *)&addr, &addrLen);
      struct ifreq ifr = {};
      strncpy(ifr.ifr_name, inet_ntoa(addr.sin_addr), IFNAMSIZ);
      srcIp = addr.sin_addr.s_addr;
      uint8_t ipArr[4];
      for (int i = 0; i < 4; i++) {
        ipArr[i] = (srcIp >> (i * 8)) & 0xFF;
      }
      (void)(ipArr);

      mbedtls_ssl_context *ssl =
          static_cast<mbedtls_ssl_context *>(esp_tls_get_ssl_context(client));

      SUPLA_LOG_DEBUG("TLS version: %s", mbedtls_ssl_get_version(ssl));
      SUPLA_LOG_DEBUG("Cipher suite: %s", mbedtls_ssl_get_ciphersuite(ssl));

      const mbedtls_x509_crt *peer = mbedtls_ssl_get_peer_cert(ssl);
      if (!peer) {
        SUPLA_LOG_ERROR(
            "No peer certificate (unexpected for normal TLS server auth)");
      } else {
#if !defined(MBEDTLS_PK_HAVE_PRIVATE_HEADER)
        mbedtls_pk_type_t kt = mbedtls_pk_get_type(&peer->pk);
        (void)(kt);
        SUPLA_LOG_DEBUG("Server cert key type: %s", pkTypeStr(kt));
#endif
        SUPLA_LOG_DEBUG("Server cert key bits: %u",
                        (unsigned)mbedtls_pk_get_bitlen(&peer->pk));
      }

      SUPLA_LOG_DEBUG("Connected via IP %d.%d.%d.%d",
                      ipArr[0],
                      ipArr[1],
                      ipArr[2],
                      ipArr[3]);
    }
  } else {
    esp_tls_error_handle_t errorHandle;
    esp_tls_get_error_handle(client, &errorHandle);

    SUPLA_LOG_DEBUG("last errors %d %d %d",
                    errorHandle->last_error,
                    errorHandle->esp_tls_error_code,
                    errorHandle->esp_tls_flags);
    if (!isFirstConnectAfterInit) {
      logConnReason(errorHandle->last_error,
                    errorHandle->esp_tls_error_code,
                    errorHandle->esp_tls_flags,
                    host);
    }
    isConnected = false;
    esp_tls_conn_destroy(client);
    client = nullptr;
  }

  // SuplaDevice expects 1 on success, which is the same
  // as esp_tls_conn_new_sync returned values
  return result;
}

std::size_t Supla::EspIdfClient::writeImp(const uint8_t *buf,
                                          std::size_t size) {
  Supla::AutoLock autoLock(mutex);
  if (client == nullptr) {
    return 0;
  }
  int sendSize = esp_tls_conn_write(client, buf, size);
  if (sendSize == 0) {
    isConnected = false;
  }
  return sendSize;
}

int Supla::EspIdfClient::available() {
  Supla::AutoLock autoLock(mutex);
  if (client == nullptr) {
    return 0;
  }

  esp_tls_conn_read(client, nullptr, 0);
  int tlsErr = 0;
  esp_tls_error_handle_t errorHandle = {};
  esp_tls_get_error_handle(client, &errorHandle);
  esp_tls_get_and_clear_last_error(errorHandle, &tlsErr, nullptr);
  autoLock.unlock();
  if (tlsErr != 0 && -tlsErr != ESP_TLS_ERR_SSL_WANT_READ &&
      -tlsErr != ESP_TLS_ERR_SSL_WANT_WRITE) {
    SUPLA_LOG_ERROR("Connection error %d", tlsErr);
    stop();
    return 0;
  }
  autoLock.lock();
  if (client == nullptr) {
    return 0;
  }
  _supla_int_t size = esp_tls_get_bytes_avail(client);
  autoLock.unlock();
  if (size < 0) {
    SUPLA_LOG_ERROR("error in esp tls get bytes avail %d", size);
    stop();
    return 0;
  }
  return size;
}

int Supla::EspIdfClient::readImp(uint8_t *buf, std::size_t size) {
  if (available() > 0) {
    Supla::AutoLock autoLock(mutex);
    int ret = 0;
    do {
      autoLock.lock();
      if (client == nullptr) {
        return 0;
      }
      ret = esp_tls_conn_read(client, buf, size);
      autoLock.unlock();

      if (ret == ESP_TLS_ERR_SSL_WANT_READ ||
          ret == ESP_TLS_ERR_SSL_WANT_WRITE) {
        delay(1);
        continue;
      }
      if (ret < 0) {
        SUPLA_LOG_ERROR("esp_tls_conn_read  returned -0x%x", -ret);
        ret = 0;
        break;
      }
      if (ret == 0) {
        SUPLA_LOG_INFO("connection closed");
        stop();
        return 0;
      }
      break;
    } while (1);

    return ret;
  }

  return -1;
}

void Supla::EspIdfClient::stop() {
  Supla::AutoLock autoLock(mutex);
  isConnected = false;
  if (client != nullptr) {
    esp_tls_conn_destroy(client);
    client = nullptr;
  }
}

uint8_t Supla::EspIdfClient::connected() {
  return isConnected;
}

void Supla::EspIdfClient::logConnReason(int error,
                                        int tlsError,
                                        int tlsFlags,
                                        const char *host) {
  if (sdc && (lastConnErr != error || lastTlsErr != tlsError)) {
    lastConnErr = error;
    lastTlsErr = tlsError;
    char buf[512] = {};
    switch (error) {
      case ESP_ERR_ESP_TLS_CANNOT_RESOLVE_HOSTNAME: {
        snprintf(buf,
                 sizeof(buf),
                 "Connection: can't resolve hostname \"%s\"",
                 host);
        sdc->addLastStateLog(buf);
        break;
      }
      case ESP_ERR_ESP_TLS_FAILED_CONNECT_TO_HOST: {
        snprintf(buf,
                 sizeof(buf),
                 "Connection: failed connect to host \"%s\"",
                 host);
        sdc->addLastStateLog(buf);
        break;
      }
      case ESP_ERR_ESP_TLS_CONNECTION_TIMEOUT: {
        snprintf(buf,
                 sizeof(buf),
                 "Connection: connection timeout to host \"%s\"",
                 host);
        sdc->addLastStateLog(buf);
        break;
      }
      case ESP_ERR_MBEDTLS_SSL_HANDSHAKE_FAILED: {
        switch (tlsError) {
          case -MBEDTLS_ERR_X509_CERT_VERIFY_FAILED: {
            snprintf(buf,
                     sizeof(buf),
                     "Connection TLS: handshake fail - server "
                     "certificate verification error \"%s\"",
                     host);

            sdc->addLastStateLog(buf);
            break;
          }
          default: {
            snprintf(buf,
                     sizeof(buf),
                     "Connection TLS: handshake fail (TLS 0x%X "
                     "flags 0x%x, \"\"%s\")",
                     tlsError,
                     tlsFlags,
                     host);
            sdc->addLastStateLog(buf);
            break;
          }
        }
        break;
      }
      case ESP_ERR_MBEDTLS_X509_CRT_PARSE_FAILED: {
        sdc->addLastStateLog(
            "CA certificate parsing error - please provide correct one");
        break;
      }
      default: {
        snprintf(buf,
                 sizeof(buf),
                 "Connection: error 0x%X (TLS 0x%X flags 0x%x \"\"%s\")",
                 error,
                 tlsError,
                 tlsFlags,
                 host);
        sdc->addLastStateLog(buf);
        break;
      }
    }
  }
}

void Supla::EspIdfClient::setTimeoutMs(uint16_t _timeoutMs) {
  timeoutMs = _timeoutMs;
}

Supla::Client *Supla::ClientBuilder() {
  return new Supla::EspIdfClient;
}
