/*
 * parse_module.c
 *
 * Copyright (c) 2018-2022 Eric Vidal <eric@obarun.org>
 *
 * All rights reserved.
 *
 * This file is part of Obarun. It is subject to the license terms in
 * the LICENSE file found in the top-level directory of this
 * distribution.
 * This file may not be copied, modified, propagated, or distributed
 * except according to the terms contained in the LICENSE file./
 */

#include <string.h>
#include <errno.h>
#include <pwd.h>
#include <sys/types.h>
#include <unistd.h> //chdir, unlink
#include <stdlib.h>
#include <stdio.h> //rename

#include <oblibs/string.h>
#include <oblibs/log.h>
#include <oblibs/mill.h>
#include <oblibs/sastr.h>
#include <oblibs/types.h>
#include <oblibs/directory.h>
#include <oblibs/environ.h>
#include <oblibs/files.h>

#include <skalibs/stralloc.h>
#include <skalibs/djbunix.h> //hiercopy
#include <skalibs/env.h> //hiercopy
#include <skalibs/bytestr.h>

#include <66/write.h>
#include <66/utils.h>
#include <66/environ.h>
#include <66/resolve.h>
#include <66/service.h>
#include <66/constants.h>
#include <66/utils.h>

#define SS_MODULE_CONFIG_DIR "/configure"
#define SS_MODULE_CONFIG_DIR_LEN (sizeof SS_MODULE_CONFIG_DIR - 1)
#define SS_MODULE_CONFIG_SCRIPT "configure"
#define SS_MODULE_CONFIG_SCRIPT_LEN (sizeof SS_MODULE_CONFIG_SCRIPT - 1)
#define SS_MODULE_SERVICE "/service"
#define SS_MODULE_SERVICE_LEN (sizeof SS_MODULE_SERVICE - 1)
#define SS_MODULE_SERVICE_INSTANCE "/service@"
#define SS_MODULE_SERVICE_INSTANCE_LEN (sizeof SS_MODULE_SERVICE_INSTANCE - 1)

static void instance_splitname_to_char(char *store, char const *name, int len, int what)
{
    log_flow() ;

    char const *copy ;
    size_t tlen = len + 1, clen = 0 ;

    char template[tlen + 1] ;
    memcpy(template,name,tlen) ;
    template[tlen] = 0 ;

    copy = name + tlen ;

    if (!what) {

        auto_strings(store, template) ;

    } else {

        clen = strlen(copy) ;
        memcpy(store, copy, clen) ;
        store[clen] = 0 ;
    }

}

static void parse_module_check_dir(char const *src,char const *dir)
{
    log_flow() ;

    int r ;
    size_t srclen = strlen(src) ;
    size_t dirlen = strlen(dir) ;

    char t[srclen + dirlen + 1] ;
    auto_strings(t, src, dir) ;

    r = scan_mode(t,S_IFDIR) ;
    if (r < 0) {
        errno = EEXIST ;
        log_diesys(LOG_EXIT_ZERO, "conflicting format of: ", t) ;
    }

    if (!r)
        if (!dir_create_parent(t, 0755))
            log_dieusys(LOG_EXIT_ZERO, "create directory: ", t) ;
}

static void parse_module_check_name(char const *src, char const *name)
{
    log_flow() ;

    char basename[strlen(src)] ;
    int insta = -1 ;

    if (!ob_basename(basename, src))
        log_dieu(LOG_EXIT_SYS, "get basename of: ", src) ;

    insta = instance_check(name) ;

    if (insta <= 0)
        log_die(LOG_EXIT_SYS, "invalid module instance name: ", name) ;

    if (basename[0] != '@')
        log_die(LOG_EXIT_USER, "invalid directory name for module: ", name, " -- directory name must start with a '@' character") ;
}

/* 0 filename undefine
 * -1 system error
 * should return at least 2 meaning :: no file define*/
static int regex_get_file_name(char *filename, char const *str)
{
    log_flow() ;

    int r ;
    size_t pos = 0 ;
    stralloc kp = STRALLOC_ZERO ;

    parse_mill_t MILL_GET_COLON = {
    .open = ':', .close = ':',
    .skip = " \t\r", .skiplen = 3,
    .forceclose = 1,
    .inner.debug = "get_colon" } ;

    r = mill_element(&kp, str, &MILL_GET_COLON, &pos) ;
    if (r == -1)
        log_dieu(LOG_EXIT_SYS, "get filename of line: ", str) ;

    auto_strings(filename, kp.s) ;

    stralloc_free(&kp) ;
    return pos ;
}

static void regex_get_replace(char *replace, char const *str)
{
    log_flow() ;

    int pos = get_len_until(str,'=') ;
    if (!pos || pos == -1)
       log_dieu(LOG_EXIT_SYS,"replace string of line: ", str) ;
    char tmp[pos + 1] ;
    memcpy(tmp,str,pos) ;
    tmp[pos] = 0 ;
    auto_strings(replace, tmp) ;
}

static void regex_get_regex(char *regex, char const *str)
{
    log_flow() ;

    size_t len = strlen(str) ;
    int pos = get_len_until(str,'=') ;
    if (!pos || pos == -1)
        log_dieu(LOG_EXIT_SYS, "get regex string of line: ", str) ;
    pos++ ; // remove '='
    char tmp[len + 1] ;
    memcpy(tmp,str + pos,len-pos) ;
    tmp[len-pos] = 0 ;
    auto_strings(regex,tmp) ;
}

static void get_list(stralloc *list, char const *src, char const *name, mode_t mode)
{
    log_flow() ;

    list->len = 0 ;
    char const *exclude[2] = { SS_MODULE_CONFIG_DIR + 1, 0 } ;

    char t[strlen(src) + 1] ;

    auto_strings(t, src) ;

    if (!sastr_dir_get_recursive(list, t, exclude, mode, 1))
        log_dieusys(LOG_EXIT_SYS,"get file(s) of module: ", name) ;

}

static void regex_replace(stralloc *list, resolve_service_t *res)
{
    log_flow() ;

    int r ;
    size_t pos = 0, idx = 0 ;

    stralloc frontend = STRALLOC_ZERO ;
    stralloc sa = STRALLOC_ZERO ;

    if (!res->regex.ninfiles)
        return ;

    if (!sastr_clean_string(&sa, res->sa.s + res->regex.infiles))
        log_dieu(LOG_EXIT_SYS, "clean string") ;

    FOREACH_SASTR(list, pos) {

        frontend.len = idx = 0 ;
        char *str = list->s + pos ;
        size_t len = strlen(str) ;
        char bname[len + 1] ;
        char dname[len + 1] ;

        if (!ob_basename(bname, str))
            log_dieu(LOG_EXIT_SYS, "get basename of: ", str) ;

        if (!ob_dirname(dname, str))
            log_dieu(LOG_EXIT_SYS, "get dirname of: ", str) ;

        //log_trace("read service file of: ", dname, bname) ;
        r = read_svfile(&frontend, bname, dname) ;
        if (!r)
            log_dieusys(LOG_EXIT_SYS, "read file: ", str) ;
        else if (r == -1)
            continue ;

        {
            FOREACH_SASTR(&sa, idx) {

                int all = 0, fpos = 0 ;
                char const *line = sa.s + idx ;
                size_t linelen = strlen(line) ;
                char filename[SS_MAX_SERVICE_NAME + 1] ;
                char replace[linelen + 1] ;
                char regex[linelen + 1] ;

                if (linelen >= SS_MAX_PATH_LEN)
                    log_die(LOG_EXIT_SYS, "limit exceeded in service: ", res->sa.s + res->name) ;

                if ((line[0] != ':') || (get_sep_before(line + 1, ':', '=') < 0))
                    log_die(LOG_EXIT_SYS, "bad format in line: ", line, " of key @infiles field") ;

                memset(filename, 0, SS_MAX_SERVICE_NAME + 1) ;
                memset(replace, 0, linelen + 1) ;
                memset(regex, 0, linelen + 1) ;

                fpos = regex_get_file_name(filename, line) ;
                if (fpos < 3) all = 1 ;

                regex_get_replace(replace, line + fpos) ;

                regex_get_regex(regex, line + fpos) ;

                if (!strcmp(bname, filename) || all) {

                    if (!sastr_replace_all(&frontend, replace, regex))
                        log_dieu(LOG_EXIT_SYS, "replace: ", replace, " by: ", regex, " in file: ", str) ;

                    if (!stralloc_0(&frontend))
                        log_dieusys(LOG_EXIT_SYS, "stralloc") ;

                    frontend.len-- ;

                    if (!file_write_unsafe(dname, bname, frontend.s, frontend.len))
                        log_dieusys(LOG_EXIT_SYS, "write: ", dname, "/", bname) ;
                }
            }
        }
    }

    stralloc_free(&frontend) ;
    stralloc_free(&sa) ;
}

static void regex_rename(stralloc *list, resolve_service_t *res, uint32_t element)
{
    log_flow() ;

    stralloc sa = STRALLOC_ZERO ;

    if (!element)
        return ;

    if (!sastr_clean_string(&sa, res->sa.s + element))
        log_dieu(LOG_EXIT_SYS, "clean string") ;

    size_t pos = 0, idx = 0, salen = sa.len ;
    char t[sa.len] ;

    sastr_to_char(t, &sa) ;

    for (; pos < salen ; pos += strlen(t + pos) + 1) {

        idx = 0 ;
        char *line = t + pos ;
        char replace[SS_MAX_PATH] = { 0 } ;
        char regex[SS_MAX_PATH] = { 0 } ;

        regex_get_replace(replace,line) ;

        regex_get_regex(regex,line) ;

        FOREACH_SASTR(list, idx) {

            sa.len = 0 ;
            char *str = list->s + idx ;
            size_t len = strlen(str) ;
            char dname[len + 1] ;

            if (!ob_dirname(dname, str))
                log_dieu(LOG_EXIT_SYS, "get dirname of: ", str) ;

            if (!sabasename(&sa, str, len))
                log_dieu(LOG_EXIT_SYS, "get basename of: ", str) ;

            if (!stralloc_0(&sa))
                log_die_nomem("stralloc") ;

            if (!sastr_replace(&sa, replace, regex))
                log_dieu(LOG_EXIT_SYS, "replace: ", replace, " by: ", regex, " in file: ", str) ;

            if (!stralloc_0(&sa))
                log_die_nomem("stralloc") ;

            char new[len + sa.len + 1] ;
            auto_strings(new, dname, sa.s) ;

            /** do not try to rename the same directory */
            if (strcmp(str, new)) {

                log_trace("rename: ", str, " to: ", new) ;
                if (rename(str, new) == -1)
                    //log_warnusys( "rename: ", str, " to: ", new) ;
                    log_dieusys(LOG_EXIT_SYS, "rename: ", str, " to: ", new) ;

                break ;
            }
        }
    }
    stralloc_free(&sa) ;
}

static void regex_configure(resolve_service_t *res, ssexec_t *info, char const *path, char const *name)
{
    log_flow() ;

    int wstat, r ;
    pid_t pid ;
    size_t clen = res->regex.configure > 0 ? 1 : 0 ;
    size_t pathlen = strlen(path), n ;

    stralloc env = STRALLOC_ZERO ;

    char const *newargv[2 + clen] ;
    unsigned int m = 0 ;

    char pwd[pathlen + 1 + SS_MODULE_CONFIG_DIR_LEN + 1] ;
    auto_strings(pwd, path, "/", SS_MODULE_CONFIG_DIR + 1) ;

    char config_script[pathlen + 1 + SS_MODULE_CONFIG_DIR_LEN + 1 + SS_MODULE_CONFIG_SCRIPT_LEN + 1] ;
    auto_strings(config_script, path, "/", SS_MODULE_CONFIG_DIR + 1, "/", SS_MODULE_CONFIG_SCRIPT) ;

    r = scan_mode(config_script, S_IFREG) ;
    if (r > 0)
    {
        /** export ssexec_t info value on the environment */
        {
            char verbo[UINT_FMT];
            verbo[uid_fmt(verbo, VERBOSITY)] = 0 ;
            if (!auto_stra(&env, \
            "MOD_NAME=", name, "\n", \
            "MOD_BASE=", res->sa.s + res->path.home, "\n", \
            "MOD_LIVE=", res->sa.s + res->live.livedir, "\n", \
            "MOD_SCANDIR=", res->sa.s + res->live.scandir, "\n", \
            "MOD_TREENAME=", res->sa.s + res->treename, "\n", \
            "MOD_OWNER=", res->sa.s + res->ownerstr, "\n", \
            "MOD_COLOR=", info->opt_color ? "1" : "0", "\n", \
            "MOD_VERBOSITY=", verbo, "\n", \
            "MOD_MODULE_DIR=", path, "\n", \
            "MOD_SKEL_DIR=", SS_SKEL_DIR, "\n", \
            "MOD_SERVICE_SYSDIR=", SS_SERVICE_SYSDIR, "\n", \
            "MOD_SERVICE_ADMDIR=", SS_SERVICE_ADMDIR, "\n", \
            "MOD_SERVICE_ADMCONFDIR=", SS_SERVICE_ADMCONFDIR, "\n", \
            "MOD_MODULE_SYSDIR=", SS_MODULE_SYSDIR, "\n", \
            "MOD_MODULE_ADMDIR=", SS_MODULE_ADMDIR, "\n", \
            "MOD_SCRIPT_SYSDIR=", SS_SCRIPT_SYSDIR, "\n", \
            "MOD_USER_DIR=", SS_USER_DIR, "\n", \
            "MOD_SERVICE_USERDIR=", SS_SERVICE_USERDIR, "\n", \
            "MOD_SERVICE_USERCONFDIR=", SS_SERVICE_USERCONFDIR, "\n", \
            "MOD_MODULE_USERDIR=", SS_MODULE_USERDIR, "\n", \
            "MOD_SCRIPT_USERDIR=", SS_SCRIPT_USERDIR, "\n"))
                log_dieu(LOG_EXIT_SYS, "append environment variables") ;
        }

        /** environment is not mandatory */
        if (res->environ.env > 0)
        {
            stralloc oenv = STRALLOC_ZERO ;
            stralloc dst = STRALLOC_ZERO ;
            char name[strlen(res->sa.s + res->name) + 2] ;
            auto_strings(name, ".", res->sa.s + res->name) ;

            if (!env_prepare_for_write(&dst, &oenv, res))
                log_dieu(LOG_EXIT_SYS, "prepare environment") ;

            write_environ(name, oenv.s, dst.s) ;

            /** Reads all files from the directory */
            if (!environ_clean_envfile(&env, dst.s))
                log_dieu(LOG_EXIT_SYS, "read environment") ;

            if (!environ_remove_unexport(&env, &env))
                log_dieu(LOG_EXIT_SYS, "remove exclamation mark from environment variables") ;

            stralloc_free(&oenv) ;
            stralloc_free(&dst) ;
        }

        if (!sastr_split_string_in_nline(&env))
            log_dieu(LOG_EXIT_SYS, "rebuild environment") ;

        n = env_len((const char *const *)environ) + 1 + byte_count(env.s,env.len,'\0') ;
        char const *newenv[n + 1] ;

        if (!env_merge (newenv, n ,(const char *const *)environ,env_len((const char *const *)environ), env.s, env.len))
            log_dieu(LOG_EXIT_SYS, "build environment") ;

        if (chdir(pwd) < 0)
            log_dieu(LOG_EXIT_SYS, "chdir to: ", pwd) ;

        m = 0 ;
        newargv[m++] = config_script ;

        if (res->regex.configure > 0)
            newargv[m++] = res->sa.s + res->regex.configure ;

        newargv[m++] = 0 ;

        log_info("launch script configure of module: ", name) ;

        pid = child_spawn0(newargv[0], newargv, newenv) ;

        if (waitpid_nointr(pid, &wstat, 0) < 0)
            log_dieusys(LOG_EXIT_SYS, "wait for: ", config_script) ;

        if (wstat)
            log_dieu(LOG_EXIT_SYS, "run: ", config_script) ;
    }

    stralloc_free(&env) ;
}

void parse_module(resolve_service_t *res, resolve_service_t *ares, unsigned int *areslen, ssexec_t *info, uint8_t force)
{
    log_flow() ;

    int r, insta = -1 ;
    unsigned int residx = 0 ;
    size_t pos = 0, pathlen = 0 ;
    char *name = res->sa.s + res->name ;
    char *src = res->sa.s + res->path.frontend ;
    char path[SS_MAX_PATH_LEN] ;
    char ainsta[strlen(name)] ;
    stralloc list = STRALLOC_ZERO ;
    resolve_wrapper_t_ref wres = 0 ;

    log_trace("parse module: ", name) ;

    insta = instance_check(name) ;
    instance_splitname_to_char(ainsta, name, insta, 0) ;

    size_t prefixlen = strlen(ainsta) ;
    size_t len = prefixlen + SS_MAX_SERVICE_NAME ;

    char prefix[len + 1] ;
    auto_strings(prefix, ainsta) ;
    //prefix[insta] = '-' ;
    prefix[insta] = 0 ;

    if (!getuid()) {

        auto_strings(path, SS_SERVICE_ADMDIR, name) ;

    } else {

        if (!set_ownerhome_stack(path))
            log_dieusys(LOG_EXIT_SYS, "unable to find the home directory of the user") ;

        pathlen = strlen(path) ;
        auto_strings(path + pathlen, SS_SERVICE_USERDIR, name) ;
    }

    uint8_t conf = res->environ.env_overwrite ;

    /** Check the validity of the module directory.
     * The frontend file of the module must be located in
     * a directory with a name in the format @template_name:
     *      - frontend file -> scandir@
     *      - directory name containing the frontend service file -> @scandir */
    parse_module_check_name(src, name) ;

    /** check mandatory directories
     * res->frontend/module_name/{configure,service,service@} */
    parse_module_check_dir(src, SS_MODULE_CONFIG_DIR) ;
    parse_module_check_dir(src, SS_MODULE_SERVICE) ;
    parse_module_check_dir(src, SS_MODULE_SERVICE_INSTANCE) ;

    r = scan_mode(path, S_IFDIR) ;
    if (r == -1) {
        errno = EEXIST ;
        log_dieusys(LOG_EXIT_SYS, "conflicting format of: ", path) ;

    } else if (!r) {

        if (!hiercopy(src, path))
            log_dieusys(LOG_EXIT_SYS, "copy: ", src, " to: ", path) ;

    } else {

        /** Must reconfigure all services of the module */
        if (force < 2) {

            log_warn("skip configuration of the module: ", name, " -- already configured") ;
            goto deps ;
        }

        if (rm_rf(path) < 0)
            log_dieusys (LOG_EXIT_SYS, "remove: ", path) ;

        if (!hiercopy(src, path))
            log_dieusys(LOG_EXIT_SYS,"copy: ", src, " to: ", path) ;
    }

    pathlen = strlen(path) ;
    /** remove the original service frontend file inside the copied directory
     * to avoid double frontend service file for a same service.*/
    auto_strings(path + pathlen, "/", ainsta) ;

    if (unlink(path) < 0)
        log_dieusys(LOG_EXIT_ZERO, "unlink: ", path) ;

    path[pathlen] = 0 ;

    /** contents */
    get_list(&list, path, name, S_IFREG) ;
    regex_replace(&list, res) ;

    /** directories */
    get_list(&list, path, name, S_IFDIR) ;
    regex_rename(&list, res, res->regex.directories) ;

    /** filename */
    get_list(&list, path, name, S_IFREG) ;
    regex_rename(&list, res, res->regex.files) ;

    /** configure script */
    regex_configure(res, info, path, name) ;

    deps:

    list.len = 0 ;

    if (!auto_stra(&list, path))
        log_die_nomem("stralloc") ;

    char t[list.len] ;

    sastr_to_char(t, &list) ;

    list.len = 0 ;
    char const *exclude[3] = { SS_MODULE_CONFIG_DIR + 1, SS_MODULE_SERVICE_INSTANCE + 1, 0 } ;
    if (!sastr_dir_get_recursive(&list, t, exclude, S_IFREG, 1))
        log_dieusys(LOG_EXIT_SYS, "get file(s) of module: ", name) ;

    char l[list.len] ;
    size_t llen = list.len ;

    sastr_to_char(l, &list) ;

    list.len = 0 ;

    for (pos = 0 ; pos < llen ; pos += strlen(l + pos) + 1) {

        char *dname = l + pos ;
        char ainsta[pathlen + SS_MODULE_SERVICE_INSTANCE_LEN + 1 + SS_MAX_SERVICE_NAME + 1] ;
        size_t namelen = strlen(dname) ;
        char bname[namelen] ;

        if (!ob_basename(bname,dname))
            log_dieu(LOG_EXIT_SYS, "find basename of: ", dname) ;

        if (instance_check(bname) > 0) {
            auto_strings(ainsta, path, SS_MODULE_SERVICE_INSTANCE, "/", bname) ;
            dname = ainsta ;
        }

        if (!sastr_add_string(&list, bname))
            log_die_nomem("stralloc") ;

        if (!strcmp(name, bname))
            log_die(LOG_EXIT_SYS, "cyclic call detected -- ", name, " call ", bname) ;

        /** nothing to do with the exit code */
        parse_frontend(dname, ares, areslen, info, force, conf, &residx, path, bname) ;

        wres = resolve_set_struct(DATA_SERVICE, &ares[residx]) ;

        ares[residx].inmodule = resolve_add_string(wres, prefix - 1) ;
    }

    char deps[list.len] ;

    sastr_to_char(deps, &list) ;

    llen = list.len ;

    list.len = 0 ;

    /* rebuild the dependencies list incorporating the services defined inside
     * the module. */

    if (res->dependencies.ndepends)
        if (!sastr_clean_string(&list, res->sa.s + res->dependencies.depends))
            log_dieu(LOG_EXIT_SYS, "clean string") ;

    for (pos = 0 ; pos < llen ; pos += strlen(deps + pos) + 1)
        if (!sastr_add_string(&list, deps + pos))
            log_die_nomem("stralloc") ;

    wres = resolve_set_struct(DATA_SERVICE, res) ;

    res->dependencies.depends = parse_compute_list(wres, &list, &res->dependencies.ndepends, 0) ;

    free(wres) ;
    stralloc_free(&list) ;
}