/*
* Roughtime
* (C) 2019 Nuno Goncalves <nunojpg@gmail.com>
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#include "cli.h"

#if defined(BOTAN_HAS_ROUGHTIME)

#include <botan/roughtime.h>
#include <botan/hex.h>
#include <botan/rng.h>
#include <botan/base64.h>
#include <botan/ed25519.h>
#include <botan/hash.h>
#include <botan/calendar.h>

#include <iomanip>
#include <fstream>

namespace Botan_CLI {

class RoughtimeCheck final : public Command
   {
   public:
      RoughtimeCheck() : Command("roughtime_check --raw-time chain-file") {}

      std::string group() const override
         {
         return "misc";
         }

      std::string description() const override
         {
         return "Parse and validate Roughtime chain file";
         }

      void go() override
         {
         const auto chain = Botan::Roughtime::Chain(slurp_file_as_str(get_arg("chain-file")));
         unsigned i = 0;
         for(const auto& response : chain.responses())
            {
            output() << std::setw(3) << ++i << ": UTC ";
            if(flag_set("raw-time"))
               { output() << Botan::Roughtime::Response::sys_microseconds64(response.utc_midpoint()).time_since_epoch().count(); }
            else
               { output() << Botan::calendar_value(response.utc_midpoint()).to_string(); }
            output() << " (+-" << Botan::Roughtime::Response::microseconds32(response.utc_radius()).count() << "us)\n";
            }
         }
   };

BOTAN_REGISTER_COMMAND("roughtime_check", RoughtimeCheck);

class Roughtime final : public Command
   {
   public:
      Roughtime() :
         Command("roughtime --raw-time --chain-file=roughtime-chain --max-chain-size=128 --check-local-clock=60 --host= --pubkey= --servers-file=") {}

      std::string help_text() const override
         {
         return Command::help_text() + R"(

--servers-file=<filename>
   List of servers that will queried in sequence.

   File contents syntax:
      <name> <key type> <base 64 encoded public key> <protocol> <host:port>

   Example servers:
      Cloudflare-Roughtime ed25519 gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo= udp roughtime.cloudflare.com:2002
      Google-Sandbox-Roughtime ed25519 etPaaIxcBMY1oUeGpwvPMCJMwlRVNxv51KK/tktoJTQ= udp roughtime.sandbox.google.com:2002

--chain-file=<filename>
   Succesfull queries are appended to this file.
   If limit of --max-chain-size records is reached, the oldest records are truncated.
   This queries records can be replayed using command roughtime_check <chain-file>.

   File contents syntax:
      <key type> <base 64 encoded public key> <base 64 encoded blind or nonce> <base 64 encoded server response>
)";
         }

      std::string group() const override
         {
         return "misc";
         }

      std::string description() const override
         {
         return "Retrieve time from Roughtime server";
         }

      void query(std::unique_ptr<Botan::Roughtime::Chain>& chain,
                 const size_t max_chain_size,
                 const std::string& address,
                 const Botan::Ed25519_PublicKey& public_key)
         {
         Botan::Roughtime::Nonce nonce;
         Botan::Roughtime::Nonce blind;
         if(chain)
            {
            blind = Botan::Roughtime::Nonce(rng());
            nonce = chain->next_nonce(blind);
            }
         else
            {
            nonce = Botan::Roughtime::Nonce(rng());
            }
         const auto response_raw = Botan::Roughtime::online_request(address, nonce, std::chrono::seconds(5));
         const auto response = Botan::Roughtime::Response::from_bits(response_raw, nonce);
         if(flag_set("raw-time"))
            { output() << "UTC " << Botan::Roughtime::Response::sys_microseconds64(response.utc_midpoint()).time_since_epoch().count(); }
         else
            { output() << "UTC " << Botan::calendar_value(response.utc_midpoint()).to_string(); }
         output() << " (+-" << Botan::Roughtime::Response::microseconds32(response.utc_radius()).count() << "us)";
         if(!response.validate(public_key))
            {
            error_output() << "ERROR: Public key does not match!\n";
            set_return_code(1);
            return;
            }
         const auto tolerance = get_arg_sz("check-local-clock");
         if(tolerance)
            {
            const auto now = std::chrono::system_clock::now();
            const auto diff_abs = now >= response.utc_midpoint() ? now - response.utc_midpoint() : response.utc_midpoint() - now;
            if(diff_abs > (response.utc_radius() + std::chrono::seconds(tolerance)))
               {
               error_output() << "ERROR: Local clock mismatch\n";
               set_return_code(1);
               return;
               }
            output() << " Local clock match";
            }
         if(chain)
            chain->append({response_raw, public_key, blind}, max_chain_size);
         output() << '\n';
         }

      void go() override
         {

         const auto max_chain_size = get_arg_sz("max-chain-size");
         const auto chain_file = get_arg("chain-file");
         const auto servers_file = get_arg_or("servers-file", "");
         const auto host = get_arg_or("host", "");
         const auto pk = get_arg_or("pubkey", "");

         std::unique_ptr<Botan::Roughtime::Chain> chain;
         if(!chain_file.empty() && max_chain_size >= 1)
            {
            try
               {
               chain.reset(new Botan::Roughtime::Chain(slurp_file_as_str(chain_file)));
               }
            catch(const CLI_IO_Error&)
               {
               chain.reset(new Botan::Roughtime::Chain()); //file is to still be created
               }
            }

         const bool from_servers_file = !servers_file.empty();
         const bool from_host_and_pk = !host.empty() && !pk.empty();
         if(from_servers_file == from_host_and_pk)
            {
            error_output() << "Please specify either --servers-file or --host and --pubkey\n";
            set_return_code(1);
            return;
            }

         if(!servers_file.empty())
            {
            const auto servers = Botan::Roughtime::servers_from_str(slurp_file_as_str(servers_file));

            for(const auto& s : servers)
               {
               output() << std::setw(25) << std::left << s.name() << ": ";
               for(const auto& a : s.addresses())
                  {
                  try
                     {
                     query(chain, max_chain_size, a, s.public_key());
                     break;
                     }
                  catch(const std::exception& ex) //network error, try next address
                     {
                     error_output() << ex.what() << '\n';
                     }
                  }
               }

            }
         else
            {
            query(chain, max_chain_size, host, Botan::Ed25519_PublicKey(Botan::base64_decode(pk)));
            }

         if(chain)
            {
            std::ofstream out(chain_file);
            out << chain->to_string();
            }
         }
   };

BOTAN_REGISTER_COMMAND("roughtime", Roughtime);

}

#endif
