/**
 * Copyright (c) 2017, Timothy Stack
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * * Neither the name of Timothy Stack nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <filesystem>

#include "fstat_vtab.hh"

#include <fcntl.h>
#include <glob.h>
#include <grp.h>
#include <pwd.h>
#include <string.h>
#include <sys/stat.h>

#include "base/auto_mem.hh"
#include "base/fs_util.hh"
#include "base/injector.hh"
#include "base/lnav_log.hh"
#include "bound_tags.hh"
#include "config.h"
#include "sql_help.hh"
#include "sql_util.hh"
#include "vtab_module.hh"

namespace {

enum {
    FSTAT_COL_PARENT,
    FSTAT_COL_NAME,
    FSTAT_COL_DEV,
    FSTAT_COL_INO,
    FSTAT_COL_TYPE,
    FSTAT_COL_MODE,
    FSTAT_COL_NLINK,
    FSTAT_COL_UID,
    FSTAT_COL_USER,
    FSTAT_COL_GID,
    FSTAT_COL_GROUP,
    FSTAT_COL_RDEV,
    FSTAT_COL_SIZE,
    FSTAT_COL_BLKSIZE,
    FSTAT_COL_BLOCKS,
    FSTAT_COL_ATIME,
    FSTAT_COL_MTIME,
    FSTAT_COL_CTIME,
    FSTAT_COL_ERROR,
    FSTAT_COL_PATTERN,
    FSTAT_COL_DATA,
};

/**
 * @feature f0:sql.tables.fstat
 */
struct fstat_table {
    static constexpr const char* NAME = "fstat";
    static constexpr const char* CREATE_STMT = R"(
-- The fstat() table-valued function allows you to query the file system.
CREATE TABLE fstat (
    st_parent TEXT,
    st_name TEXT,
    st_dev INTEGER,
    st_ino INTEGER,
    st_type TEXT,
    st_mode INTEGER,
    st_nlink INTEGER,
    st_uid TEXT,
    st_user TEXT,
    st_gid TEXT,
    st_group TEXT,
    st_rdev INTEGER,
    st_size INTEGER,
    st_blksize INTEGER,
    st_blocks INTEGER,
    st_atime DATETIME,
    st_mtime DATETIME,
    st_ctime DATETIME,
    error TEXT,
    pattern TEXT HIDDEN,
    data BLOB HIDDEN
);
)";

    struct cursor {
        sqlite3_vtab_cursor base;
        std::string c_pattern;
        static_root_mem<glob_t, globfree> c_glob;
        size_t c_path_index{0};
        struct stat c_stat;
        std::string c_error;

        explicit cursor(sqlite3_vtab* vt) : base({vt})
        {
            memset(&this->c_stat, 0, sizeof(this->c_stat));
        }

        void load_stat()
        {
            if (this->c_path_index >= this->c_glob->gl_pathc) {
                return;
            }
            auto rc = lstat(this->c_glob->gl_pathv[this->c_path_index],
                            &this->c_stat);

            if (rc == -1) {
                this->c_error = strerror(errno);
            }
        }

        int next()
        {
            log_debug("fstat_vtab::next %d %d",
                      this->c_path_index,
                      this->c_glob->gl_pathc);
            if (this->c_path_index < this->c_glob->gl_pathc) {
                this->c_path_index += 1;
                this->load_stat();
            }

            return SQLITE_OK;
        }

        int reset() { return SQLITE_OK; }

        int eof()
        {
            log_debug("eof %d %d", this->c_path_index, this->c_glob->gl_pathc);
            return this->c_path_index >= this->c_glob->gl_pathc;
        }

        int get_rowid(sqlite3_int64& rowid_out)
        {
            rowid_out = this->c_path_index;

            return SQLITE_OK;
        }
    };

    int get_column(const cursor& vc, sqlite3_context* ctx, int col)
    {
        const char* path = vc.c_glob->gl_pathv[vc.c_path_index];
        char time_buf[32];

        switch (col) {
            case FSTAT_COL_PARENT: {
                const char* slash = strrchr(path, '/');

                if (slash == nullptr) {
                    sqlite3_result_text(ctx, ".", 1, SQLITE_STATIC);
                } else if (path[1] == '\0') {
                    sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
                } else {
                    sqlite3_result_text(
                        ctx, path, slash - path + 1, SQLITE_TRANSIENT);
                }
                break;
            }
            case FSTAT_COL_NAME: {
                const char* slash = strrchr(path, '/');

                if (slash == nullptr) {
                    sqlite3_result_text(ctx, path, -1, SQLITE_TRANSIENT);
                } else {
                    sqlite3_result_text(ctx, slash + 1, -1, SQLITE_TRANSIENT);
                }
                break;
            }
            case FSTAT_COL_DEV:
                if (vc.c_error.empty()) {
                    sqlite3_result_int(ctx, vc.c_stat.st_dev);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_INO:
                if (vc.c_error.empty()) {
                    sqlite3_result_int64(ctx, vc.c_stat.st_ino);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_TYPE:
                if (!vc.c_error.empty()) {
                    sqlite3_result_null(ctx);
                } else if (S_ISREG(vc.c_stat.st_mode)) {
                    sqlite3_result_text(ctx, "reg", 3, SQLITE_STATIC);
                } else if (S_ISBLK(vc.c_stat.st_mode)) {
                    sqlite3_result_text(ctx, "blk", 3, SQLITE_STATIC);
                } else if (S_ISCHR(vc.c_stat.st_mode)) {
                    sqlite3_result_text(ctx, "chr", 3, SQLITE_STATIC);
                } else if (S_ISDIR(vc.c_stat.st_mode)) {
                    sqlite3_result_text(ctx, "dir", 3, SQLITE_STATIC);
                } else if (S_ISFIFO(vc.c_stat.st_mode)) {
                    sqlite3_result_text(ctx, "fifo", 4, SQLITE_STATIC);
                } else if (S_ISLNK(vc.c_stat.st_mode)) {
                    sqlite3_result_text(ctx, "lnk", 3, SQLITE_STATIC);
                } else if (S_ISSOCK(vc.c_stat.st_mode)) {
                    sqlite3_result_text(ctx, "sock", 3, SQLITE_STATIC);
                }
                break;
            case FSTAT_COL_MODE:
                if (vc.c_error.empty()) {
                    sqlite3_result_int(ctx, vc.c_stat.st_mode & 0777);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_NLINK:
                if (vc.c_error.empty()) {
                    sqlite3_result_int(ctx, vc.c_stat.st_nlink);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_UID:
                if (vc.c_error.empty()) {
                    sqlite3_result_int(ctx, vc.c_stat.st_uid);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_USER: {
                if (vc.c_error.empty()) {
                    struct passwd* pw = getpwuid(vc.c_stat.st_uid);

                    if (pw != nullptr) {
                        sqlite3_result_text(
                            ctx, pw->pw_name, -1, SQLITE_TRANSIENT);
                    } else {
                        sqlite3_result_int(ctx, vc.c_stat.st_uid);
                    }
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            }
            case FSTAT_COL_GID:
                if (vc.c_error.empty()) {
                    sqlite3_result_int(ctx, vc.c_stat.st_gid);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_GROUP: {
                if (vc.c_error.empty()) {
                    struct group* gr = getgrgid(vc.c_stat.st_gid);

                    if (gr != nullptr) {
                        sqlite3_result_text(
                            ctx, gr->gr_name, -1, SQLITE_TRANSIENT);
                    } else {
                        sqlite3_result_int(ctx, vc.c_stat.st_gid);
                    }
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            }
            case FSTAT_COL_RDEV:
                if (vc.c_error.empty()) {
                    sqlite3_result_int(ctx, vc.c_stat.st_rdev);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_SIZE:
                if (vc.c_error.empty()) {
                    sqlite3_result_int64(ctx, vc.c_stat.st_size);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_BLKSIZE:
                if (vc.c_error.empty()) {
                    sqlite3_result_int(ctx, vc.c_stat.st_blksize);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_BLOCKS:
                if (vc.c_error.empty()) {
                    sqlite3_result_int(ctx, vc.c_stat.st_blocks);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_ATIME:
                if (vc.c_error.empty()) {
                    sql_strftime(
                        time_buf, sizeof(time_buf), vc.c_stat.st_atime, 0);
                    sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_MTIME:
                if (vc.c_error.empty()) {
                    sql_strftime(
                        time_buf, sizeof(time_buf), vc.c_stat.st_mtime, 0);
                    sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_CTIME:
                if (vc.c_error.empty()) {
                    sql_strftime(
                        time_buf, sizeof(time_buf), vc.c_stat.st_ctime, 0);
                    sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT);
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            case FSTAT_COL_ERROR:
                if (vc.c_error.empty()) {
                    sqlite3_result_null(ctx);
                } else {
                    to_sqlite(ctx, vc.c_error);
                }
                break;
            case FSTAT_COL_PATTERN:
                sqlite3_result_text(ctx,
                                    vc.c_pattern.c_str(),
                                    vc.c_pattern.length(),
                                    SQLITE_TRANSIENT);
                break;
            case FSTAT_COL_DATA: {
                auto fs_path = std::filesystem::path{path};
                if (!vc.c_error.empty()) {
                    sqlite3_result_null(ctx);
                } else if (S_ISREG(vc.c_stat.st_mode)) {
                    auto open_res
                        = lnav::filesystem::open_file(fs_path, O_RDONLY);

                    if (open_res.isErr()) {
                        log_error("unable to read file: %s -- %s",
                                  path,
                                  open_res.unwrapErr().c_str());
                        sqlite3_result_null(ctx);
                    } else {
                        auto buffer = auto_buffer::alloc(vc.c_stat.st_size);
                        auto fd = open_res.unwrap();

                        while (true) {
                            if (buffer.available() == 0) {
                                buffer.expand_by(4096);
                            }
                            auto rc = read(fd,
                                           buffer.next_available(),
                                           buffer.available());

                            if (rc <= 0) {
                                break;
                            }
                            buffer.resize_by(rc);
                        }
                        to_sqlite(ctx, blob_auto_buffer{std::move(buffer)});
                    }
                } else if (S_ISLNK(vc.c_stat.st_mode)) {
                    auto link_path = std::filesystem::read_symlink(fs_path);

                    to_sqlite(ctx, link_path.string());
                } else {
                    sqlite3_result_null(ctx);
                }
                break;
            }
        }

        return SQLITE_OK;
    }

#if 0
    int update_row(sqlite3_vtab *tab,
                   sqlite3_int64 &index,
                   const char *st_parent,
                   const char *st_name,
                   int64_t st_dev,
                   int64_t st_ino,
                   const char *st_type,
                   int64_t st_mode,
                   int64_t st_nlink,
                   int64_t st_uid,
                   const char *st_user,
                   int64_t st_gid,
                   const char *st_group,
                   int64_t st_rdev,
                   int64_t st_size,
                   int64_t st_blksize,
                   int64_t st_blocks,
                   const char *atime,
                   const char *mtime,
                   const char *ctime,
                   const char *pattern) {

    };
#endif
};

int
rcBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo)
{
    vtab_index_constraints vic(pIdxInfo);
    vtab_index_usage viu(pIdxInfo);

    for (auto iter = vic.begin(); iter != vic.end(); ++iter) {
        if (iter->op != SQLITE_INDEX_CONSTRAINT_EQ) {
            continue;
        }

        switch (iter->iColumn) {
            case FSTAT_COL_PATTERN:
                viu.column_used(iter);
                break;
        }
    }

    viu.allocate_args(FSTAT_COL_PATTERN, FSTAT_COL_PATTERN, 1);
    return SQLITE_OK;
}

int
rcFilter(sqlite3_vtab_cursor* pVtabCursor,
         int idxNum,
         const char* idxStr,
         int argc,
         sqlite3_value** argv)
{
    auto* pCur = (fstat_table::cursor*) pVtabCursor;

    if (argc != 1) {
        pCur->c_pattern.clear();
        return SQLITE_OK;
    }

    const char* pattern = (const char*) sqlite3_value_text(argv[0]);
    pCur->c_pattern = pattern;
    pCur->c_path_index = 0;
    pCur->c_error.clear();

    auto glob_flags = GLOB_ERR;
    if (!lnav::filesystem::is_glob(pCur->c_pattern)) {
        glob_flags |= GLOB_NOCHECK;
    }

#ifdef GLOB_TILDE
    glob_flags |= GLOB_TILDE;
#endif

    log_debug("doing glob %s", pattern);
#if defined(__MSYS__)
    auto win_path = lnav::filesystem::escape_glob_for_win(pattern);
    switch (glob(win_path.c_str(), glob_flags, nullptr, pCur->c_glob.inout())) {
#else
    switch (glob(pattern, glob_flags, nullptr, pCur->c_glob.inout())) {
#endif
        case GLOB_NOSPACE:
            pVtabCursor->pVtab->zErrMsg
                = sqlite3_mprintf("No space to perform glob()");
            return SQLITE_ERROR;
        case GLOB_NOMATCH:
            return SQLITE_OK;
    }

    pCur->load_stat();

    return SQLITE_OK;
}

}  // namespace

int
register_fstat_vtab(sqlite3* db)
{
    static vtab_module<tvt_no_update<fstat_table>> FSTAT_MODULE;
    static auto fstat_help
        = help_text("fstat",
                    "A table-valued function for getting information about "
                    "file paths/globs")
              .sql_table_valued_function()
              .with_parameter(
                  {"pattern", "The file path or glob pattern to query."})
              .with_result(
                  {"st_parent", "The parent path of the directory entry"})
              .with_result({"st_name", "The name of the directory entry"})
              .with_result({"st_dev", "The device number"})
              .with_result({"st_ino", "The inode number"})
              .with_result(help_text{"st_type", "The type of the entry"}
                               .with_enum_values({
                                   "reg"_frag,
                                   "blk"_frag,
                                   "chr"_frag,
                                   "dir"_frag,
                                   "fifo"_frag,
                                   "lnk"_frag,
                                   "sock"_frag,
                               }))
              .with_result({"st_mode", "The protection mode"})
              .with_result(
                  {"st_nlink", "The number of hard links to the entry"})
              .with_result({"st_uid", "The ID of the owning user"})
              .with_result({"st_user", "The user name"})
              .with_result({"st_gid", "The ID of the owning group"})
              .with_result({"st_group", "The group name"})
              .with_result({"st_rdev", "The device type"})
              .with_result({"st_size", "The size of the entry in bytes"})
              .with_result({"st_blksize", "The optimal size for I/O"})
              .with_result({"st_blocks", "Blocks allocated for the file"})
              .with_result({"st_atime", "The last access time"})
              .with_result({"st_mtime", "The last modified time"})
              .with_result({"st_ctime", "The creation time"})
              .with_result({"error",
                            "Error message if there was a problem looking up "
                            "the entry"})
              .with_result({"data", "The contents of the file"})
              .with_example(help_example{
                  "To read a file and raise an error if there is a problem",
                  "SELECT ifnull(data, raise_error('cannot read: ' || st_name, "
                  "error)) FROM fstat('/non-existent')",
              });

    int rc;

    FSTAT_MODULE.vm_module.xBestIndex = rcBestIndex;
    FSTAT_MODULE.vm_module.xFilter = rcFilter;

    static auto& lnav_flags = injector::get<unsigned long&, lnav_flags_tag>();

    if (lnav_flags & LNF_SECURE_MODE) {
        return SQLITE_OK;
    }

    rc = FSTAT_MODULE.create(db, "fstat");
    sqlite_function_help.emplace("fstat", &fstat_help);
    fstat_help.index_tags();

    ensure(rc == SQLITE_OK);

    return rc;
}
