const gitclient = require("nodegit"),
      toString = require('stream-to-string'),
      fs = require('fs'),
      Readable = require('stream').Readable,
      Path = require('path'),
      crypto = require("crypto"),
      BASE_PATH = "/tmp/";

let repos = {};
setInterval(autoVacuum, 1000*60*60); // autovacuum every hour

module.exports = {
    test: function(params){
        if(!params || !params.repo){ return Promise.reject({message: 'invalid authentication', code: 'INVALID_PARAMS'}) };
        if(!params.commit) params.commit = "{action} ({filename}): {path}";
        if(!params.branch) params.branch = 'master';
        if(!params.author_name) params.author_name = "Nuage";
        if(!params.author_email) params.author_email = "https://nuage.kerjean.me";
        if(!params.committer_name) params.committer_name = "Nuage";
        if(!params.committer_email) params.committer_email = "https://nuage.kerjean.me";

        if(params.password && params.password.length > 2700){
            return Promise.reject({message: "Your password couldn\'t fit in a cookie :/", code: "COOKIE_ERROR"})
        }
        return git.open(params)
            .then(() => Promise.resolve(params));
    },
    cat: function(path, params){
        return git.open(params)
            .then((repo) => git.refresh(repo, params))
            .then(() => file.cat(calculate_path(params, path)));
    },
    ls: function(path, params){
        return git.open(params)
            .then((repo) => git.refresh(repo, params))
            .then(() => file.ls(calculate_path(params, path)))
            .then((files) => files.filter((file) => (file.name === '.git' && file.type === 'directory') ? false: true));
    },
    write: function(path, content, params){
        return git.open(params)
            .then(() => file.write(calculate_path(params, path), content))
            .then(() => git.save(params, path, "write"));
    },
    rm: function(path, params){
        return git.open(params)
            .then(() => file.rm(calculate_path(params, path)))
            .then(() => git.save(params, path, "delete"));
    },
    mv: function(from, to, params){
        return git.open(params)
            .then(() => file.mv(calculate_path(params, from), calculate_path(params, to)))
            .then(() => git.save(params, to, 'move'));
    },
    mkdir: function(path, params){
        return git.open(params)
            .then(() => file.mkdir(calculate_path(params, path)))
            .then(() => git.save(params, path, "create"))
    },
    touch: function(path, params){
        var stream = new Readable(); stream.push(''); stream.push(null);
        return git.open(params)
            .then(() => file.write(calculate_path(params, path), stream))
            .then(() => git.save(params, path, 'create'));
    }
};

function autoVacuum(){
    file.ls(BASE_PATH).then((files) => {
        files.map((_file) => {
            const filename = _file.name,
                  full_path = BASE_PATH + filename;

            if(wasCreatedByTheGitBackend(full_path) === false) return;

            if(repos[full_path] === undefined){
                // remove stuff that was created in a previous session
                // => happen on server restart
                remove(full_path);
            }

            // clean up after 5 hours without activity in the repo
            const MAXIMUM_DATE_BEFORE_CLEAN = repos[full_path] + 1000*60*60*5;
            if(new Date().getTime() > MAXIMUM_DATE_BEFORE_CLEAN){
                remove(full_path);
                delete repos[full_path];
            }
        });
    });

    function remove(path){
        return file.rm(path).catch((err) => {
            console.warn("WARNING: vacuum", err);
        });
    }

    function wasCreatedByTheGitBackend(name){
        return name.indexOf(BASE_PATH+"git_") === 0;
    }
}

function calculate_path(params, path){
    const repo = path_repo(params);
    const full_path = Path.posix.join(repo, path);
    if(full_path.indexOf(BASE_PATH) !== 0 || full_path === BASE_PATH){
        return BASE_PATH+"error";
    }
    return full_path;
}

function path_repo(obj){
    let hash = crypto.createHash('md5');
    for(let key in obj){
        if(typeof obj[key] === 'string'){
            hash.update(obj[key]);
        }
    }
    const path = BASE_PATH+"git_"+obj.uid+"_"+obj.repo.replace(/[^a-zA-Z]/g, "")+"_"+hash.digest('hex');
    repos[path] = new Date().getTime();
    return path;
}

const file = {};
file.write = function (path, stream){
    return new Promise((done, err) => {
        let writer = fs.createWriteStream(path, { flags : 'w' });
        stream.pipe(writer);
        writer.on('close', function(){
            done('ok');
        });
        writer.on('error', function(error){
            err(error);
        });
    });
};
file.mkdir = function(path){
    return new Promise((done, err) => {
        fs.mkdir(path, function(error){
            if(error){ return err(error); }
            return done("ok");
        });
    });
}
file.mv = function(from, to){
    return new Promise((done, err) => {
        fs.rename(from, to, function(error){
            if(error){ return err(error); }
            return done("ok");
        });
    });
}
file.ls = function(path){
    return new Promise((done, err) => {
        fs.readdir(path, (error, files) => {
            if(error){ return err(error); }
            Promise.all(files.map((file) => {
                return stats(path+file).then((stat) => {
                    stat.name = file;
                    return Promise.resolve(stat);
                });
            })).then((files) => {
                done(files.map((file) => {
                    return {
                        size: file.size,
                        time: new Date(file.mtime).getTime(),
                        name: file.name,
                        type: file.isFile()? 'file' : 'directory'
                    };
                }));
            }).catch((error) => err(error));
        });
    });

    function stats(path){
        return new Promise((done, err) => {
            fs.stat(path, function(error, res){
                if(error) return err(error);
                return done(res);
            });
        });
    }
}
file.rm = function(path){
    return rm(path);

    function rm(path){
        return stat(path).then((_stat) => {
            if(_stat.isDirectory()){
                return ls(path)
                    .then((files) => Promise.all(files.map(file => rm(path+"/"+file))))
                    .then(() => removeEmptyFolder(path));
            }else{
                return removeFileOrLink(path);
            }
        });
    }

    function removeEmptyFolder(path){
        return new Promise((done, err) => {
            fs.rmdir(path, function(error){
                if(error){ return err(error); }
                return done("ok");
            });
        });
    }
    function removeFileOrLink(path){
        return new Promise((done, err) => {
            fs.unlink(path, function(error){
                if(error){ return err(error); }
                return done("ok");
            });
        });
    }
    function ls(path){
        return new Promise((done, err) => {
            fs.readdir(path, function (error, files) {
                if(error) return err(error)
                return done(files)
            });
        });
    }
    function stat(path){
        return new Promise((done, err) => {
            fs.stat(path, function (error, _stat) {
                if(error){ return err(error); }
                return done(_stat);
            });
        });
    }
}

file.cat = function(path){
    return Promise.resolve(fs.createReadStream(path));
}


const git = {};
git.open = function(params){
    count = 0;
    return gitclient.Repository.open(path_repo(params))
        .catch((err) => {
            return gitclient.Clone(params.repo, path_repo(params), {fetchOpts: { callbacks: { credentials: git_creds.bind(null, params) }}})
                .then((repo) => {
                    const branch = params.branch;
                    return repo.getBranchCommit("origin/"+branch)
                        .catch(() => repo.getHeadCommit("origin"))
                        .then((commit) => {
                            return repo.createBranch(branch, commit)
                                .then(() => repo.checkoutBranch(branch))
                                .then(() => Promise.resolve(repo));
                        })
                        .catch(() => Promise.resolve(repo));
                });
        });
};

git.refresh = function(repo, params){
    count = 0;
    return repo.fetchAll({callbacks: { credentials: git_creds.bind(null, params) }})
        .then(() => repo.mergeBranches(params.branch, "origin/"+params.branch, gitclient.Signature.default(repo), 2))
        .catch(err => {
            if(err.errno === -13){
                return git.save(params, '', 'merge')
                    .then(() => git.refresh(repo, params))
                    .then(() => Promise.resolve(repo));
            }
            return Promise.resolve(repo);
        });
};

git.save = function(params, path = '', type = ''){
    count = 0;
    const author = gitclient.Signature.now(params.author_name, params.author_email);
    const committer = gitclient.Signature.now(params.committer_name, params.committer_email);
    const message = params.commit
          .replace("{action}", type)
          .replace("{dirname}", Path.posix.dirname(path))
          .replace("{filename}", Path.posix.basename(path))
          .replace("{path}", path || '');

    return git.open(params)
        .then((repo) => Promise.all([
            Promise.resolve(repo),
            getParent(repo, params),
            refresh(repo, params)
        ]))
        .then((data) => {
            const [repo, commit, oid] = data;
            const parents = commit ? [commit] : [];
            return repo.createCommit("HEAD", author, committer, message, oid, parents)
                .then(() => Promise.resolve(repo));
        })
        .then((repo) => {
            return repo.getRemote("origin")
                .then((remote) => {
                    return remote.push(
                        ["refs/heads/"+params.branch+":refs/heads/"+params.branch],
                        { callbacks: { credentials: git_creds.bind(null, params, true) }}
                    );
                })
                .catch((err) => Promise.reject({status: 403, message: "Not authorized to push"}));
        });

    function getParent(repo, params){
        return repo.getBranchCommit(params.branch)
            .catch(() => {
                return repo.getHeadCommit()
                    .catch(() => Promise.resolve(null));
            });
    }
    function refresh(repo, params){
        return repo.refreshIndex()
            .then((index) => {
                return index.addAll()
                    .then(() => index.write())
                    .then(() => index.writeTree());
            });
    }
};





// the count thinghy is used to see if the request succeeded or not
// when something fail, nodegit would just run the callback again and again.
// The only way to make it throw an error is to return the defaultNew thinghy
let count = 0;
function git_creds(params, fn, _count){
    count += 1;

    if(count > 1 && _count !== undefined){
        return new gitclient.Cred.defaultNew();
    }else if(/http[s]?\:\/\//.test(params.repo)){
        return new gitclient.Cred.userpassPlaintextNew(params.username, params.password);
    }else{
        return new gitclient.Cred.sshKeyMemoryNew(params.username, "", params.password, params.passphrase || "")
    }
}
