/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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.
 */
#define DEBUG false
#include "Log.h"

#include "Reporter.h"

#include "Privacy.h"
#include "report_directory.h"
#include "section_list.h"

#include <android-base/properties.h>
#include <android/os/DropBoxManager.h>
#include <private/android_filesystem_config.h>
#include <utils/SystemClock.h>

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string>

/**
 * The directory where the incident reports are stored.
 */
static const char* INCIDENT_DIRECTORY = "/data/misc/incidents/";

namespace android {
namespace os {
namespace incidentd {

// ================================================================================
ReportRequest::ReportRequest(const IncidentReportArgs& a,
                             const sp<IIncidentReportStatusListener>& l, int f)
    : args(a), listener(l), fd(f), err(NO_ERROR) {}

ReportRequest::~ReportRequest() {
    if (fd >= 0) {
        // clean up the opened file descriptor
        close(fd);
    }
}

bool ReportRequest::ok() { return fd >= 0 && err == NO_ERROR; }

// ================================================================================
ReportRequestSet::ReportRequestSet()
    : mRequests(), mSections(), mMainFd(-1), mMainDest(-1), mMetadata(), mSectionStats() {}

ReportRequestSet::~ReportRequestSet() {}

// TODO: dedup on exact same args and fd, report the status back to listener!
void ReportRequestSet::add(const sp<ReportRequest>& request) {
    mRequests.push_back(request);
    mSections.merge(request->args);
    mMetadata.set_request_size(mMetadata.request_size() + 1);
}

void ReportRequestSet::setMainFd(int fd) {
    mMainFd = fd;
    mMetadata.set_use_dropbox(fd > 0);
}

void ReportRequestSet::setMainDest(int dest) {
    mMainDest = dest;
    PrivacySpec spec = PrivacySpec::new_spec(dest);
    switch (spec.dest) {
        case android::os::DEST_AUTOMATIC:
            mMetadata.set_dest(IncidentMetadata_Destination_AUTOMATIC);
            break;
        case android::os::DEST_EXPLICIT:
            mMetadata.set_dest(IncidentMetadata_Destination_EXPLICIT);
            break;
        case android::os::DEST_LOCAL:
            mMetadata.set_dest(IncidentMetadata_Destination_LOCAL);
            break;
    }
}

bool ReportRequestSet::containsSection(int id) { return mSections.containsSection(id); }

IncidentMetadata::SectionStats* ReportRequestSet::sectionStats(int id) {
    if (mSectionStats.find(id) == mSectionStats.end()) {
        IncidentMetadata::SectionStats stats;
        stats.set_id(id);
        mSectionStats[id] = stats;
    }
    return &mSectionStats[id];
}

// ================================================================================
Reporter::Reporter() : Reporter(INCIDENT_DIRECTORY) { isTest = false; };

Reporter::Reporter(const char* directory) : batch() {
    char buf[100];

    mMaxSize = 30 * 1024 * 1024;  // incident reports can take up to 30MB on disk
    mMaxCount = 100;

    // string ends up with '/' is a directory
    String8 dir = String8(directory);
    if (directory[dir.size() - 1] != '/') dir += "/";
    mIncidentDirectory = dir.string();

    // There can't be two at the same time because it's on one thread.
    mStartTime = time(NULL);
    strftime(buf, sizeof(buf), "incident-%Y%m%d-%H%M%S", localtime(&mStartTime));
    mFilename = mIncidentDirectory + buf;
}

Reporter::~Reporter() {}

Reporter::run_report_status_t Reporter::runReport(size_t* reportByteSize) {
    status_t err = NO_ERROR;
    bool needMainFd = false;
    int mainFd = -1;
    int mainDest = -1;
    HeaderSection headers;
    MetadataSection metadataSection;
    std::string buildType = android::base::GetProperty("ro.build.type", "");
    const bool isUserdebugOrEng = buildType == "userdebug" || buildType == "eng";

    // See if we need the main file
    for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) {
        if ((*it)->fd < 0 && mainFd < 0) {
            needMainFd = true;
            mainDest = (*it)->args.dest();
            break;
        }
    }
    if (needMainFd) {
        // Create the directory
        if (!isTest) err = create_directory(mIncidentDirectory);
        if (err != NO_ERROR) {
            goto DONE;
        }

        // If there are too many files in the directory (for whatever reason),
        // delete the oldest ones until it's under the limit. Doing this first
        // does mean that we can go over, so the max size is not a hard limit.
        if (!isTest) clean_directory(mIncidentDirectory, mMaxSize, mMaxCount);

        // Open the file.
        err = create_file(&mainFd);
        if (err != NO_ERROR) {
            goto DONE;
        }

        // Add to the set
        batch.setMainFd(mainFd);
        batch.setMainDest(mainDest);
    }

    // Tell everyone that we're starting.
    for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) {
        if ((*it)->listener != NULL) {
            (*it)->listener->onReportStarted();
        }
    }

    // Write the incident headers
    headers.Execute(&batch);

    // For each of the report fields, see if we need it, and if so, execute the command
    // and report to those that care that we're doing it.
    for (const Section** section = SECTION_LIST; *section; section++) {
        const int id = (*section)->id;
        if ((*section)->userdebugAndEngOnly && !isUserdebugOrEng) {
            ALOGD("Skipping incident report section %d '%s' because it's limited to userdebug/eng",
                  id, (*section)->name.string());
            continue;
        }
        if (this->batch.containsSection(id)) {
            ALOGD("Taking incident report section %d '%s'", id, (*section)->name.string());
            for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) {
                if ((*it)->listener != NULL && (*it)->args.containsSection(id)) {
                    (*it)->listener->onReportSectionStatus(
                            id, IIncidentReportStatusListener::STATUS_STARTING);
                }
            }

            // Execute - go get the data and write it into the file descriptors.
            IncidentMetadata::SectionStats* stats = batch.sectionStats(id);
            int64_t startTime = uptimeMillis();
            err = (*section)->Execute(&batch);
            int64_t endTime = uptimeMillis();
            stats->set_success(err == NO_ERROR);
            stats->set_exec_duration_ms(endTime - startTime);
            if (err != NO_ERROR) {
                ALOGW("Incident section %s (%d) failed: %s. Stopping report.",
                      (*section)->name.string(), id, strerror(-err));
                goto DONE;
            }
            (*reportByteSize) += stats->report_size_bytes();

            // Notify listener of starting
            for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) {
                if ((*it)->listener != NULL && (*it)->args.containsSection(id)) {
                    (*it)->listener->onReportSectionStatus(
                            id, IIncidentReportStatusListener::STATUS_FINISHED);
                }
            }
            ALOGD("Finish incident report section %d '%s'", id, (*section)->name.string());
        }
    }

DONE:
    // Reports the metdadata when taking the incident report.
    if (!isTest) metadataSection.Execute(&batch);

    // Close the file.
    if (mainFd >= 0) {
        close(mainFd);
    }

    // Tell everyone that we're done.
    for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) {
        if ((*it)->listener != NULL) {
            if (err == NO_ERROR) {
                (*it)->listener->onReportFinished();
            } else {
                (*it)->listener->onReportFailed();
            }
        }
    }

    // Put the report into dropbox.
    if (needMainFd && err == NO_ERROR) {
        sp<DropBoxManager> dropbox = new DropBoxManager();
        Status status = dropbox->addFile(String16("incident"), mFilename, 0);
        ALOGD("Incident report done. dropbox status=%s\n", status.toString8().string());
        if (!status.isOk()) {
            return REPORT_NEEDS_DROPBOX;
        }

        // If the status was ok, delete the file. If not, leave it around until the next
        // boot or the next checkin. If the directory gets too big older files will
        // be rotated out.
        if (!isTest) unlink(mFilename.c_str());
    }

    return REPORT_FINISHED;
}

/**
 * Create our output file and set the access permissions to -rw-rw----
 */
status_t Reporter::create_file(int* fd) {
    const char* filename = mFilename.c_str();

    *fd = open(filename, O_CREAT | O_TRUNC | O_RDWR | O_CLOEXEC, 0660);
    if (*fd < 0) {
        ALOGE("Couldn't open incident file: %s (%s)", filename, strerror(errno));
        return -errno;
    }

    // Override umask. Not super critical. If it fails go on with life.
    chmod(filename, 0660);

    if (chown(filename, AID_INCIDENTD, AID_INCIDENTD)) {
        ALOGE("Unable to change ownership of incident file %s: %s\n", filename, strerror(errno));
        status_t err = -errno;
        unlink(mFilename.c_str());
        return err;
    }

    return NO_ERROR;
}

Reporter::run_report_status_t Reporter::upload_backlog() {
    DIR* dir;
    struct dirent* entry;
    struct stat st;
    status_t err;

    ALOGD("Start uploading backlogs in %s", INCIDENT_DIRECTORY);
    if ((err = create_directory(INCIDENT_DIRECTORY)) != NO_ERROR) {
        ALOGE("directory doesn't exist: %s", strerror(-err));
        return REPORT_FINISHED;
    }

    if ((dir = opendir(INCIDENT_DIRECTORY)) == NULL) {
        ALOGE("Couldn't open incident directory: %s", INCIDENT_DIRECTORY);
        return REPORT_NEEDS_DROPBOX;
    }

    sp<DropBoxManager> dropbox = new DropBoxManager();

    // Enumerate, count and add up size
    int count = 0;
    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_name[0] == '.') {
            continue;
        }
        String8 filename = String8(INCIDENT_DIRECTORY) + entry->d_name;
        if (stat(filename.string(), &st) != 0) {
            ALOGE("Unable to stat file %s", filename.string());
            continue;
        }
        if (!S_ISREG(st.st_mode)) {
            continue;
        }

        Status status = dropbox->addFile(String16("incident"), filename.string(), 0);
        ALOGD("Incident report done. dropbox status=%s\n", status.toString8().string());
        if (!status.isOk()) {
            return REPORT_NEEDS_DROPBOX;
        }

        // If the status was ok, delete the file. If not, leave it around until the next
        // boot or the next checkin. If the directory gets too big older files will
        // be rotated out.
        unlink(filename.string());
        count++;
    }
    ALOGD("Successfully uploaded %d files to Dropbox.", count);
    closedir(dir);

    return REPORT_FINISHED;
}

}  // namespace incidentd
}  // namespace os
}  // namespace android
