#include "CondCore/CondDB/interface/DecodingKey.h"
#include "CondCore/CondDB/interface/FileUtils.h"
#include "CondCore/CondDB/interface/Exception.h"
#include "CondCore/CondDB/interface/Cipher.h"
//
#include <sstream>
#include <cstring>
#include <fstream>
#include <vector>
#include <pwd.h>
#include <ctime>
#include <unistd.h>

constexpr char ItemSeparator = ';';
constexpr char LineSeparator = '!';

// character set same as base64, except for last two (missing are + and / )
static const char* b64str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

static const std::string KEY_HEADER("Cond_Authentication_Key");

static const std::string VERSIONPREFIX("V=");
static const std::string NAMEPREFIX("N=");
static const std::string KEYPREFIX("K=");
static const std::string OWNERPREFIX("O=");

static const std::string DATEPREFIX("D=");

static const std::string SERVICEPREFIX("S=");
static const std::string CONNECTIONPREFIX("C=");
static const std::string USERPREFIX("U=");
static const std::string PASSWORDPREFIX("P=");

static const std::string DEFAULT_SERVICE("Cond_Default_Service");

namespace cond {
  char randomChar() {
    int irand = ::rand() % (::strlen(b64str));
    return b64str[irand];
  }

  std::string getLoginName() {
    std::string loginName("");
    struct passwd* userp = ::getpwuid(::getuid());
    if (userp) {
      char* uName = userp->pw_name;
      if (uName) {
        loginName += uName;
      }
    }
    if (loginName.empty()) {
      std::string msg("Cannot determine login name.");
      throwException(msg, "DecodingKey::getLoginName");
    }
    return loginName;
  }

  void parseLineForNamedParams(const std::string& line, std::map<std::string, std::string>& params) {
    std::stringstream str(line);
    std::string paramName("");
    std::string paramValue("");
    while (str.good()) {
      std::string item("");
      getline(str, item, ItemSeparator);
      if (item.size() > 3) {
        paramName = item.substr(0, 2);
        paramValue = item.substr(2);
        params.insert(std::make_pair(paramName, paramValue));
      }
    }
  }

}  // namespace cond

std::string cond::auth::KeyGenerator::make(size_t keySize) {
  ::srand(m_iteration + 2);
  int rseed = ::rand();
  int seed = ::time(nullptr) % 10 + rseed;
  ::srand(seed);
  std::string ret("");
  for (size_t i = 0; i < keySize; i++) {
    ret += randomChar();
  }
  m_iteration++;
  return ret;
}

std::string cond::auth::KeyGenerator::makeWithRandomSize(size_t maxSize) {
  ::srand(m_iteration + 2);
  int rseed = ::rand();
  int seed = ::time(nullptr) % 10 + rseed;
  ::srand(seed);
  size_t sz = rand() % maxSize;
  return make(sz);
}

std::string cond::auth::DecodingKey::templateFile() {
  std::stringstream s;
  s << VERSIONPREFIX << KEY_FMT_VERSION << std::endl;
  s << NAMEPREFIX << "<principal_name>" << std::endl;
  s << OWNERPREFIX << "<owner_name, optional>" << std::endl;
  s << KEYPREFIX << "<key, leave empty if generated>" << std::endl;
  //s<<DATEPREFIX<<"<expiring date, optional>"<<std::endl;
  s << SERVICEPREFIX << "<service_name0>;" << CONNECTIONPREFIX << "<service0_connection_string>;" << USERPREFIX
    << "<user0_name>;" << PASSWORDPREFIX << "<password0>;" << std::endl;
  s << SERVICEPREFIX << "<service_name1>;" << CONNECTIONPREFIX << "<service1_connection_string>;" << USERPREFIX
    << "<user1_name>;" << PASSWORDPREFIX << "<password1>;" << std::endl;
  s << SERVICEPREFIX << "<service_name2>;" << CONNECTIONPREFIX << "<service2_connection_string>;" << USERPREFIX
    << "<user2_name>;" << PASSWORDPREFIX << "<password2>;" << std::endl;
  return s.str();
}

size_t cond::auth::DecodingKey::init(const std::string& keyFileName, const std::string& password, bool readMode) {
  if (keyFileName.empty()) {
    std::string msg("Provided key file name is empty.");
    throwException(msg, "DecodingKey::init");
  }
  m_fileName = keyFileName;
  m_pwd = password;
  m_mode = readMode;
  m_version.clear();
  m_principalName.clear();
  m_principalKey.clear();
  m_owner.clear();
  m_services.clear();
  size_t nelem = 0;
  if (m_mode) {
    std::ifstream keyFile(m_fileName.c_str(), std::ios::in | std::ios::binary | std::ios::ate);
    if (keyFile.is_open()) {
      size_t fsize = keyFile.tellg();
      unsigned char* buff = (unsigned char*)malloc(fsize);
      keyFile.seekg(0, std::ios::beg);
      keyFile.read(reinterpret_cast<char*>(buff), fsize);
      Cipher cipher(m_pwd);
      std::string content = cipher.decrypt(buff, fsize);
      free(buff);
      // skip the header + line separator
      if (content.substr(0, KEY_HEADER.size()) != KEY_HEADER) {
        std::string msg("Provided key content is invalid.");
        throwException(msg, "DecodingKey::init");
      }
      std::stringstream str(content.substr(KEY_HEADER.size() + 1));
      while (str.good()) {
        std::string line;
        getline(str, line, LineSeparator);
        if (line.size() > 3) {
          if (line.substr(0, 2) == VERSIONPREFIX) {
            m_version = line.substr(2);
          } else if (line.substr(0, 2) == NAMEPREFIX) {
            m_principalName = line.substr(2);
          } else if (line.substr(0, 2) == KEYPREFIX) {
            m_principalKey = line.substr(2);
          } else if (line.substr(0, 2) == OWNERPREFIX) {
            m_owner = line.substr(2);
          } else if (line.substr(0, 2) == SERVICEPREFIX) {
            std::stringstream serviceStr(line.substr(2));
            std::vector<std::string> sdata;
            while (serviceStr.good()) {
              sdata.push_back(std::string(""));
              getline(serviceStr, sdata.back(), ItemSeparator);
            }
            std::map<std::string, ServiceCredentials>::iterator iS =
                m_services.insert(std::make_pair(sdata[0], ServiceCredentials())).first;
            iS->second.connectionString = sdata[1];
            iS->second.userName = sdata[2];
            iS->second.password = sdata[3];
            nelem++;
          }
        }
      }
      keyFile.close();
      if (m_principalName.empty() || m_principalKey.empty()) {
        std::string msg = "Provided key is invalid.";
        throwException(msg, "DecodingKey::init");
      }
      if (!m_owner.empty()) {
        std::string currentUser = getLoginName();
        if (m_owner != currentUser) {
          m_principalName.clear();
          m_principalKey.clear();
          m_owner.clear();
          m_services.clear();
          std::string msg = "Provided key is invalid for user=" + currentUser;
          throwException(msg, "DecodingKey::init");
        }
      }
    } else {
      std::string msg = "Required Key File \"" + m_fileName + "\" is missing or unreadable.";
      throwException(msg, "DecodingKey::init");
    }
  }
  return nelem;
}

size_t cond::auth::DecodingKey::createFromInputFile(const std::string& inputFileName, size_t generatedKeySize) {
  size_t nelem = 0;
  if (inputFileName.empty()) {
    std::string msg("Provided input file name is empty.");
    throwException(msg, "DecodingKey::readFromInputFile");
  }
  m_version.clear();
  m_principalName.clear();
  m_principalKey.clear();
  m_owner.clear();
  m_services.clear();
  std::ifstream inputFile(inputFileName.c_str());
  if (inputFile.is_open()) {
    std::map<std::string, std::string> params;
    while (inputFile.good()) {
      std::string line;
      getline(inputFile, line);
      params.clear();
      if (line.size() > 3) {
        if (line.substr(0, 2) == VERSIONPREFIX) {
          m_version = line.substr(2);
        } else if (line.substr(0, 2) == NAMEPREFIX) {
          m_principalName = line.substr(2);
        } else if (line.substr(0, 2) == KEYPREFIX) {
          m_principalKey = line.substr(2);
        } else if (line.substr(0, 2) == OWNERPREFIX) {
          m_owner = line.substr(2);
        } else if (line.substr(0, 2) == SERVICEPREFIX) {
          parseLineForNamedParams(line, params);
          std::string& serviceName = params[SERVICEPREFIX];
          ServiceCredentials creds;
          creds.connectionString = params[CONNECTIONPREFIX];
          creds.userName = params[USERPREFIX];
          creds.password = params[PASSWORDPREFIX];
          m_services.insert(std::make_pair(serviceName, creds));
          nelem++;
        }
      }
    }
    inputFile.close();
    if (m_principalKey.empty() && generatedKeySize) {
      KeyGenerator gen;
      m_principalKey = gen.make(generatedKeySize);
    }

  } else {
    std::string msg = "Provided Input File \"" + inputFileName + "\n is invalid.";
    throwException(msg, "DecodingKey::readFromInputFile");
  }
  return nelem;
}

void cond::auth::DecodingKey::list(std::ostream& out) {
  out << VERSIONPREFIX << m_version << std::endl;
  out << NAMEPREFIX << m_principalName << std::endl;
  out << KEYPREFIX << m_principalKey << std::endl;
  out << OWNERPREFIX << m_owner << std::endl;
  for (std::map<std::string, ServiceCredentials>::const_iterator iS = m_services.begin(); iS != m_services.end();
       iS++) {
    out << SERVICEPREFIX << iS->first << ";";
    out << CONNECTIONPREFIX << iS->second.connectionString << ";";
    out << USERPREFIX << iS->second.userName << ";";
    out << PASSWORDPREFIX << iS->second.password << ";" << std::endl;
  }
}

void cond::auth::DecodingKey::flush() {
  std::ofstream outFile(m_fileName.c_str(), std::ios::binary);
  if (outFile.is_open()) {
    std::stringstream content;
    content << KEY_HEADER << LineSeparator;
    if (!m_version.empty()) {
      content << VERSIONPREFIX << m_version << LineSeparator;
    }
    if (!m_principalName.empty()) {
      content << NAMEPREFIX << m_principalName << LineSeparator;
    }
    if (!m_principalKey.empty()) {
      content << KEYPREFIX << m_principalKey << LineSeparator;
    }
    if (!m_owner.empty()) {
      content << OWNERPREFIX << m_owner << LineSeparator;
    }
    for (std::map<std::string, ServiceCredentials>::const_iterator iD = m_services.begin(); iD != m_services.end();
         ++iD) {
      content << SERVICEPREFIX << iD->first << ItemSeparator;
      content << iD->second.connectionString << ItemSeparator;
      content << iD->second.userName << ItemSeparator;
      content << iD->second.password << ItemSeparator;
      content << LineSeparator;
    }
    Cipher cipher(m_pwd);
    unsigned char* out;
    size_t outSize = cipher.encrypt(content.str(), out);
    outFile.write(reinterpret_cast<char*>(out), outSize);
    free(out);
  } else {
    std::string msg("");
    msg += "Provided Key File \"" + m_fileName + "\n is invalid.";
    throwException(msg, "DecodingKey::flush");
  }
  outFile.close();
}

void cond::auth::DecodingKey::addDefaultService(const std::string& connectionString) {
  addService(DEFAULT_SERVICE, connectionString, "", "");
}

void cond::auth::DecodingKey::addService(const std::string& serviceName,
                                         const std::string& connectionString,
                                         const std::string& userName,
                                         const std::string& password) {
  std::map<std::string, ServiceCredentials>::iterator iK = m_services.find(serviceName);
  if (iK == m_services.end()) {
    iK = m_services.insert(std::make_pair(serviceName, ServiceCredentials())).first;
  }
  iK->second.connectionString = connectionString;
  iK->second.userName = userName;
  iK->second.password = password;
}
