#ifndef CROMP_CLIENT_IMPLEMENTATION_FILE
#define CROMP_CLIENT_IMPLEMENTATION_FILE

/*****************************************************************************\
*                                                                             *
*  Name   : cromp_client                                                      *
*  Author : Chris Koeritz                                                     *
*                                                                             *
*******************************************************************************
* Copyright (c) 2000-$now By Author.  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 is online at:      *
*     http://www.fsf.org/copyleft/gpl.html                                    *
* Please send any updates to: fred@gruntose.com                               *
\*****************************************************************************/

#include "cromp_client.h"
#include "cromp_common.h"
#include "cromp_transaction.h"

#include <basis/chaos.h>
#include <basis/function.h>
#include <basis/istring.h>
#include <basis/log_base.h>
#include <basis/mutex.h>
#include <basis/portable.h>
#include <data_struct/static_memory_gremlin.h>
#include <mechanisms/ithread.h>
#include <mechanisms/roller.cpp>
#include <mechanisms/time_stamp.h>
#include <octopus/entity_defs.h>
#include <octopus/identity_infoton.h>
#include <octopus/unhandled_request.h>
#include <sockets/address.h>
#include <sockets/machine_uid.h>
#include <sockets/spocket.h>
#include <sockets/tcpip_stack.h>
#include <tentacles/encryption_tentacle.h>
#include <tentacles/encryption_wrapper.h>
#include <tentacles/entity_registry.h>
#include <tentacles/key_repository.h>
#include <tentacles/login_tentacle.h>
#include <tentacles/security_infoton.h>

#ifndef OMIT_CRYPTO_SUPPORT
  #include <crypto/rsa_crypto.h>
#endif

//#define DEBUG_CROMP_CLIENT
  // uncomment for noisier version.

#undef LOG
#define LOG(s) CLASS_EMERGENCY_LOG(program_wide_logger(), s)
#undef FILT_LOG
#define FILT_LOG(filter, s) CLASS_FILTER_LOG(program_wide_logger(), s, filter)

const int MAX_CONN_ATTEMPTS = 3;
  // the number of times we'll retry connecting for certain errors.

const int INTERCONNECTION_SNOOZE = 200;
  // we will pause this long if the initial connection attempt failed, and
  // in between each attempt thereafter except the last.

// grab control of the class, preventing multiple threads from trampling data.
#define AUTO_LOCK \
  auto_synchronizer l(*_lock)

// make sure the client is in an operational state.
#define CHECK_LOCKOUT \
  if (_disallowed) { \
    /* we can't do anything now due to the state of the connection. */ \
    return NO_CONNECTION; \
  }

// tries to get a particular type of object back from an infoton response.
#define CAST_REPLY(type, varname, newvar, retval) \
  type *newvar = dynamic_cast<type *>(varname); \
  if (!newvar) { \
    LOG("failed to cast " #varname " to appropriate type, " #type "."); \
    WHACK(varname); \
    return retval; \
  }

////////////////////////////////////////////////////////////////////////////

class asynch_connection_thread : public ithread
{
public:
  asynch_connection_thread(cromp_client &parent)
      : ithread(), _parent(parent) {}
  ~asynch_connection_thread() { stop(); }
  void perform_activity(void *formal(ptr)) {
    FUNCDEF("perform_activity");
    while (!should_stop()) {
      if (_parent.connected()) {
        FILT_LOG(common::NETWORK_LOGGING, _parent.instance_name()
            + " got connected.");
        break;  // done?
      }
      // invoke the real connection maker.  we should be synchronized wrt
      // multiple threads since the "_disallowed" flag is set before this
      // thread is ever started.  no one that locks the cromp_client will
      // get a chance to interfere.
      FILT_LOG(common::NETWORK_LOGGING, _parent.instance_name() + " still "
          "unconnected; trying connect now.");
      _parent.locked_connect();
      FILT_LOG(common::NETWORK_LOGGING, _parent.instance_name()
          + " done calling connect.");
    }
    // single shot thread is exiting now.
    _parent._disallowed = false;
  }

private:
  cromp_client &_parent;
};

////////////////////////////////////////////////////////////////////////////

cromp_client::cromp_client(const internet_address &addr, int connection_wait,
    int max_per_ent)
: cromp_common(cromp_common::chew_hostname(addr), max_per_ent),
  _encrypting(false),
  _connection_wait(connection_wait),
  _lock(new mutex),
  _ent(new octopus_entity(randomize_entity())),
  _req_id(new int_roller(1, MAXINT - 20)),
  _identified(false),
  _authorized(false),
  _disallowed(false),
  _asynch_connector(NIL),
#ifndef OMIT_CRYPTO_SUPPORT
  _channel_secured(false),
  _crypto(new blowfish_crypto(encryption_infoton::BLOWFISH_KEY_SIZE)),
  _encrypt_arm(NIL),
#endif
  _guardian(new blank_entity_registry)
{
  FUNCDEF("constructor");
#ifdef DEBUG_CROMP_CLIENT
  LOG(istring("initial entity=") + _ent->mangled_form());
#endif
  open_common(addr);

  // add simple security handling.
  add_tentacle(new login_tentacle(*_guardian));
    // add a non-filtering tentacle for checking security.  we mainly need
    // this to be able to unpack answers from the server.
}

cromp_client::~cromp_client()
{
  FUNCDEF("destructor");
  disconnect();
  close_common();
  _identified = false;
  _authorized = false;
  WHACK(_ent);
  WHACK(_req_id);
#ifndef OMIT_CRYPTO_SUPPORT
  _channel_secured = false;
  WHACK(_crypto);
#endif
  WHACK(_guardian);
  WHACK(_lock);
}

bool cromp_client::connected() const { return spock()->connected(); }

void cromp_client::enable_encryption()
{
  FUNCDEF("enable_encryption");
#ifndef OMIT_CRYPTO_SUPPORT
  AUTO_LOCK;

#ifdef DEBUG_CROMP_CLIENT
  LOG(istring("enabling encryption for ") + class_name() + " on "
      + other_side().text_form());
#endif
  _encrypting = true;

  // plug in the encryption support.
  if (other_side().is_localhost()) {
    // if the address is localhost, then we will use the standard key.
    byte_array temp_priv_key;
    localhost_only_key().private_key(temp_priv_key);
    _encrypt_arm = new encryption_tentacle(temp_priv_key);
//hmmm: there is a risk that if they reset to a new address we'd still be
//      using the slightly less secure local key.  could be ameliorated by
//      zapping the encryption tentacle for a reset and readding it if it
//      existed?
  } else
    _encrypt_arm = new encryption_tentacle(encryption_infoton::RSA_KEY_SIZE);
  add_tentacle(_encrypt_arm, true);
  add_tentacle(new unwrapping_tentacle, false);
#endif
}

void cromp_client::stop_asynch_thread()
{
  FUNCDEF("stop_asynch_thread");
  if (_asynch_connector) {
#ifdef DEBUG_CROMP_CLIENT
    LOG(instance_name() + " stopping thread.");
#endif
    _asynch_connector->cancel();  // send it a nudge before we grab control.
    AUTO_LOCK;  // lock the class to prevent interference.
    _asynch_connector->stop();
    WHACK(_asynch_connector);
  }
  _disallowed = false;  // no longer running the background thread.
}

void cromp_client::reset(const internet_address &addr, int connection_wait,
    int max_per_ent)
{
  FUNCDEF("reset");
  stop_asynch_thread();
  AUTO_LOCK;
  close_common();  // shut down the low-level stuff.
  max_bytes_per_entity(max_per_ent);
  *_ent = randomize_entity();
  _req_id->set_current(1);
  _identified = false;
  _authorized = false;
#ifndef OMIT_CRYPTO_SUPPORT
  _channel_secured = false;
#endif
  _connection_wait = connection_wait;
  _disallowed = false;
#ifdef DEBUG_CROMP_CLIENT
  LOG(istring("resetting entity=") + _ent->mangled_form());
#endif
  open_common(addr);
}

const octopus_entity &cromp_client::entity() const
{
  AUTO_LOCK;
  return *_ent;
}

SAFE_STATIC(tcpip_stack, _hidden_stack, )

octopus_entity cromp_client::randomize_entity() const
{
  istring host = cromp_common::chew_hostname(internet_address
      (byte_array::empty_array(), _hidden_stack().hostname(), 0), NIL);
  chaos randomizer;
  return octopus_entity(host, portable::process_id(),
      randomizer.inclusive(0, MAXINT / 3),
      randomizer.inclusive(0, MAXINT / 3));
}

octopus_request_id cromp_client::next_id()
{
  AUTO_LOCK;
  return octopus_request_id(*_ent, _req_id->next_id());
}

outcome cromp_client::synchronous_request(const infoton &to_send,
    infoton * & received, octopus_request_id &item_id,
    int timeout)
{
  FUNCDEF("synchronous_request");
  received = NIL;
  outcome ret = submit(to_send, item_id);
  if (ret != OKAY) {
    FILT_LOG(common::NETWORK_LOGGING, istring("failed to submit request: ")
        + outcome_name(ret) + " on " + to_send.text_form());
    return ret;
  }
  ret = acquire(received, item_id, timeout);
  if (ret != OKAY) {
    FILT_LOG(common::NETWORK_LOGGING, istring("failed to acquire response: ")
        + outcome_name(ret) + " for " + to_send.text_form());
    return ret;
  }
  return OKAY;
}

outcome cromp_client::login()
{
  FUNCDEF("login");
  CHECK_LOCKOUT;
  if (!_identified) {
#ifndef OMIT_CRYPTO_SUPPORT
    _channel_secured = false;
#endif
    // we need to secure an identity with the server.
    identity_infoton identity;
    octopus_request_id item_id = octopus_request_id::randomized_id();
    infoton *response;
    outcome ret = synchronous_request(identity, response, item_id);
    if (ret != OKAY) return ret;

    CAST_REPLY(identity_infoton, response, ide_reply, NO_SERVER);
    if (!ide_reply->_new_name.blank()) {
#ifdef DEBUG_CROMP_CLIENT
      LOG(istring("setting new entity to: ")
          + ide_reply->_new_name.mangled_form());
#endif
      AUTO_LOCK;
      *_ent = ide_reply->_new_name;
      _identified = true;
    } else {
#ifdef DEBUG_CROMP_CLIENT
      LOG("identity request failed: got blank name.");
#endif
    }
    WHACK(ide_reply);
  }

#ifndef OMIT_CRYPTO_SUPPORT
  if (_encrypting && !_channel_secured) {
    // now the encryption needs to be cranked up.

    if (!_encrypt_arm)
      LOG("there's no encryption arm!!!!");

    encryption_infoton encro;
    {
      AUTO_LOCK;
      encro.prepare_public_key(_encrypt_arm->private_key());
    }

    infoton *response;
    octopus_request_id item_id;
    outcome ret = synchronous_request(encro, response, item_id);
    if (ret != OKAY) return ret;

    CAST_REPLY(encryption_infoton, response, enc_reply, ENCRYPTION_MISMATCH);
      // this is a reasonable answer (mismatch), because a non-encrypting
      // server should tell us a general failure response, since it shouldn't
      // understand the request.

    // handle the encryption infoton by feeding our tentacle the new key.
    byte_array transformed;
    ret = _encrypt_arm->consume(*enc_reply, item_id, transformed);
    if (ret != OKAY) {
      LOG(istring("failed to process encryption infoton for ")
          + item_id.text_form());
      WHACK(enc_reply);  // nothing to give out.
      return ret;
    }
    WHACK(enc_reply);

    octenc_key_record *reco = _encrypt_arm->keys().lock(item_id._entity);
    if (!reco) {
      LOG(istring("failed to locate key for ") + item_id._entity.text_form());
      return NOT_FOUND;
    }
    _crypto->set_key(reco->_key.get_key(),
        encryption_infoton::BLOWFISH_KEY_SIZE);
    _encrypt_arm->keys().unlock(reco);
    _channel_secured = true;
  }
#endif

  if (!_authorized) {
    // we need to go through whatever authentication is used by the server.
    security_infoton::login_modes login_type = security_infoton::LI_LOGIN;
    security_infoton securinfo(login_type, OKAY, byte_array());
    octopus_request_id item_id;
    infoton *response;
    outcome ret = synchronous_request(securinfo, response, item_id);
    unhandled_request *temp_unh = dynamic_cast<unhandled_request *>(response);
    if (temp_unh) {
#ifdef DEBUG_CROMP_CLIENT
      LOG(istring("got an unhandled request with reason: ")
          + common::outcome_name(temp_unh->_reason));
#endif
      return temp_unh->_reason;  // return the original reason.
    }
    CAST_REPLY(security_infoton, response, sec_reply, NO_SERVER);
    outcome success = sec_reply->_success;
    if (success == tentacle::OKAY) {
      AUTO_LOCK;
      _authorized = true;
    } else {
#ifdef DEBUG_CROMP_CLIENT
      LOG(istring("login request failed."));
#endif
    }
    WHACK(sec_reply);
  }

  return OKAY;
}

outcome cromp_client::connect()
{
  FUNCDEF("connect");
  stop_asynch_thread();
  AUTO_LOCK;  // protect from multiple connect attempts.
  return locked_connect();
}

outcome cromp_client::asynch_connect()
{
  FUNCDEF("asynch_connect");
  if (connected()) return OKAY;  // why bother?
  if (_asynch_connector) return NO_CONNECTION;  // in progress.
//#ifdef DEBUG_CROMP_CLIENT
  LOG(instance_name() + " entry.");
//#endif
  {
    AUTO_LOCK;
      // protect this block only; we want to unlock before thread gets started.
    if (connected()) return OKAY;  // done already somehow.
    if (_asynch_connector) {
      LOG("logic error: asynchronous connector already exists.");
      return NO_CONNECTION;
    }
    _disallowed = true;
    _asynch_connector = new asynch_connection_thread(*this);
  }
  _asynch_connector->start(NIL);
//#ifdef DEBUG_CROMP_CLIENT
  LOG(instance_name() + " exit.");
//#endif
  return NO_CONNECTION;
}

outcome cromp_client::locked_connect()
{
  FUNCDEF("locked_connect");
  if (!spock()) return BAD_INPUT;
  if (connected()) return OKAY;  // already connected.

  locked_disconnect();  // clean out any previous connection.
  *_ent = randomize_entity();  // reset the login id.

  int attempts = 0;
  while (attempts++ < MAX_CONN_ATTEMPTS) {
#ifdef DEBUG_CROMP_CLIENT
    LOG(instance_name() + " calling spocket connect.");
#endif
    outcome ret = spock()->connect(_connection_wait);
#ifdef DEBUG_CROMP_CLIENT
    LOG(instance_name() + " done calling spocket connect.");
#endif
    if (ret == spocket::OKAY) {
#ifdef DEBUG_CROMP_CLIENT
      LOG("finished connection...  now affirming identity.");
#endif
      return login();
    }
    if (ret == spocket::TIMED_OUT) return TIMED_OUT;
    if ( (ret == spocket::NO_ANSWER) || (ret == spocket::ACCESS_DENIED) ) {
      // clean up.  this is a real case of something hosed.
      locked_disconnect();
      return NO_SERVER;
    }
#ifdef DEBUG_CROMP_CLIENT
    LOG(isprintf("error gotten=%s", spocket::outcome_name(ret)));
#endif

    if (attempts < MAX_CONN_ATTEMPTS - 1)
      portable::sleep_ms(INTERCONNECTION_SNOOZE);
  }
  FILT_LOG(common::NETWORK_LOGGING, instance_name() + " failed to connect.");
  locked_disconnect();  // clean up.
  return NO_CONNECTION;
}

outcome cromp_client::disconnect()
{
  stop_asynch_thread();
  AUTO_LOCK;
  return locked_disconnect();
}

void cromp_client::keep_alive_pause(int duration, int interval)
{
  if (duration < 0) duration = 0;
  if (interval < 0) interval = 40;
  if (interval > duration) interval = duration;
  
  // keep looping on the cromp stimulation methods until the time has elapsed.
  time_stamp leave_at(duration);
  while (time_stamp() < leave_at) {
    push_outgoing(1);
    grab_anything(false);
    // we'll only sleep if they didn't give us a zero duration.
    if (duration)
      portable::sleep_ms(interval);  // snooze a hopefully short time.
  }
}

outcome cromp_client::locked_disconnect()
{
  if (!spock()) return BAD_INPUT;
  outcome ret = spock()->disconnect();
  _identified = false;
  _authorized = false;
#ifndef OMIT_CRYPTO_SUPPORT
  _channel_secured = false;
#endif
  *_ent = octopus_entity();  // reset the login id.
  if (ret != spocket::OKAY) {
//hmmm: any other outcomes to return?
    return OKAY;
  }
  return OKAY;
}

#ifndef OMIT_CRYPTO_SUPPORT
bool cromp_client::wrap_infoton(const infoton &request,
    encryption_wrapper &wrapped)
{
  FUNCDEF("wrap_infoton");
  if (!_channel_secured) return false;
  // identity is not wrapped with encryption; we need to establish and identity
  // to talk on a distinct channel with the server.  even if that identity were
  // compromised, the interloper should still not be able to listen in on the
  // establishment of an encryption channel.
  bool is_ident = !!dynamic_cast<const identity_infoton *>(&request);
  bool is_encrypt = !!dynamic_cast<const encryption_infoton *>(&request);
  bool is_wrapper = !!dynamic_cast<const encryption_wrapper *>(&request);
  if (!is_ident && !is_encrypt && !is_wrapper) {
    // check that we have already got a channel to speak over.  otherwise, we
    // can't do any encrypting of messages yet.
    if (!_channel_secured) {
#ifdef DEBUG_CROMP_CLIENT
      LOG("the channel has not been secured yet.");
#endif
      return false;
    }
#ifdef DEBUG_CROMP_CLIENT
    LOG(istring("encrypting ") + request.text_form());
#endif
    byte_array packed_request;
    infoton::fast_pack(packed_request, request);
    _crypto->encrypt(packed_request, wrapped._wrapped);
    return true;
  } else return false;  // we didn't need or want to wrap it.
}
#endif

outcome cromp_client::submit(const infoton &request,
    octopus_request_id &item_id, int max_tries)
{
  FUNCDEF("submit");
  CHECK_LOCKOUT;
  item_id = next_id();
  bool is_ident = !!dynamic_cast<const identity_infoton *>(&request);
  if (!_identified && !is_ident) return BAD_INPUT;

#ifndef OMIT_CRYPTO_SUPPORT
  if (_encrypting && _channel_secured) {
    // if we're encrypting things, then we need to encrypt this too.  this
    // assumes that authentication is wrapped by encryption, which is the sane
    // thing to do.  identity is not wrapped that way though; we need to
    // establish and identity to talk on a distinct channel with the server.
    // even if that identity were compromised, the interloper would still not
    // be able to listen in on the establishment of an encryption channel.

    encryption_wrapper real_request;
    bool wrapped_okay = wrap_infoton(request, real_request);
    if (wrapped_okay) {
      outcome to_return = cromp_common::pack_and_ship(real_request, item_id,
          max_tries);
      return to_return;
    }
    // if it didn't wrap okay, we fall through to a normal send, because it's
    // probably an encryption or identity infoton, which needs to go through
    // without being wrapped.
  } else {
#ifdef DEBUG_CROMP_CLIENT
    LOG("the channel has not been secured yet.");
#endif
  }
#endif

  outcome to_return = cromp_common::pack_and_ship(request, item_id, max_tries);
  return to_return;
}

outcome cromp_client::acquire(infoton * &response,
    const octopus_request_id &cmd_id, int timeout)
{
  FUNCDEF("acquire");
  CHECK_LOCKOUT;
  outcome to_return = cromp_common::retrieve_and_restore(response, cmd_id,
      timeout);

  unhandled_request *intermed = dynamic_cast<unhandled_request *>(response);
  if (intermed) {
    // override the return value with the real outcome of a failed operation.
    to_return = intermed->_reason;
  }

  decrypt_package_as_needed(to_return, response, cmd_id);

  return to_return;
}

void cromp_client::decrypt_package_as_needed(outcome &to_return,
    infoton * &response, const octopus_request_id &cmd_id)
{
  FUNCDEF("decrypt_package_as_needed");
#ifndef OMIT_CRYPTO_SUPPORT
  if (dynamic_cast<encryption_wrapper *>(response)) {
    if (!_encrypt_arm) {
      LOG(istring("received an encryption_wrapper but we are not "
          "encrypting, on ") + cmd_id.text_form());
      to_return = ENCRYPTION_MISMATCH;
      return;
    }
    byte_array transformed;
    outcome ret = _encrypt_arm->consume(*response, cmd_id, transformed);
    if ( (ret != OKAY) && (ret != PARTIAL) ) {
      LOG(istring("failed to decrypt wrapper for ") + cmd_id.text_form());
      to_return = ret;
      return;
    }

    string_array classif;
    byte_array decro;  // decrypted packed infoton.
    bool worked = infoton::fast_unpack(transformed, classif, decro);
    if (!worked) {
      LOG("failed to fast_unpack the transformed data.");
      to_return = ENCRYPTION_MISMATCH;  // what else would we call that?
    } else {
      infoton *new_req = NIL;
      outcome rest_ret = octo()->restore(classif, decro, new_req);
      if (rest_ret == tentacle::OKAY) {
        // we got a good transformed version.
        WHACK(response);
        response = new_req;  // substitution complete.
      } else {
        LOG("failed to restore transformed infoton.");
        to_return = ENCRYPTION_MISMATCH;  // what else would we call that?
      }
    }
  }
#else
  // encryption is not needed.
  if (cmd_id._request_num || response) {}
  to_return = OKAY;
#endif
}


#endif //CROMP_CLIENT_IMPLEMENTATION_FILE

