// deployer main programm
// Copyright (C) 2024 Jean-Cloud
// GNU General Public License v3

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <filesystem>
#include <fstream>
#include "Services.h"
#include "Modules.h"
#include "BashManager.h"
using namespace std;
namespace fs=filesystem;

void help(char * argv0)
{
    //temporary 
    cout << "usage: ./" << argv0 <<"action file \n with action=deploy or remove and file=path to a file or all. \n" << endl;
}



int createUser(string serviceUsername)
{
    //this method creates a Unix user dedicated to the service
    //get the User ID from servers.csv
    int uidStart=stoi(getenv("SERVICES_UID_START")); //so that the uids do not overlap with existing uids
    Services services = Services();
    const Service * service = services.FindByUsername(serviceUsername);
    int uid = (*service).GetUserID()+uidStart;
    cout << "uid vaut" << uid << endl;
    //test if user already exists
    string cmd = "id -u "+serviceUsername; 
    string res = BashManager::ExecuteAndReadResult (cmd);
    if(res==to_string(uid)){
        cout << "user already existed" << endl;
        return 0;
    }
    //create user
    string cmd2 ="useradd -u " + to_string(uid) + " " + serviceUsername + " && usermod -s /sbin/nologin "+ serviceUsername; //no direct login 
    
    string res2 = BashManager::ExecuteAndReadResult(cmd2);
    if (res2 != ""){
        cerr << "Error when executing the bash command to create a user specific to the service." << endl;
        cerr << res2 << endl;
        return -1;
    }
    cout << "user created " << endl;
    return 0;
}

int deleteUser(string serviceUsername)
{
    //this method deletes the Unix user dedicated to the service
    //get the User ID from servers.csv
    int uidStart=stoi(getenv("SERVICES_UID_START"));
    Services services = Services();
    const Service * service = services.FindByUsername(serviceUsername);
    int uid = (*service).GetUserID()+uidStart;
    //test if user exists
    string cmd = "id -u "+serviceUsername; 
    string res = BashManager::ExecuteAndReadResult (cmd);
    if(res!=to_string(uid)){
        cout << "no user to delete" << endl;
        return 0;
    }
    //delete user
    string cmd2 ="deluser --delete-home " + serviceUsername;
    string res2 = BashManager::ExecuteAndReadResult(cmd2);
    if (res2.find("Done") == std::string::npos){
        cerr << "Error when executing the bash command to delete the user specific to the service." << endl;
        cerr << res2 << endl;
        return -1;
    }
    cout << "user deleted" << endl;
    return 0;
}

string findCertificate(string serviceUsername)
{
    //this method searches for a specific ssl certificate for the service, either in dns or http directories
    //it would be under a serviceUsername* folder and named fullchain.perm
    //if none is found, it returns the dummy certificate

    //searching is dns_certs_path
    string dns_certs_path=getenv("DNS_CERTS_PATH"); //dns_certs_path is an environment variable
    //finding the serviceUsername* directory
    string cmd="ls $dns_certs_path/"+serviceUsername+" | grep '^"+serviceUsername+"\\(-[0-9]\\{4\\}\\)\\?$'";
    string name = BashManager::ExecuteAndReadResult(cmd);
    cout << "result 1: " << name << endl;
    if (!name.empty()){
        //finding the certificate
        string cert = dns_certs_path+"/"+name+"/fullchain.pem";
        if (fs::exists(cert)){
            return cert;
        } else {
            cout << "No certificate in " << dns_certs_path << endl;
        }
    }
    //searching in http_certs_path
    string http_certs_path=getenv(("HTTP_CERTS_PATH")); //http_certs_path is an environment variable
    //finding the serviceUsername* directory
    cmd="ls $http_certs_path/"+serviceUsername+" | grep '^"+serviceUsername+"\\(-[0-9]\\{4\\}\\)\\?$'";
    name = BashManager::ExecuteAndReadResult(cmd);
    cout << "result 2: " << name << endl;
    if (!name.empty()){    
        //finding the certificate 
        string cert = http_certs_path+"/"+name+"/fullchain.pem";
        if (fs::exists(cert)){
            return cert;
        } else {
            cout << "No certificate in " << http_certs_path << endl;
        }
    } 
    cout << "Using dummy certificate" << endl;
    return getenv("DUMMY_CERT_PATH");
}

int createEnvService(string serviceUsername)
{
    //this method creates the environment variables of the service as well as its directories 
    Services services;
    //environment variables creation
    string http_dir="/srv/http/"+serviceUsername;
    string data_dir="/data/"+serviceUsername;
    string secret_dir="/data/secrets/"+serviceUsername;
    string docker_dir="/services/"+serviceUsername;
    string jc_service=serviceUsername;
    string home="/data/"+serviceUsername;
    string jc_id=to_string(services.FindByUsername(serviceUsername)->GetUserID()+stoi(getenv("SERVICES_UID_START")));
    string net="172.29."+jc_id;
    string jc_cert=findCertificate(serviceUsername);

    
    //create a .env file accessible outside the C++ program
    string file = "./services/"+serviceUsername+"/.env";
    ofstream outfile(file);
    outfile << "http_dir=" << http_dir << endl;
    outfile << "data_dir=" << data_dir << endl;
    outfile << "secret_dir=" << secret_dir << endl;
    outfile << "docker_dir=" << docker_dir << endl;
    outfile << "jc_service=" << jc_service << endl;
    outfile << "home=" << home << endl;
    outfile << "jc_id=" << jc_id << endl;
    outfile << "net=" << net << endl;
    outfile << "jc_cert=" << jc_cert << endl;
    outfile.close();

    //setting the environment variables for all the shell commands called in this C++ programm
    setenv("HTTP_DIR",http_dir.c_str(),1);
    setenv("DATA_DIR",data_dir.c_str(),1);
    setenv("SECRET_DIR",secret_dir.c_str(),1);
    setenv("DOCKER_DIR",docker_dir.c_str(),1);
    setenv("JC_SERVICE",jc_service.c_str(),1);
    setenv("HOME",home.c_str(),1);
    setenv("JC_ID",jc_id.c_str(),1);
    setenv("NET",net.c_str(),1);
    setenv("JC_CERT",jc_cert.c_str(),1);

    //create the directories
    //data_dir
    fs::create_directories(data_dir);
    string cmd="chown "+ jc_id +" "+data_dir;
    BashManager::Execute(cmd); 
    fs::permissions(data_dir,fs::perms::owner_all|fs::perms::group_read|fs::perms::group_exec|fs::perms::others_exec,fs::perm_options::replace);
    //http_dir
    fs::create_directories(http_dir);
    cmd="chown "+ jc_id +":www-data -R "+http_dir;
    BashManager::Execute(cmd); 
    //secret_dir
    fs::create_directories(secret_dir);
    if (chown(secret_dir.c_str(), (unsigned int)stoi(jc_id),(unsigned int)stoi(jc_id)) != 0) {
        cerr << "Error changing ownership of" << secret_dir << endl;
        return -1;
    }
    fs::permissions(secret_dir,fs::perms::owner_all|fs::perms::group_read|fs::perms::group_exec|fs::perms::others_exec,fs::perm_options::replace);  
    cout << "service " << serviceUsername << " environment created" << endl;
    return 0;
}

int removeEnvService()
{
    string http_dir=getenv("HTTP_DIR");
    if(fs::exists(http_dir)){
        fs::remove_all(http_dir);
    }
    cout << "http directory of service deleted" << endl;
    return 0;
}

int createEnv()
{
    //this method writes environment variables in a file and sets them for every bash call
    string proxy_dir="/etc/nginx";
    string nginx_conf_path=proxy_dir+"/sites-enabled";
    string new_nginx_conf_path=proxy_dir+"/new-sites-enabled";
    string dns_certs_path="/data/dnscerts.jean-cloud.org/certs/live";
    string http_certs_path="/etc/letsencrypt/live";
    string dummy_cert_path=http_certs_path+"/dummy";
    string servicefile="./services/services.csv";
    string services_uid_start="2000";

    //create a file accessible outside the C++ program
    ofstream outfile ("/etc/jeancloud.env");
    outfile << "proxy_dir=" << proxy_dir << endl;
    outfile << "nginx_conf_path=" << nginx_conf_path << endl;
    outfile << "new_nginx_conf_path=" << new_nginx_conf_path << endl; 
    outfile << "dns_certs_path=" << dns_certs_path << endl;
    outfile << "http_certs_path=" << http_certs_path << endl;
    outfile << "dummy_cert_path=" << dummy_cert_path << endl;
    outfile << "servicefile=" << servicefile << endl;
    outfile << "services_uid_start=" << services_uid_start << endl;
    outfile.close();

    //setting the environment variables for all the shell commands called in this C++ programm
    setenv("PROXY_DIR",proxy_dir.c_str(),1);
    setenv("NGINX_CONF_PATH",nginx_conf_path.c_str(),1);
    setenv("NEW_NGINX_CONF_PATH",new_nginx_conf_path.c_str(),1);
    setenv("DNS_CERTS_PATH",dns_certs_path.c_str(),1);
    setenv("HTTP_CERTS_PATH",http_certs_path.c_str(),1);
    setenv("DUMMY_CERT_PATH",dummy_cert_path.c_str(),1);
    setenv("SERVICEFILE",servicefile.c_str(),1);
    setenv("SERVICES_UID_START",services_uid_start.c_str(),1);
    cout << "global environment created" << endl;
    return 0;
}

int deployService(string serviceUsername){
    //this method deploys indicated service if it is on this server
    if (isServiceOnServer(serviceUsername)==0){
        //bash user creation
        if(int userCreated = createUser(serviceUsername);userCreated!=0){
            return -1;
        }
        //environment variables creation
        if(int envCreated = createEnvService(serviceUsername);envCreated!=0){
            return -1; 
        }
        //call to the deploy functionality of all modules
        //the modules themselves determine their course of action depending on the service
        
        for(Module * mod_ptr : modules){
            int modResult = (*mod_ptr).Deploy(serviceUsername);

            if (modResult!=0){
                //cerr << "Error in " << (*mod_ptr) << " when deploying " << serviceUsername << endl;
                cerr << "Error in module " << endl;
            }
        }
        return 0;
    }
    return 0; 
}

int deployAll()
{
    //this method deploys all the services that are on this server
    cout << "deploying all" <<endl;
    createEnv();
    //initializing modules
    for(Module * mod_ptr : modules){
        cout << *mod_ptr << endl;
        int modPrep = (*mod_ptr).Prepare();
        int modClean = (*mod_ptr).Clean();
        if ((modPrep && modClean) !=0){
            cerr << "Error in " << (*mod_ptr) << " when preparing and cleaning." << endl;
        }
    }
    Services services = Services();
    for (Service service : services.GetServices()){
        deployService(service.GetUsername());
    }
    return 0;
}

int removeAll()
{
    cout << "removing all"<<endl;
    Services services = Services();
    for (Service service : services.GetServices()){
        deployService(service.GetUsername());
    }
    for(Module * mod_ptr : modules){
        int modClean = (*mod_ptr).Clean();
        if ((modClean) !=0){
            cerr << "Error in " << (*mod_ptr) << " when preparing and cleaning." << endl;
        }
    }
    return 0;
}

int removeService(string serviceUsername)
{
    cout<< "removing service"<<endl;
    if (isServiceOnServer(serviceUsername)==0){
        for(Module * mod_ptr : modules){
            int modResult = (*mod_ptr).Remove(serviceUsername);
            if (modResult!=0){
                cerr << "Error in " << (*mod_ptr) << " when removing " << serviceUsername << endl;
            }
        }
        removeEnvService();
    }

    return 0;
}

int main(int argc, char *argv[])
{
    //check number of arguments
    if (argc != 3){
        cerr << "Invalid number of arguments. \n" << endl;
        help(argv[0]);
        return -1;
    } else {
        //check that data is mounted on the server
        if (!(fs::exists("/data/mounted"))) {
            cerr << "Error. The data is not mounted on the server" << endl;
            return -1;
        } else {
            //read instructions
            string action = argv[1];
            string service = argv[2];
            if (action=="deploy"){
                if(service=="all"){
                    deployAll();
                } else {
                    deployService(service);
                }
            } else if (action=="remove"){
                if(service=="all"){
                    removeAll();
                } else {
                    removeService(service);
                }
            } else {
            cerr << "Invalid argument. \n" << endl;
            return -1;
            }
        }
    }
    return 0;
}


