// Copyright (c) 2016 Baidu, Inc.
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Authors: Ge,Jun (gejun@baidu.com)

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include "butil/fast_rand.h"
#include "brpc/log.h"
#include "brpc/channel.h"
#include "brpc/trackme.pb.h"
#include "brpc/policy/hasher.h"
#include "butil/files/scoped_file.h"

namespace brpc {

#ifdef BAIDU_INTERNAL
DEFINE_string(trackme_server, "http://brpc.baidu.com:8877",
              "Where the TrackMe requests are sent to");
#else
DEFINE_string(trackme_server, "", "Where the TrackMe requests are sent to");
#endif

static const int32_t TRACKME_MIN_INTERVAL = 30;
static const int32_t TRACKME_MAX_INTERVAL = 600;
static int32_t s_trackme_interval = TRACKME_MIN_INTERVAL;
// Protecting global vars on trackme
static pthread_mutex_t g_trackme_mutex = PTHREAD_MUTEX_INITIALIZER;
// For contacting with trackme_server.
static Channel* g_trackme_chan = NULL;
// Any server address in this process.
static std::string* g_trackme_addr = NULL;

// Information of bugs.
// Notice that this structure may be a combination of all affected bugs.
// Namely `severity' is severity of the worst bug and `error_text' is
// combination of descriptions of all bugs. Check tools/trackme_server
// for impl. details.
struct BugInfo {
    TrackMeSeverity severity;
    std::string error_text;
    bool operator==(const BugInfo& bug) const {
        return severity == bug.severity && error_text == bug.error_text;
    }
};
// If a bug was shown, its info was stored in this var as well so that we
// can avoid showing the same bug repeatly.
static BugInfo* g_bug_info = NULL;
// The timestamp(microseconds) that we sent TrackMeRequest.
static int64_t g_trackme_last_time = 0;

// version of RPC.
// Since the code for getting BRPC_REVISION often fails, 
// BRPC_REVISION must be defined to string and be converted to number
// within our code.
// The code running before main() may see g_rpc_version=0, should be OK.
#if defined(BRPC_REVISION)
const int64_t g_rpc_version = atoll(BRPC_REVISION);
#else
const int64_t g_rpc_version = 0;
#endif

int ReadJPaasHostPort(int container_port) {
    const uid_t uid = getuid();
    struct passwd* pw = getpwuid(uid);
    if (pw == NULL) {
        RPC_VLOG << "Fail to get password file entry of uid=" << uid;
        return -1;
    }
    char JPAAS_LOG_PATH[64];
    snprintf(JPAAS_LOG_PATH, sizeof(JPAAS_LOG_PATH),
             "%s/jpaas_run/logs/env.log", pw->pw_dir);
    char* line = NULL;
    size_t line_len = 0;
    ssize_t nr = 0;
    butil::ScopedFILE fp(fopen(JPAAS_LOG_PATH, "r"));
    if (!fp) {
        RPC_VLOG << "Fail to open `" << JPAAS_LOG_PATH << '\'';
        return -1;
    }
    int host_port = -1;
    char prefix[32];
    const int prefix_len =
        snprintf(prefix, sizeof(prefix), "JPAAS_HOST_PORT_%d=", container_port);
    while ((nr = getline(&line, &line_len, fp.get())) != -1) {
        if (line[nr - 1] == '\n') { // remove ending newline
            --nr;
        }
        if (nr > prefix_len && memcmp(line, prefix, prefix_len) == 0) {
            host_port = strtol(line + prefix_len, NULL, 10);
            break;
        }
    }
    free(line);
    RPC_VLOG_IF(host_port < 0) << "No entry starting with `" << prefix << "' found";
    return host_port;
}

// Called in server.cpp
void SetTrackMeAddress(butil::EndPoint pt) {
    BAIDU_SCOPED_LOCK(g_trackme_mutex);
    if (g_trackme_addr == NULL) {
        // JPAAS has NAT capabilities, read its log to figure out the open port
        // accessible from outside.
        const int jpaas_port = ReadJPaasHostPort(pt.port);
        if (jpaas_port > 0) {
            RPC_VLOG << "Use jpaas_host_port=" << jpaas_port
                     << " instead of jpaas_container_port=" << pt.port;
            pt.port = jpaas_port;
        }
        g_trackme_addr = new std::string(butil::endpoint2str(pt).c_str());
    }
}

static void HandleTrackMeResponse(Controller* cntl, TrackMeResponse* res) {
    if (cntl->Failed()) {
        RPC_VLOG << "Fail to access " << FLAGS_trackme_server << ", " << cntl->ErrorText();
    } else {
        BugInfo cur_info;
        cur_info.severity = res->severity();
        cur_info.error_text = res->error_text();
        bool already_reported = false;
        {
            BAIDU_SCOPED_LOCK(g_trackme_mutex);
            if (g_bug_info != NULL && *g_bug_info == cur_info) {
                // we've shown the bug.
                already_reported = true;
            } else {
                // save the bug.
                if (g_bug_info == NULL) {
                    g_bug_info = new BugInfo(cur_info);
                } else {
                    *g_bug_info = cur_info;
                }
            }
        }
        if (!already_reported) {
            switch (res->severity()) {
            case TrackMeOK:
                break;
            case TrackMeFatal:
                LOG(ERROR) << "Your brpc (r" << g_rpc_version
                           << ") is affected by: " << res->error_text();
                break;
            case TrackMeWarning:
                LOG(WARNING) << "Your brpc (r" << g_rpc_version
                             << ") is affected by: " << res->error_text();
                break;
            default:
                LOG(WARNING) << "Unknown severity=" << res->severity();
                break;
            }
        }
        if (res->has_new_interval()) {
            // We can't fully trust the result from trackme_server which may
            // have bugs. Make sure the reporting interval is not too short or
            // too long
            int32_t new_interval = res->new_interval();
            new_interval = std::max(new_interval, TRACKME_MIN_INTERVAL);
            new_interval = std::min(new_interval, TRACKME_MAX_INTERVAL);
            if (new_interval != s_trackme_interval) {
                s_trackme_interval = new_interval;
                RPC_VLOG << "Update s_trackme_interval to " << new_interval;
            }
        }
    }
    delete cntl;
    delete res;
}

static void TrackMeNow(std::unique_lock<pthread_mutex_t>& mu) {
    if (g_trackme_addr == NULL) {
        return;
    }
    if (g_trackme_chan == NULL) {
        Channel* chan = new (std::nothrow) Channel;
        if (chan == NULL) {
            LOG(FATAL) << "Fail to new trackme channel";
            return;
        }
        ChannelOptions opt;
        // keep #connections on server-side low
        opt.connection_type = CONNECTION_TYPE_SHORT;
        if (chan->Init(FLAGS_trackme_server.c_str(), "c_murmurhash", &opt) != 0) {
            LOG(WARNING) << "Fail to connect to " << FLAGS_trackme_server;
            delete chan;
            return;
        }
        g_trackme_chan = chan;
    }
    mu.unlock();
    TrackMeService_Stub stub(g_trackme_chan);
    TrackMeRequest req;
    req.set_rpc_version(g_rpc_version);
    req.set_server_addr(*g_trackme_addr);
    TrackMeResponse* res = new TrackMeResponse;
    Controller* cntl = new Controller;
    cntl->set_request_code(policy::MurmurHash32(g_trackme_addr->data(), g_trackme_addr->size()));
    google::protobuf::Closure* done = 
        ::brpc::NewCallback(&HandleTrackMeResponse, cntl, res);
    stub.TrackMe(cntl, &req, res, done);
}

// Called in global.cpp
// [Thread-safe] supposed to be called in low frequency.
void TrackMe() {
    if (FLAGS_trackme_server.empty()) {
        return;
    }
    int64_t now = butil::gettimeofday_us();
    std::unique_lock<pthread_mutex_t> mu(g_trackme_mutex);
    if (g_trackme_last_time == 0) {
        // Delay the first ping randomly within s_trackme_interval. This 
        // protects trackme_server from ping storms.
        g_trackme_last_time =
            now + butil::fast_rand_less_than(s_trackme_interval) * 1000000L;
    }
    if (now > g_trackme_last_time + 1000000L * s_trackme_interval) {
        g_trackme_last_time = now;
        return TrackMeNow(mu);
    }
}

} // namespace brpc
