/*
 * SessionRPubs.cpp
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

#include "SessionRPubs.hpp"

#include <boost/utility.hpp>
#include <boost/format.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/bind/bind.hpp>

#include <shared_core/Error.hpp>
#include <core/Exec.hpp>
#include <core/Log.hpp>
#include <core/Settings.hpp>
#include <core/FileSerializer.hpp>

#include <core/text/CsvParser.hpp>
#include <core/http/Util.hpp>
#include <core/system/Process.hpp>

#include <r/RSexp.hpp>
#include <r/RRoutines.hpp>

#include <session/SessionModuleContext.hpp>

#include <session/projects/SessionProjects.hpp>

using namespace rstudio::core;
using namespace boost::placeholders;

namespace rstudio {
namespace session {

namespace {

// we get a fresh settings object for each read or write so multiple
// processes can all read and write without cache issues
void getUploadIdSettings(Settings* pSettings)
{
   FilePath rpubsUploadIds =
      module_context::scopedScratchPath().completePath("rpubs_upload_ids");
   Error error = pSettings->initialize(rpubsUploadIds);
   if (error)
      LOG_ERROR(error);
}

std::string pathIdentifier(const FilePath& filePath)
{
   // use a relative path if we are in a project
   std::string path;
   projects::ProjectContext& projectContext = projects::projectContext();
   if (projectContext.hasProject() &&
       filePath.isWithin(projectContext.directory()))
   {
      path = filePath.getRelativePath(projectContext.directory());
   }
   else
   {
      path = filePath.getAbsolutePath();
   }

   // urlencode so we can use it as a key
   return http::util::urlEncode(path);
}


} // anonymous namespace


namespace modules { 
namespace rpubs {

namespace {

class RPubsUpload : boost::noncopyable,
                    public boost::enable_shared_from_this<RPubsUpload>
{
public:
   static boost::shared_ptr<RPubsUpload> create(const std::string& contextId,
                                                const std::string& title,
                                                const FilePath& originalRmd,
                                                const FilePath& htmlFile,
                                                const std::string& uploadId,
                                                bool allowUpdate)
   {
      boost::shared_ptr<RPubsUpload> pUpload(new RPubsUpload(contextId));
      pUpload->start(title, originalRmd, htmlFile, uploadId, allowUpdate);
      return pUpload;
   }

   virtual ~RPubsUpload()
   {
   }

   bool isRunning() const { return isRunning_; }

   void terminate()
   {
      terminationRequested_ = true;
   }

private:
   explicit RPubsUpload(const std::string& contextId)
      : contextId_(contextId), terminationRequested_(false), isRunning_(false)
   {
   }

   void start(const std::string& title, const FilePath& originalRmd, 
              const FilePath& htmlFile, const std::string& uploadId, 
              bool allowUpdate)
   {
      using namespace rstudio::core::string_utils;
      using namespace module_context;

      htmlFile_ = htmlFile;
      csvOutputFile_ = module_context::tempFile("rpubsupload", "csv");
      isRunning_ = true;

      // if we don't already have an ID for this file, and updates are allowed,
      // check for a previous upload ID
      std::string id = allowUpdate && uploadId.empty() ? 
         previousRpubsUploadId(htmlFile_) : uploadId;
      boost::algorithm::trim(id);

      // R binary
      FilePath rProgramPath;
      Error error = rScriptPath(&rProgramPath);
      if (error)
      {
         terminateWithError(error);
         return;
      }

      // args
      std::vector<std::string> args;
      args.push_back("--no-save");
      args.push_back("--no-restore");
      args.push_back("-s");
      args.push_back("-e");

      boost::format fmt(
               "result <- rsconnect::rpubsUpload('%1%', '%2%', '%3%', %4%); "
               "utils::write.csv(as.data.frame(result), "
                               " file='%5%', "
                               " row.names=FALSE);");

      std::string htmlPath = utf8ToSystem(htmlFile.getAbsolutePath());
      std::string outputPath = utf8ToSystem(csvOutputFile_.getAbsolutePath());

      // we may not have an original R Markdown document for this publish
      // event (and that's fine)
      std::string rmdPath = originalRmd == FilePath() ? "" :
         utf8ToSystem(originalRmd.getAbsolutePath());

      std::string escapedTitle = string_utils::jsLiteralEscape(title);
      std::string escapedHtmlPath = string_utils::jsLiteralEscape(htmlPath);
      std::string escapedRmdPath = string_utils::jsLiteralEscape(rmdPath);
      std::string escapedId = string_utils::jsLiteralEscape(id);
      std::string escapedOutputPath = string_utils::jsLiteralEscape(outputPath);

      std::string cmd = boost::str(fmt %
                    escapedTitle % escapedHtmlPath % escapedRmdPath %
                    (!escapedId.empty() ? "'" + escapedId + "'" : "NULL") %
                    escapedOutputPath);
      args.push_back(cmd);

      // options
      core::system::ProcessOptions options;
      options.terminateChildren = true;
      options.workingDir = htmlFile.getParent();

      // callbacks
      core::system::ProcessCallbacks cb;
      cb.onContinue = boost::bind(&RPubsUpload::onContinue,
                                  RPubsUpload::shared_from_this());
      cb.onStdout = boost::bind(&RPubsUpload::onStdOut,
                                RPubsUpload::shared_from_this(), _2);
      cb.onStderr = boost::bind(&RPubsUpload::onStdErr,
                                RPubsUpload::shared_from_this(), _2);
      cb.onExit =  boost::bind(&RPubsUpload::onCompleted,
                                RPubsUpload::shared_from_this(), _1);

      // execute
      processSupervisor().runProgram(
         rProgramPath.getAbsolutePath(),
                                     args,
                                     options,
                                     cb);
   }

   bool onContinue()
   {
      return !terminationRequested_;
   }

   void onStdOut(const std::string& output)
   {
      output_.append(output);
   }

   void onStdErr(const std::string& error)
   {
      error_.append(error);
   }

   void onCompleted(int exitStatus)
   {
      if (exitStatus == EXIT_SUCCESS)
      {
         if(csvOutputFile_.exists())
         {
            std::string csvOutput;
            Error error = core::readStringFromFile(
                                             csvOutputFile_,
                                             &csvOutput,
                                             string_utils::LineEndingPosix);
            if (error)
            {
               terminateWithError(error);
            }
            else
            {
               // parse output
               Result result = parseOutput(csvOutput);
               if (!result.empty())
                  terminateWithResult(result);
               else
                  terminateWithError(
                           "Unexpected output from upload: " + csvOutput);
            }
         }
         else
         {
            terminateWithError("Unexpected output from upload: " + output_);
         }
      }
      else
      {
         terminateWithError(error_);
      }
   }

   void terminateWithError(const Error& error)
   {
      terminateWithError(error.getSummary());
   }

   void terminateWithError(const std::string& error)
   {
      terminateWithResult(Result(error));
   }

   struct Result
   {
      Result()
      {
      }

      Result(const std::string& error)
         : error(error)
      {
      }

      Result(const std::string& id, const std::string& continueUrl)
         : id(id), continueUrl(continueUrl)
      {
      }

      bool empty() const { return id.empty() && error.empty();  }

      std::string id;
      std::string continueUrl;
      std::string error;
   };

   void terminateWithResult(const Result& result)
   {
      isRunning_ = false;
      json::Object statusJson;
      statusJson["contextId"] = contextId_;
      statusJson["id"] = result.id;
      statusJson["continueUrl"] = result.continueUrl;
      statusJson["error"] = result.error;
      ClientEvent event(client_events::kRPubsUploadStatus, statusJson);
      module_context::enqueClientEvent(event);
   }

   Result parseOutput(const std::string& output)
   {
      std::pair<std::vector<std::string>, std::string::const_iterator>
                  line = text::parseCsvLine(output.begin(), output.end());
      if (!line.first.empty())
      {
         std::vector<std::string> headers = line.first;

         line = text::parseCsvLine(line.second, output.end());
         if (!line.first.empty())
         {
            std::vector<std::string> data = line.first;

            if (headers.size() == 1 &&
                data.size() == 1 &&
                headers[0] == "error")
            {
               return Result(data[0]);
            }
            else if (headers.size() == 2 &&
                     data.size() == 2 &&
                     headers[0] == "id" &&
                     headers[1] == "continueUrl")
            {
               return Result(data[0], data[1]);
            }
         }
      }

      return Result();
   }

private:
   std::string contextId_;
   FilePath htmlFile_;
   bool terminationRequested_;
   bool isRunning_;
   std::string output_;
   std::string error_;
   FilePath csvOutputFile_;
};

std::map<std::string, boost::shared_ptr<RPubsUpload> > s_pCurrentUploads;

bool isUploadRunning(const std::string& contextId)
{
   boost::shared_ptr<RPubsUpload> pCurrentUpload = s_pCurrentUploads[contextId];
   return pCurrentUpload && pCurrentUpload->isRunning();
}

Error rpubsIsPublished(const json::JsonRpcRequest& request,
                       json::JsonRpcResponse* pResponse)
{
    std::string htmlFile;
    Error error = json::readParams(request.params, &htmlFile);
    if (error)
       return error;

    FilePath filePath = module_context::resolveAliasedPath(htmlFile);

    pResponse->setResult(
          !module_context::previousRpubsUploadId(filePath).empty());

    return Success();
}

Error rpubsUpload(const json::JsonRpcRequest& request,
                  json::JsonRpcResponse* pResponse)
{
   std::string contextId, title, originalRmd, htmlFile, uploadId;
   bool isUpdate;
   Error error = json::readParams(request.params,
                                  &contextId,
                                  &title,
                                  &originalRmd, 
                                  &htmlFile,
                                  &uploadId,
                                  &isUpdate);
   if (error)
      return error;

   // The HTML file may not be self-contained, but we can only publish self-contained content to
   // Rpubs. Attempt to determine whether the document is self-contained, and if it does not appear
   // to be, attempt to convert it before we publish it.
   bool selfContained = false;
   FilePath filePath = module_context::resolveAliasedPath(htmlFile);
   if (filePath.getSize() > 1024 * 1024)
   {
      // Presume that HTML files over 1MB are self-contained
      selfContained = true;
   }
   else
   {
      // Check the file's contents for self-contained smells
      std::string contents;
      error = readStringFromFile(filePath, &contents);

      if (error)
      {
        // Log and proceed as though the file was not self contained (transformation is idempotent)
        LOG_ERROR(error);
      }
      else
      {
        auto pos = contents.find("script src=\"data:");
        if (pos != std::string::npos)
        {
           // has self contained scripts
           selfContained = true;
        }
      }
   }

   if (!selfContained)
   {
      // Doesn't appear to be self-contained; attempt conversion.
      FilePath tempFile = module_context::tempFile("rpubs-", "html");
      error = module_context::createSelfContainedHtml(filePath, tempFile);
      if (error)
      {
         // If we didn't succeed in converting to self-contained, log and try to publish anyway
         LOG_ERROR(error);
      }
      else
      {
         // We succeeded, try to copy the self-contained version over the original.
         error = tempFile.move(filePath, FilePath::MoveCrossDevice, true /* overwrite */);
         if (error)
         {
            // Again, if we can't do this, proceed with attempting to publish the original
            LOG_ERROR(error);
         }
      }
   }

   if (isUploadRunning(contextId))
   {
      pResponse->setResult(false);
   }
   else
   {
      // provide a default title if necessary
      if (title.empty())
         title = "Untitled";

      FilePath rmdPath = originalRmd.empty() ? FilePath() :
         module_context::resolveAliasedPath(originalRmd);
      s_pCurrentUploads[contextId] = RPubsUpload::create(contextId,
                                                         title,
                                                         rmdPath,
                                                         filePath,
                                                         uploadId,
                                                         isUpdate);
      pResponse->setResult(true);
   }

   return Success();
}

Error terminateRpubsUpload(const json::JsonRpcRequest& request,
                           json::JsonRpcResponse* pResponse)
{
   std::string contextId;
   Error error = json::readParam(request.params, 0, &contextId);
   if (error)
      return error;


   if (isUploadRunning(contextId))
      s_pCurrentUploads[contextId]->terminate();

   return Success();
}

} // anonymous namespace

Error initialize()
{
   using boost::bind;
   using namespace module_context;
   ExecBlock initBlock;
   initBlock.addFunctions()
      (bind(registerRpcMethod, "rpubs_is_published", rpubsIsPublished))
      (bind(registerRpcMethod, "rpubs_upload", rpubsUpload))
      (bind(registerRpcMethod, "terminate_rpubs_upload", terminateRpubsUpload))
   ;
   return initBlock.execute();
}
   
   
} // namespace rpubs
} // namespace modules

namespace module_context {

std::string previousRpubsUploadId(const FilePath& filePath)
{
   Settings settings;
   getUploadIdSettings(&settings);
   return settings.get(pathIdentifier(filePath));
}

} // namespace module_context

} // namespace session
} // namespace rstudio

