/*
 * Copyright (C) 2011 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include "server.h"
#include "connection.h"
#include "client.h"

#include <synthesis/san.h>
#include <syncevo/TransportAgent.h>
#include <syncevo/SyncContext.h>

using namespace GDBusCXX;

SE_BEGIN_CXX

void Connection::failed(const std::string &reason)
{
    SE_LOG_DEBUG(NULL, "Connection %s: failed: %s (old state %s)",
                 m_sessionID.c_str(),
                 reason.c_str(),
                 SessionCommon::ConnectionStateToString(m_state).c_str());
    if (m_failure.empty()) {
        m_failure = reason;
        if (m_session) {
            m_session->setStubConnectionError(reason);
        }
    }
    // notify client
    abort();
    // ensure that state is failed
    m_state = SessionCommon::FAILED;
    // tell helper (again)
    m_statusSignal(reason);

    // A "failed" connection is dead, no further operations on it
    // are allowed, in particular not the normal Connection.Close().
    // Therefore remove ourselves.
    //
    // But don't delete ourselves while some code of the Connection still
    // runs. Instead let server do that as part of its event loop.
    auto c = weak_from_this().lock();
    if (c) {
        m_server.delayDeletion(c);
        m_server.detach(this);
    }
}

std::string Connection::buildDescription(const StringMap &peer)
{
    StringMap::const_iterator
        desc = peer.find("description"),
        id = peer.find("id"),
        trans = peer.find("transport"),
        trans_desc = peer.find("transport_description");
    std::string buffer;
    buffer.reserve(256);
    if (desc != peer.end()) {
        buffer += desc->second;
    }
    if (id != peer.end() || trans != peer.end()) {
        if (!buffer.empty()) {
            buffer += " ";
        }
        buffer += "(";
        if (id != peer.end()) {
            buffer += id->second;
            if (trans != peer.end()) {
                buffer += " via ";
            }
        }
        if (trans != peer.end()) {
            buffer += trans->second;
            if (trans_desc != peer.end()) {
                buffer += " ";
                buffer += trans_desc->second;
            }
        }
        buffer += ")";
    }
    return buffer;
}

static bool IsSyncML(const std::string &messageType)
{
    return messageType == TransportAgent::m_contentTypeSyncML ||
        messageType == TransportAgent::m_contentTypeSyncWBXML;
}


void Connection::process(const Caller_t &caller,
                         const GDBusCXX::DBusArray<uint8_t> &message,
                         const std::string &message_type)
{
    SE_LOG_DEBUG(NULL, "Connection %s: D-Bus client %s sends %lu bytes, %s (old state %s)",
                 m_sessionID.c_str(),
                 caller.c_str(),
                 (unsigned long)message.first,
                 message_type.c_str(),
                 SessionCommon::ConnectionStateToString(m_state).c_str());

    std::shared_ptr<Client> client(m_server.findClient(caller));
    if (!client) {
        SE_THROW("unknown client");
    }

    std::shared_ptr<Connection> myself =
        std::static_pointer_cast<Connection, Resource>(client->findResource(this));
    if (!myself) {
        SE_THROW("client does not own connection");
    }

    // any kind of error from now on terminates the connection
    try {
        switch (m_state) {
        case SessionCommon::SETUP: {
            std::string config;
            std::string peerDeviceID;
            bool serverMode = false;
            bool serverAlerted = false;
            // check message type, determine whether we act
            // as client or server, choose config
            if (message_type == "HTTP Config") {
                // type used for testing, payload is config name
                config.assign(reinterpret_cast<const char *>(message.second),
                              message.first);
            } else if (message_type == TransportAgent::m_contentTypeServerAlertedNotificationDS) {
                serverAlerted = true;
                sysync::SanPackage san;
                if (san.PassSan(const_cast<uint8_t *>(message.second), message.first, 2) || san.GetHeader()) {
                    // We are very tolerant regarding the content of the message.
                    // If it doesn't parse, try to do something useful anyway.
                    // only for SAN 1.2, for SAN 1.0/1.1 we can not be sure
                    // whether it is a SAN package or a normal sync pacakge
                    if (message_type == TransportAgent::m_contentTypeServerAlertedNotificationDS) {
                        config = "default";
                        SE_LOG_DEBUG(NULL, "SAN parsing failed, falling back to 'default' config");
                    }
                } else { //Server alerted notification case
                    // Extract server ID and match it against a server
                    // configuration.  Multiple different peers might use the
                    // same serverID ("PC Suite"), so check properties of the
                    // of our configs first before going back to the name itself.
                    std::string serverID = san.fServerID;
                    SyncConfig::ConfigList servers = SyncConfig::getConfigs();
                    for (const auto &server: servers) {
                        SyncConfig conf(server.first);
                        vector<string> urls = conf.getSyncURL();
                        for (const string &url: urls) {
                            if (url == serverID) {
                                config = server.first;
                                break;
                            }
                        }
                        if (!config.empty()) {
                            break;
                        }
                    }

                    // for Bluetooth transports match against mac address.
                    auto id = m_peer.find("id"),
                        trans = m_peer.find("transport");
                    if (trans != m_peer.end() && id != m_peer.end()) {
                        if (trans->second == "org.openobex.obexd") {
                            m_peerBtAddr = id->second.substr(0, id->second.find("+"));
                            for (const auto &server: servers) {
                                SyncConfig conf(server.first);
                                vector<string> urls = conf.getSyncURL();
                                for (std::string &url: urls){
                                    url = url.substr (0, url.find("+"));
                                    SE_LOG_DEBUG(NULL, "matching against %s",url.c_str());
                                    if (url == "obex-bt://" + m_peerBtAddr) {
                                        config = server.first;
                                        break;
                                    }
                                }
                                if (!config.empty()){
                                    break;
                                }
                            }
                        }
                    }

                    if (config.empty()) {
                        for (const auto &server: servers) {
                            if (server.first == serverID) {
                                config = serverID;
                                break;
                            }
                        }
                    }

                    // create a default configuration name if none matched
                    if (config.empty()) {
                        config = serverID+"_"+getCurrentTime();
                        SE_LOG_DEBUG(NULL, "SAN Server ID '%s' unknown, falling back to automatically created '%s' config",
                                     serverID.c_str(), config.c_str());
                    }


                    SE_LOG_DEBUG(NULL, "SAN sync with config %s", config.c_str());

                    m_SANContent.reset (new SANContent ());
                    // extract number of sources
                    int numSources = san.fNSync;
                    int syncType;
                    uint32_t contentType;
                    std::string serverURI;
                    if (!numSources) {
                        SE_LOG_DEBUG(NULL, "SAN message with no datastores, using selected modes");
                        // Synchronize all known sources with the default mode.
                        if (san.GetNthSync(0, syncType, contentType, serverURI)) {
                            SE_LOG_DEBUG(NULL, "SAN invalid header, using default modes");
                        } else if (syncType < SYNC_FIRST || syncType > SYNC_LAST) {
                            SE_LOG_DEBUG(NULL, "SAN invalid sync type %d, using default modes", syncType);
                        } else {
                            m_syncMode = PrettyPrintSyncMode(SyncMode(syncType), true);
                            SE_LOG_DEBUG(NULL, "SAN sync mode for all configured datastores: %s", m_syncMode.c_str());
                        }
                    } else {
                        for (int sync = 1; sync <= numSources; sync++) {
                            if (san.GetNthSync(sync, syncType, contentType, serverURI)) {
                                SE_LOG_DEBUG(NULL, "SAN invalid sync entry #%d", sync);
                            } else if (syncType < SYNC_FIRST || syncType > SYNC_LAST) {
                                SE_LOG_DEBUG(NULL, "SAN invalid sync type %d for entry #%d, ignoring entry", syncType, sync);
                            } else {
                                std::string syncMode = PrettyPrintSyncMode(SyncMode(syncType), true);
                                m_SANContent->m_syncType.push_back (syncMode);
                                m_SANContent->m_serverURI.push_back (serverURI);
                                m_SANContent->m_contentType.push_back (contentType);
                            }
                        }
                    }
                }
                // TODO: use the session ID set by the server if non-null
            } else if (// relaxed checking for XML: ignore stuff like "; CHARSET=UTF-8"
                       IsSyncML(message_type.substr(0, message_type.find(';')))) {
                // run a new SyncML session as server
                serverMode = true;
                if (m_peer.find("config") == m_peer.end() &&
                    !m_peer["config"].empty()) {
                    SE_LOG_DEBUG(NULL, "ignoring pre-chosen config '%s'",
                                 m_peer["config"].c_str());
                }

                // peek into the data to extract the locURI = device ID,
                // then use it to find the configuration
                SyncContext::SyncMLMessageInfo info;
                info = SyncContext::analyzeSyncMLMessage(reinterpret_cast<const char *>(message.second),
                                                         message.first,
                                                         message_type);
                if (info.m_deviceID.empty()) {
                    // TODO: proper exception
                    SE_THROW("could not extract LocURI=deviceID from initial message");
                }
                for (const auto &entry: SyncConfig::getConfigs()) {
                    SyncConfig peer(entry.first);
                    if (info.m_deviceID == peer.getRemoteDevID()) {
                        config = entry.first;
                        SE_LOG_INFO(NULL, "matched %s against config %s (%s)",
                                    info.toString().c_str(),
                                    entry.first.c_str(),
                                    entry.second.c_str());
                        // Stop searching. Other peer configs might have the same remoteDevID.
                        // We go with the first one found, which because of the sort order
                        // of getConfigs() ensures that "foo" is found before "foo.old".
                        break;
                    }
                }
                if (config.empty()) {
                    // TODO: proper exception
                    SE_THROW(string("no configuration found for ") + info.toString());
                }

                // identified peer, still need to abort previous sessions below
                peerDeviceID = info.m_deviceID;
            } else {
                SE_THROW(StringPrintf("message type '%s' not supported for starting a sync", message_type.c_str()));
            }

            // run session as client or server
            m_state = SessionCommon::PROCESSING;
            m_session = make_weak_shared::make<Session>(m_server,
                                                           peerDeviceID,
                                                           config,
                                                           m_sessionID);
            m_session->activate();
            if (serverMode) {
                m_session->initServer(SharedBuffer(reinterpret_cast<const char *>(message.second),
                                                   message.first),
                                      message_type);
            }
            m_session->setServerAlerted(serverAlerted);
            m_session->setPriority(Session::PRI_CONNECTION);
            m_session->setStubConnection(myself);
            // this will be reset only when the connection shuts down okay
            // or overwritten with the error given to us in
            // Connection::close()
            m_session->setStubConnectionError("closed prematurely");

            // Now abort all earlier sessions, if necessary.  The new
            // session will be enqueued below and thus won't get
            // killed. It also won't run unless all other sessions
            // before it terminate, therefore we don't need to check
            // for success.
            if (!peerDeviceID.empty()) {
                // TODO: On failure we should kill the connection (beware,
                // it might go away before killing completes and/or
                // fails - need to use shared pointer tracking).
                //
                // std::shared_ptr<Connection> c = m_me.lock();
                // if (!c) {
                //     SE_THROW("internal error: Connection::process() cannot lock its own instance");
                // }
                m_server.killSessionsAsync(peerDeviceID,
                                           SimpleResult(SuccessCb_t(),
                                                        ErrorCb_t()));
            }
            m_server.enqueue(m_session);
            break;
        }
        case SessionCommon::PROCESSING:
            SE_THROW("protocol error: already processing a message");
            break;
        case SessionCommon::WAITING:
            m_incomingMsg = SharedBuffer(reinterpret_cast<const char *>(message.second),
                                         message.first);
            m_incomingMsgType = message_type;
            m_messageSignal(DBusArray<uint8_t>(m_incomingMsg.size(),
                                               reinterpret_cast<uint8_t *>(m_incomingMsg.get())),
                            m_incomingMsgType);
            m_state = SessionCommon::PROCESSING;
            m_timeout.deactivate();
            break;
        case SessionCommon::FINAL:
            SE_THROW("protocol error: final reply sent, no further message processing possible");
        case SessionCommon::DONE:
            SE_THROW("protocol error: connection closed, no further message processing possible");
            break;
        case SessionCommon::FAILED:
            SE_THROW(m_failure);
            break;
        default:
            SE_THROW("protocol error: unknown internal state");
            break;
        }
    } catch (const std::exception &error) {
        failed(error.what());
        throw;
    } catch (...) {
        failed("unknown exception in Connection::process");
        throw;
    }
}

void Connection::send(const DBusArray<uint8_t> buffer,
                      const std::string &type,
                      const std::string &url)
{
    SE_LOG_DEBUG(NULL, "Connection %s: send %lu bytes, %s, %s (old state %s)",
                 m_sessionID.c_str(),
                 (unsigned long)buffer.first,
                 type.c_str(),
                 url.c_str(),
                 SessionCommon::ConnectionStateToString(m_state).c_str());

    if (m_state != SessionCommon::PROCESSING) {
        SE_THROW_EXCEPTION(TransportException,
                           "cannot send to our D-Bus peer");
    }

    // Change state in advance. If we fail while replying, then all
    // further resends will fail with the error above.
    m_state = SessionCommon::WAITING;
    activateTimeout();
    m_incomingMsg = SharedBuffer();

    // TODO: turn D-Bus exceptions into transport exceptions
    StringMap meta;
    meta["URL"] = url;
    reply(buffer, type, meta, false, m_sessionID);
}

void Connection::sendFinalMsg()
{
    SE_LOG_DEBUG(NULL, "Connection %s: shut down (old state %s)",
                 m_sessionID.c_str(),
                 SessionCommon::ConnectionStateToString(m_state).c_str());
    if (m_state == SessionCommon::PROCESSING) {
        // send final, empty message and wait for close
        m_state = SessionCommon::FINAL;
        reply(GDBusCXX::DBusArray<uint8_t>(0, 0),
              "", StringMap(),
              true, m_sessionID);
    }
}

void Connection::close(const Caller_t &caller,
                       bool normal,
                       const std::string &error)
{
    SE_LOG_DEBUG(NULL, "Connection %s: client %s closes connection %s %s%s%s (old state %s)",
                 m_sessionID.c_str(),
                 caller.c_str(),
                 getPath(),
                 normal ? "normally" : "with error",
                 error.empty() ? "" : ": ",
                 error.c_str(),
                 SessionCommon::ConnectionStateToString(m_state).c_str());

    std::shared_ptr<Client> client(m_server.findClient(caller));
    if (!client) {
        SE_THROW("unknown client");
    }

    // Remove reference to us from client, will destruct *this*
    // instance. To let us finish our work safely, keep a reference
    // that the server will unref when everything is idle again.
    auto c = weak_from_this().lock();
    if (!c) {
        SE_THROW("connection already destructing");
    }
    m_server.delayDeletion(c);
    client->detach(this);

    if (!normal ||
        m_state != SessionCommon::FINAL) {
        std::string err = error.empty() ?
            "connection closed unexpectedly" :
            error;
        m_statusSignal(err);
        if (m_session) {
            m_session->setStubConnectionError(err);
        }
        failed(err);
    } else {
        m_state = SessionCommon::DONE;
        m_statusSignal("");
        if (m_session) {
            m_session->setStubConnectionError("");
        }
    }

    // TODO (?): errors during shutdown of the helper will not get
    // reported back to the client, which sees the operation as
    // completed successfully once this call returns.
}

void Connection::abort()
{
    if (!m_abortSent) {
        SE_LOG_DEBUG(NULL, "Connection %s: send abort to client (state %s)",
                     m_sessionID.c_str(),
                     SessionCommon::ConnectionStateToString(m_state).c_str());
        sendAbort();
        m_abortSent = true;
    } else {
        SE_LOG_DEBUG(NULL, "Connection %s: not sending abort to client, already done (state %s)",
                     m_sessionID.c_str(),
                     SessionCommon::ConnectionStateToString(m_state).c_str());
    }
}


void Connection::shutdown()
{
    SE_LOG_DEBUG(NULL, "Connection %s: self-destructing (state %s)",
                 m_sessionID.c_str(),
                 SessionCommon::ConnectionStateToString(m_state).c_str());
    // trigger removal of this connection by removing all
    // references to it
    m_server.detach(this);
}

Connection::Connection(Server &server,
                       const DBusConnectionPtr &conn,
                       const std::string &sessionID,
                       const StringMap &peer,
                       bool must_authenticate) :
    DBusObjectHelper(conn,
                     std::string("/org/syncevolution/Connection/") + sessionID,
                     "org.syncevolution.Connection",
                     [serverPtr=&server] () { serverPtr->autoTermCallback(); }),
    m_server(server),
    m_peer(peer),
    m_mustAuthenticate(must_authenticate),
    m_state(SessionCommon::SETUP),
    m_sessionID(sessionID),
    m_timeoutSeconds(-1),
    sendAbort(*this, "Abort"),
    m_abortSent(false),
    reply(*this, "Reply"),
    m_description(buildDescription(peer))
{
    add(this, &Connection::process, "Process");
    add(this, &Connection::close, "Close");
    add(sendAbort);
    add(reply);
    m_server.autoTermRef();

    SE_LOG_DEBUG(NULL, "Connection %s: created",
                 m_sessionID.c_str());
}

Connection::~Connection()
{
    SE_LOG_DEBUG(NULL, "Connection %s: done with '%s'%s%s%s (old state %s)",
                 m_sessionID.c_str(),
                 m_description.c_str(),
                 m_state == SessionCommon::DONE ? ", normal shutdown" : " unexpectedly",
                 m_failure.empty() ? "" : ": ",
                 m_failure.c_str(),
                 SessionCommon::ConnectionStateToString(m_state).c_str());
    try {
        if (m_state != SessionCommon::DONE) {
            abort();
            m_state = SessionCommon::FAILED;
        }
        // DBusTransportAgent waiting? Wake it up.
        m_statusSignal(m_failure);
        m_session.reset();
    } catch (...) {
        // log errors, but do not propagate them because we are
        // destructing
        Exception::handle();
    }
    m_server.autoTermUnref();
}

void Connection::ready()
{
    SE_LOG_DEBUG(NULL, "Connection %s: ready to run (old state %s)",
                 m_sessionID.c_str(),
                 SessionCommon::ConnectionStateToString(m_state).c_str());

    //if configuration not yet created
    std::string configName = m_session->getConfigName();
    SyncConfig config (configName);
    if (!config.exists() && m_SANContent) {
        SE_LOG_DEBUG(NULL, "Configuration %s not exists for a runnable session in a SAN context, create it automatically", configName.c_str());
        ReadOperations::Config_t from;
        const std::string templateName = "SyncEvolution";
        // TODO: support SAN from other well known servers
        ReadOperations ops(templateName, m_server);
        ops.getConfig(true , from);
        if (!m_peerBtAddr.empty()){
            from[""]["SyncURL"] = string ("obex-bt://") + m_peerBtAddr;
        }
        m_session->setConfig (false, false, from);
    }

    // As we cannot resend messages via D-Bus even if running as
    // client (API not designed for it), let's use the hard server
    // timeout from RetryDuration here.
    m_timeoutSeconds = config.getRetryDuration();

    const SyncContext context (configName);
    std::list<std::string> sources = context.getSyncSources();

    if (m_SANContent && !m_SANContent->m_syncType.empty()) {
        // check what the server wants us to synchronize
        // and only synchronize that
        m_syncMode = "disabled";
        for (size_t sync=0; sync<m_SANContent->m_syncType.size(); sync++) {
            std::string syncMode = m_SANContent->m_syncType[sync];
            std::string serverURI = m_SANContent->m_serverURI[sync];
            //uint32_t contentType = m_SANContent->m_contentType[sync];
            bool found = false;
            for (const std::string &source: sources) {
                std::shared_ptr<const PersistentSyncSourceConfig> sourceConfig(context.getSyncSourceConfig(source));
                // prefix match because the local
                // configuration might contain
                // additional parameters (like date
                // range selection for events)
                if (boost::starts_with(sourceConfig->getURINonEmpty(), serverURI)) {
                    SE_LOG_DEBUG(NULL,
                                 "SAN entry #%d = datastore %s with mode %s",
                                 (int)sync, source.c_str(), syncMode.c_str());
                    m_sourceModes[source] = syncMode;
                    found = true;
                    break;
                }
            }
            if (!found) {
                SE_LOG_DEBUG(NULL,
                             "SAN entry #%d with mode %s ignored because Server URI %s is unknown",
                             (int)sync, syncMode.c_str(), serverURI.c_str());
            }
        }
        if (m_sourceModes.empty()) {
            SE_LOG_DEBUG(NULL,
                    "SAN message with no known entries, falling back to default");
            m_syncMode = "";
        }
    }

    if (m_SANContent) {
        m_session->setRemoteInitiated(true);
    }
    // proceed with sync now that our session is ready
    m_session->sync(m_syncMode, m_sourceModes);
}

void Connection::activateTimeout()
{
    if (m_timeoutSeconds >= 0) {
        auto timeoutCb = [this] () {
            SE_LOG_DEBUG(NULL, "Connection %s: timed out after %ds (state %s)",
                         m_sessionID.c_str(), m_timeoutSeconds,
                         SessionCommon::ConnectionStateToString(m_state).c_str());
            failed(StringPrintf("timed out after %ds", m_timeoutSeconds));
        };
        m_timeout.runOnce(m_timeoutSeconds, timeoutCb);
    } else {
        m_timeout.deactivate();
    }
}

SE_END_CXX
