// Copyright @2012 JDSU Xtreme

#include "xtreme/common/curlwrapper/filecopy_client.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>

#include <boost/lexical_cast.hpp>
#include <boost/filesystem.hpp>

#include "xtreme/common/utils.h"
#include "xtreme/common/xtreme_exception.h"
#include "xtreme/common/curlwrapper/utils.h"

namespace xtreme {
  namespace curlwrapper {

    // Both SFTP and SCP use SSH port 22 [by default]
    const int SSH_PORT_DEFAULT = 22;

    struct CpFile {
      CpFile (const std::string& _fname) : st_fileName(_fname), st_fp(nullptr), st_fileSize(-1) {
        open();
      }

      ~CpFile() {
        close();
      }

      std::string st_fileName;
      FILE        * st_fp;
      //struct      st_fileStat;
      off_t       st_fileSize;   // off_t is s signed integer value that can hold -1

    private:
      inline void open() {
        // Open in binary mode ... default transfer mode of libcurl-easy-interface, is 'binary'
        st_fp = fopen(st_fileName.c_str(), "rb");

        // int ret = stat(st_fileName, &st_fileStat);
        // st_fileSize = (0 == ret) ? st_fileStat.st_size : -1;

        // Determine size using fseeko/ftello ... since the file is already open
        // POSIX fseeko/ftello :- provide large file (> 4GB) support
        if (nullptr != st_fp) {
          int retSeek = fseeko(st_fp, 0, SEEK_END);
          if(0 == retSeek) {
            st_fileSize = ftello(st_fp);
            retSeek = fseeko(st_fp, 0, SEEK_SET);  // rewind (may be a large) file.
          }

          // If either seek or ftell failed, close file
          if ((0 != retSeek) || (st_fileSize < 0))
            close();
        }
      }

      inline void close() {
        if (nullptr != st_fp)
          fclose(st_fp);

          st_fp = nullptr;
          st_fileSize = -1;
          CURLWRAPPER_INFO("Closed Copy-File : " << st_fileName);
      }
    };

    // Used as libcurl 'Callback' ... CURLOPT_READFUNCTION
    static size_t ReadData( void *ptr, size_t size, size_t nmemb, void *userdata) {
      CURLWRAPPER_TRACE("Read Copy-File : CURLOPT_READFUNCTION called back...");
      struct CpFile * pInput = static_cast<struct CpFile *> (userdata);

      if (nullptr == pInput)
        return CURL_READFUNC_ABORT;

      if (nullptr == pInput->st_fp)
          return CURL_READFUNC_ABORT;  // Failed to open read-file earlier

      size_t ret = fread (ptr, size, nmemb, pInput->st_fp);
      if ((0 == ret) && ferror(pInput->st_fp)) {
				CURLWRAPPER_DEBUG("Read Copy-File : " << pInput->st_fileName << ": returning CURL_READFUNC_ABORT. [size=" << size << "], [nmemb=" << nmemb << "], [ret=" << ret << "]");
        return CURL_READFUNC_ABORT;
      }
      
      curl_off_t nread = size * ret;
      CURLWRAPPER_DEBUG("Read Copy-File : " << pInput->st_fileName << ": " << ret << " [size=" << size << 
      		"], [nmemb=" << nmemb << "], [nread=" << nread << "]");
      return (size_t)(nread);
    }


    CFileCopyClient::CFileCopyClient(EnFileCopyType _transferType) : m_transferType(_transferType), m_sshPort(SSH_PORT_DEFAULT) {
       init();
    }

    CFileCopyClient::~CFileCopyClient() {
      clenup();
    }

    void CFileCopyClient::init() {
      m_pCurl = curl_easy_init();
      if (NULL == m_pCurl) {
        CURLWRAPPER_FATAL("Copy-File : Failed to create CURL easy-handle !!!");
        throw xtreme::XtremeException("Copy-File : Failed to create CURL easy-handle");
      }
      // Set the callback-function to read data [to be sent to peer]
      curl_easy_setopt(m_pCurl, CURLOPT_READFUNCTION, ReadData);

      // Set the callback-function to write data [received from peer]
      // curl_easy_setopt(m_pCurl, CURLOPT_WRITEFUNCTION, WriteData);

	// create remote missing directories & retry
	CURLWRAPPER_DEBUG("transfertype="<<m_transferType);
	if (kSFTP == m_transferType) {
        curl_easy_setopt(m_pCurl, CURLOPT_FTP_CREATE_MISSING_DIRS, 1L);
    }
	}

    void CFileCopyClient::clenup() {
      curl_easy_cleanup(m_pCurl);
      CURLWRAPPER_TRACE("Copy-File : Cleanup CURL easy-handle !!!");
    }


    void CFileCopyClient::set_USERPWD(const std::string& _user, const std::string& _passwd) {
      curl_easy_setopt(m_pCurl, CURLOPT_USERPWD, (_user+":"+_passwd).c_str());

      // TODO(later): set CURLOPT_SSH_AUTH_TYPES to CURLSSH_AUTH_PASSWORD ??
      return;
    }

    // Max-time (in seconds) allowed for libcurl to 'connect' to server
    void CFileCopyClient::SetConnTimeout(uint32_t seconds) {
      uint32_t effSeconds = ((seconds > 0) ? seconds : 0);
      CURLcode retCode = curl_easy_setopt(m_pCurl, CURLOPT_CONNECTTIMEOUT, effSeconds);
      CURLWRAPPER_DEBUG("Copy-File : Set CURLOPT_CONNECTTIMEOUT [" << effSeconds << "] ... " << ((CURLE_OK == retCode) ? "OK" : "NOT OK"));
      return;
    }

    // Max-time (in seconds) allowed for libcurl to 'perform' an operation
    void CFileCopyClient::SetOperTimeout(uint32_t seconds) {
      CURLcode retCode;
      uint32_t effSeconds = 0;

      effSeconds = ((seconds > 0) ? seconds : 0);
      retCode = curl_easy_setopt(m_pCurl, CURLOPT_TIMEOUT, effSeconds);
      CURLWRAPPER_DEBUG("Copy-File : Set CURLOPT_TIMEOUT [" << effSeconds << "] ... " << ((CURLE_OK == retCode) ? "OK" : "NOT OK"));

      effSeconds = ((seconds > 5) ? (seconds-5)  : 0);
      retCode = curl_easy_setopt(m_pCurl, CURLOPT_FTP_RESPONSE_TIMEOUT, effSeconds);
      CURLWRAPPER_DEBUG("Copy-File : Set CURLOPT_FTP_RESPONSE_TIMEOUT [" << effSeconds << "] ... " << ((CURLE_OK == retCode) ? "OK" : "NOT OK"));

      return;
    }

    // Create missing sub-directory if does not exist
    bool CFileCopyClient::createMissingDirs(const std::string& destpath, std::string& errstr){
    	std::string dirpath;
    	std::string fname;
    	xtreme::extractDirectoryNFileName(destpath, dirpath, fname);
    	boost::filesystem::path boostpath = dirpath;
    	bool result = false;
        boost::system::error_code ec;
    	try {
    	 result = boost::filesystem::create_directories(boostpath, ec);
     	 if (!result && ec.value() > 0){
     		CURLWRAPPER_ERROR("create dirs failed:error:"<<ec.value()<<", mesg:"<<ec.message());
     		return result;
     	 }
    	}
    	catch(boost::filesystem3::filesystem_error& fserr)
    	{
            ec = fserr.code();
            CURLWRAPPER_ERROR("create dirs failed:exception:"<<ec.message()<<", error:"<<ec.value());
            CURLWRAPPER_ERROR("exception:what:"<<fserr.what());
			errstr.assign(ec.message());
            return false;
        }
    	return true;
    }


    // TODO(IfRequired): Implement setter for CURLOPT_SSH_AUTH_TYPES='CURLSSH_AUTH_PUBLICKEY' if
    //                   authentication is to be done by SSH public/private KEY
    //   curl_setopt($curl, CURLOPT_SSH_AUTH_TYPES, CURLSSH_AUTH_PUBLICKEY);
    //   curl_setopt($curl, CURLOPT_SSH_PUBLIC_KEYFILE, $pubkey_filename); // you already did it
    //   curl_setopt($curl, CURLOPT_SSH_PRIVATE_KEYFILE, $privkey_filename);
    //   curl_setopt($curl, CURLOPT_KEYPASSWD, '');


    // _host ... is expected as IP address string
    // _remoteffname and _localffname ... are expected to be 'absolute' (full) filenames
    CPerformResult CFileCopyClient::copyTO(const std::string& _host, const std::string& _remoteffname,
        const std::string& _localffname) {

      CPerformResult result(CURLE_OK, "");

      // If copying to same appliance (machine) ... use file local File-Copy
      if (isApplianceIP(_host)) {
        boost::filesystem::path src_path = _localffname;
        boost::filesystem::path dst_path = _remoteffname;
        // create missing dirs
        std::string errstr;
        bool ret = createMissingDirs(_remoteffname, errstr);
        if (!ret){
        	std::string errinfo("Local File-Copy failed while creating sub directory with ");
        	errinfo.append(errstr);
            return CPerformResult(CURLE_NOT_BUILT_IN, errinfo);
        }
        CURLWRAPPER_DEBUG("Copy-File : Starting Local File-Copy from [" << _localffname << "] ...to... ["
            << _remoteffname << "]");
        boost::system::error_code ec;
        boost::filesystem::copy_file(src_path, dst_path, boost::filesystem::copy_option::fail_if_exists, ec);

        if (0 != ec) {
          CURLWRAPPER_ERROR("Copy-File : FAILED   Local File-Copy from [" << _localffname << "] ...to... ["
              << _remoteffname << "]");
          return CPerformResult (CURLE_NOT_BUILT_IN, "Local File-Copy failed.");
        }

        CURLWRAPPER_INFO("Copy-File : DONE     Local File-Copy from [" << _localffname << "] ...to... ["
            << _remoteffname << "]");
        return result;
      }

      // Otherwise, proceed to copy to a remote appliance (machine) ... use SFTP

      // Prepare for upload (send) to peer
      curl_easy_setopt(m_pCurl, CURLOPT_UPLOAD, 1);

      // Set destination URL in peer
      std::string destUrl = getDestURL(_host, _remoteffname);
      curl_easy_setopt(m_pCurl, CURLOPT_URL, destUrl.c_str());

      // Scoped 'CpFile'
      {
        CpFile _cpSendFile(_localffname);

        curl_easy_setopt(m_pCurl, CURLOPT_READDATA, (void*)(&_cpSendFile));

        curl_easy_setopt(m_pCurl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)_cpSendFile.st_fileSize);

        // TODO(Later): CURLOPT_DEBUGFUNCTION that uses log4cxx ... required ??

        // TODO(Later): CURLOPT_ERRORBUFFER ??

        // TODO(later): remove debug setting of CURLOPT_VERBOSE.
        curl_easy_setopt(m_pCurl, CURLOPT_VERBOSE, 1L);

				CURLWRAPPER_DEBUG("Copy-File : Starting upload of ... " << _cpSendFile.st_fileName);
        result.result_code_ = curl_easy_perform(m_pCurl);
        CURLWRAPPER_DEBUG("Copy-File : " << _cpSendFile.st_fileName << " " << _cpSendFile.st_fileSize << " bytes upload to, host="
            <<_host << " [destination= " << destUrl << "] ...result_code=" << result.result_code_);        
      }

      if(result.result_code_ != CURLE_OK) {
        result.info_ = std::string(curl_easy_strerror(result.result_code_));
        CURLWRAPPER_ERROR("FAILED: curl file copyTo, host=" <<_host << ", port=" << m_sshPort << "[destination= " << destUrl << "].");
        CURLWRAPPER_ERROR("curl error code [" << result.result_code_ << "]  info = " << result.info_);
      } else {
        CURLWRAPPER_DEBUG("DONE:   curl file copyTo, host=" <<_host << ", port=" << m_sshPort << "[destination= " << destUrl << "].");
      }
      return result;
    }

    // _remoteffname is expected to be 'absolute' (full) filename
    std::string CFileCopyClient::getDestURL(const std::string& _host, const std::string& _remoteffname) {
      std::string url("?");

      // Instead of specifying port in URL ...
      // CURLOPT_PORT can be set to control remote port to connect-to.
      switch (m_transferType) {
      case kSCP:
        url = "scp://" + _host + ":" + boost::lexical_cast<std::string>(m_sshPort) + _remoteffname;
        break;

      case kSFTP:
        url = "sftp://"+ _host + ":" + boost::lexical_cast<std::string>(m_sshPort) + _remoteffname;
        break;

      default:
        break;
      }

      return url;
    }

  }
}
