/**
 * Copyright JS Foundation and other contributors, http://js.foundation
 *
 * 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.
 **/


var fs = require('fs-extra');
var fspath = require("path");
var os = require('os');

var gitTools = require("./git");
var util = require("../util");
var defaultFileSet = require("./defaultFileSet");
var sshKeys = require("./ssh");
var settings;
var runtime;
var log = require("@node-red/util").log;
const events = require("@node-red/util").events;

var projectsDir;

var authCache = require("./git/authCache");

// TODO: DRY - red/api/editor/sshkeys !
function getSSHKeyUsername(userObj) {
    var username = '__default';
    if ( userObj && userObj.username ) {
        username = userObj.username;
    }
    return username;
}

function getUserGitSettings(user) {
    var username;
    if (!user) {
        username = "_";
    } else {
        username = user.username;
    }
    var userSettings = settings.getUserSettings(username)||{};
    return userSettings.git;
}

function getGitUser(user) {
    var gitSettings = getUserGitSettings(user);
    if (gitSettings) {
        return gitSettings.user;
    }
    return null;
}

function Project(path) {
    this.path = path;
    this.name = fspath.basename(path);
    this.paths = {};
    this.files = {};
    this.auth = {origin:{}};
    this.missingFiles = [];
    this.credentialSecret = null;
}
Project.prototype.load = function () {
    var project = this;
    var globalProjectSettings = settings.get("projects");
// console.log(globalProjectSettings)
    var projectSettings = {};
    if (globalProjectSettings) {
        if (globalProjectSettings.projects.hasOwnProperty(this.name)) {
            projectSettings = globalProjectSettings.projects[this.name] || {};
        }
    }
    this.paths.root = projectSettings.rootPath || "";
    this.credentialSecret = projectSettings.credentialSecret;
    this.git = projectSettings.git || { user:{} };

    // this.paths.flowFile = fspath.join(this.path,"flow.json");
    // this.paths.credentialsFile = fspath.join(this.path,"flow_cred.json");

    var promises = [];
    return checkProjectFiles(project).then(function(missingFiles) {
        project.missingFiles = missingFiles;
        if (missingFiles.indexOf('package.json') === -1) {
            // We have a package.json in project.path+project.paths.root+"package.json"
            project.paths['package.json'] = fspath.join(project.paths.root,"package.json");
            promises.push(fs.readFile(fspath.join(project.path,project.paths['package.json']),"utf8").then(function(content) {
                try {
                    project.package = util.parseJSON(content);
                    if (project.package.hasOwnProperty('node-red')) {
                        if (project.package['node-red'].hasOwnProperty('settings')) {
                            project.paths.flowFile = fspath.join(project.paths.root,project.package['node-red'].settings.flowFile);
                            project.paths.credentialsFile = fspath.join(project.paths.root,project.package['node-red'].settings.credentialsFile);
                        }
                    } else {
                        // TODO: package.json doesn't have a node-red section
                        //       is that a bad thing?
                    }
                } catch(err) {
                    // package.json isn't valid JSON... is a merge underway?
                    project.package = {};
                }
            }).catch(err => {})); //
            if (missingFiles.indexOf('README.md') === -1) {
                project.paths['README.md'] = fspath.join(project.paths.root,"README.md");
                promises.push(fs.readFile(fspath.join(project.path,project.paths['README.md']),"utf8").then(function(content) {
                    project.description = content;
                }).catch(err => {}));
            } else {
                project.description = "";
            }
        } else {
            project.package = {};
            project.description = "";
        }

        // if (missingFiles.indexOf('flow.json') !== -1) {
        //     console.log("MISSING FLOW FILE");
        // } else {
        //     project.paths.flowFile = fspath.join(project.path,"flow.json");
        // }
        // if (missingFiles.indexOf('flow_cred.json') !== -1) {
        //     console.log("MISSING CREDS FILE");
        // } else {
        //     project.paths.credentialsFile = fspath.join(project.path,"flow_cred.json");
        // }

        promises.push(project.loadRemotes().catch(err => {}));

        return Promise.all(promises).then(function(results) {
            return project;
        })
    });
};

Project.prototype.initialise = function(user,data) {
    var project = this;
    // if (!this.empty) {
    //     throw new Error("Cannot initialise non-empty project");
    // }
    var files = Object.keys(defaultFileSet);
    var promises = [];

    if (data.hasOwnProperty('credentialSecret')) {
        var projects = settings.get('projects');
        projects.projects[project.name] = projects.projects[project.name] || {};
        projects.projects[project.name].credentialSecret = data.credentialSecret;
        promises.push(settings.set('projects',projects));
    }

    if (data.hasOwnProperty('files')) {
        if (data.files.hasOwnProperty('flow') && data.files.hasOwnProperty('credentials')) {
            project.files.flow = data.files.flow;
            project.files.credentials = data.files.credentials;
            var flowFilePath = fspath.join(project.path,project.files.flow);
            var credsFilePath = getCredentialsFilename(flowFilePath);
            promises.push(util.writeFile(flowFilePath,"[]"));
            promises.push(util.writeFile(credsFilePath,"{}"));
            files.push(project.files.flow);
            files.push(project.files.credentials);
        }
    }
    for (var file in defaultFileSet) {
        if (defaultFileSet.hasOwnProperty(file)) {
            var path = fspath.join(project.path,file);
            if (!fs.existsSync(path)) {
                promises.push(util.writeFile(path,defaultFileSet[file](project)));
            }

        }
    }

    return Promise.all(promises).then(function() {
        return gitTools.stageFile(project.path,files);
    }).then(function() {
        return gitTools.commit(project.path,"Create project files",getGitUser(user));
    }).then(function() {
        return project.load()
    })
}

Project.prototype.loadRemotes = function() {
    var project = this;
    return gitTools.getRemotes(project.path).then(function(remotes) {
        project.remotes = remotes;
    }).then(function() {
        project.branches = {};
        return project.status();
    }).then(function() {
        if (project.remotes) {
            var allRemotes = Object.keys(project.remotes);
            var match = "";
            if (project.branches.remote) {
                allRemotes.forEach(function(remote) {
                    if (project.branches.remote.indexOf(remote) === 0 && match.length < remote.length) {
                        match = remote;
                    }
                });
                project.currentRemote = project.parseRemoteBranch(project.branches.remote).remote;
            }
        } else {
            delete project.currentRemote;
        }
    });
}

Project.prototype.parseRemoteBranch = function (remoteBranch) {
    if (!remoteBranch) {
        return {}
    }
    var project = this;
    var allRemotes = Object.keys(project.remotes);
    var match = "";
    allRemotes.forEach(function(remote) {
        if (remoteBranch.indexOf(remote) === 0 && match.length < remote.length) {
            match = remote;
        }
    });
    return {
        remote: match,
        branch: remoteBranch.substring(match.length+1)
    }

};

Project.prototype.isEmpty = function () {
    return this.empty;
};

Project.prototype.isMerging = function() {
    return this.merging;
}

Project.prototype.update = async function (user, data) {
    var username;
    if (!user) {
        username = "_";
    } else {
        username = user.username;
    }

    var promises = [];
    var project = this;
    var saveSettings = false;
    var saveREADME = false;
    var savePackage = false;
    var flowFilesChanged = false;
    var credentialSecretChanged = false;
    var reloadProject = false;

    var globalProjectSettings = settings.get("projects");
    if (!globalProjectSettings.projects.hasOwnProperty(this.name)) {
        globalProjectSettings.projects[this.name] = {};
        saveSettings = true;
    }

    if (data.credentialSecret && data.credentialSecret !== this.credentialSecret) {
        var existingSecret = data.currentCredentialSecret;
        var isReset = data.resetCredentialSecret;
        var secret = data.credentialSecret;

        // console.log("updating credentialSecret");
        // console.log("request:");
        // console.log(JSON.stringify(data,"",4));
        // console.log(" this.credentialSecret",this.credentialSecret);
        // console.log(" this.info", this.info);

        if (!isReset && // not a reset
            this.credentialSecret && // key already set
            !this.credentialSecretInvalid && // key not invalid
            this.credentialSecret !== existingSecret) { // key doesn't match provided existing key
                var e = new Error("Cannot change credentialSecret without current key");
                e.code = "missing_current_credential_key";
                throw e;
        }
        this.credentialSecret = secret;

        globalProjectSettings.projects[this.name].credentialSecret = project.credentialSecret;
        delete this.credentialSecretInvalid;
        saveSettings = true;
        credentialSecretChanged = true;
    }

    if (this.missingFiles.indexOf('package.json') !== -1) {
        if (!data.files || !data.files.package) {
            // Cannot update a project that doesn't have a known package.json
            throw new Error("Cannot update project with missing package.json");
        }
    }

    if (data.hasOwnProperty('files')) {
        this.package['node-red'] = this.package['node-red'] || { settings: {}};
        if (data.files.hasOwnProperty('package') && (data.files.package !== fspath.join(this.paths.root,"package.json") || !this.paths['package.json'])) {
            // We have a package file. It could be one that doesn't exist yet,
            // or it does exist and we need to load it.
            if (!/package\.json$/.test(data.files.package)) {
                return new Error("Invalid package file: "+data.files.package)
            }
            var root = data.files.package.substring(0,data.files.package.length-12);
            if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.package)))) {
                return Promise.reject("Invalid package file: "+data.files.package)
            }
            this.paths.root = root;
            this.paths['package.json'] = data.files.package;
            globalProjectSettings.projects[this.name].rootPath = root;
            saveSettings = true;
            // 1. check if it exists
            if (fs.existsSync(fspath.join(this.path,this.paths['package.json']))) {
                // Load the existing one....
            } else {
                var newPackage = defaultFileSet["package.json"](this);
                fs.writeFileSync(fspath.join(this.path,this.paths['package.json']),newPackage);
                this.package = JSON.parse(newPackage);
            }
            reloadProject = true;
            flowFilesChanged = true;
        }

        if (data.files.hasOwnProperty('flow') && this.package['node-red'].settings.flowFile !== data.files.flow.substring(this.paths.root.length)) {
            if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.flow)))) {
                return Promise.reject("Invalid flow file: "+data.files.flow)
            }
            this.paths.flowFile = data.files.flow;
            this.package['node-red'].settings.flowFile = data.files.flow.substring(this.paths.root.length);
            savePackage = true;
            flowFilesChanged = true;
        }
        if (data.files.hasOwnProperty('credentials') && this.package['node-red'].settings.credentialsFile !== data.files.credentials.substring(this.paths.root.length)) {
            if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.credentials)))) {
                return Promise.reject("Invalid credentials file: "+data.files.credentials)
            }
            this.paths.credentialsFile = data.files.credentials;
            this.package['node-red'].settings.credentialsFile = data.files.credentials.substring(this.paths.root.length);
            // Don't know if the credSecret is invalid or not so clear the flag
            delete this.credentialSecretInvalid;
            savePackage = true;
            flowFilesChanged = true;
        }
    }

    if (data.hasOwnProperty('description')) {
        saveREADME = true;
        this.description = data.description;
    }
    if (data.hasOwnProperty('dependencies')) {
        savePackage = true;
        this.package.dependencies = data.dependencies;
    }
    if (data.hasOwnProperty('summary')) {
        savePackage = true;
        this.package.description = data.summary;
    }
    if (data.hasOwnProperty('version')) {
        savePackage = true;
        this.package.version = data.version;
    }

    if (data.hasOwnProperty('git')) {
        if (data.git.hasOwnProperty('user')) {
            globalProjectSettings.projects[this.name].git = globalProjectSettings.projects[this.name].git || {};
            globalProjectSettings.projects[this.name].git.user = globalProjectSettings.projects[this.name].git.user || {};
            globalProjectSettings.projects[this.name].git.user[username] = {
                name: data.git.user.name,
                email: data.git.user.email
            }
            this.git.user[username] = {
                name: data.git.user.name,
                email: data.git.user.email
            }
            saveSettings = true;
        }
        if (data.git.hasOwnProperty('remotes')) {
            var remoteNames = Object.keys(data.git.remotes);
            var remotesChanged = false;
            var modifyRemotesPromise = Promise.resolve();
            remoteNames.forEach(function(name) {
                if (data.git.remotes[name].removed) {
                    remotesChanged = true;
                    modifyRemotesPromise = modifyRemotesPromise.then(function() { gitTools.removeRemote(project.path,name) });
                } else {
                    if (data.git.remotes[name].url) {
                        remotesChanged = true;
                        modifyRemotesPromise = modifyRemotesPromise.then(function() { gitTools.addRemote(project.path,name,data.git.remotes[name])});
                    }
                    if (data.git.remotes[name].username && data.git.remotes[name].password) {
                        var url = data.git.remotes[name].url || project.remotes[name].fetch;
                        authCache.set(project.name,url,username,data.git.remotes[name]);
                    }
                }
            })
            if (remotesChanged) {
                modifyRemotesPromise = modifyRemotesPromise.then(function() {
                    return project.loadRemotes();
                });
                promises.push(modifyRemotesPromise.catch(err => {}));
            }
        }
    }


    if (saveSettings) {
        promises.push(settings.set("projects",globalProjectSettings).catch(err => {}));
    }

    var modifiedFiles = [];

    if (saveREADME) {
        promises.push(util.writeFile(fspath.join(this.path,this.paths['README.md']), this.description).catch(err => {}));
        modifiedFiles.push('README.md');
    }
    if (savePackage) {
        promises.push(fs.readFile(fspath.join(this.path,this.paths['package.json']),"utf8").then(content => {
            var currentPackage = {};
            try {
                currentPackage = util.parseJSON(content);
            } catch(err) {
            }
            this.package = Object.assign(currentPackage,this.package);
            return util.writeFile(fspath.join(project.path,this.paths['package.json']), JSON.stringify(this.package,"",4));
        }).catch(err => {}));
        modifiedFiles.push('package.json');
    }
    return Promise.all(promises).then(function(res) {
        var gitSettings = getUserGitSettings(user) || {};
        var workflowMode = (gitSettings.workflow||{}).mode || settings.editorTheme.projects.workflow.mode;
        if (workflowMode === 'auto') {
            return project.stageFile(modifiedFiles.map(f => project.paths[f])).then(() => {
                return project.commit(user,{message:"Update "+modifiedFiles.join(", ")})
            })
        }
    }).then(res => {
        if (reloadProject) {
            return this.load()
        }
    }).then(function() {
        return {
            flowFilesChanged: flowFilesChanged,
            credentialSecretChanged: credentialSecretChanged
        }})
};

Project.prototype.getFiles = function () {
    return gitTools.getFiles(this.path).catch(function(err) {
        if (/ambiguous argument/.test(err.message)) {
            return {};
        }
        throw err;
    });
};

Project.prototype.stageFile = function(file) {
    return gitTools.stageFile(this.path,file);
};

Project.prototype.unstageFile = function(file) {
    return gitTools.unstageFile(this.path,file);
}

Project.prototype.commit = function(user, options) {
    var self = this;
    return gitTools.commit(this.path,options.message,getGitUser(user)).then(function() {
        if (self.merging) {
            self.merging = false;
            return
        }
    });
}

Project.prototype.getFileDiff = function(file,type) {
    return gitTools.getFileDiff(this.path,file,type);
}

Project.prototype.getCommits = function(options) {
    return gitTools.getCommits(this.path,options).catch(function(err) {
        if (/bad default revision/i.test(err.message) || /ambiguous argument/i.test(err.message) || /does not have any commits yet/i.test(err.message)) {
            return {
                count:0,
                commits:[],
                total: 0
            }
        }
        throw err;
    })
}

Project.prototype.getCommit = function(sha) {
    return gitTools.getCommit(this.path,sha);
}

Project.prototype.getFile = function (filePath,treeish) {
    if (treeish !== "_") {
        return gitTools.getFile(this.path, filePath, treeish);
    } else {
        let fullPath = fspath.join(this.path,filePath);
        if (/^\.\./.test(fspath.relative(this.path,fullPath))) {
            throw new Error("Invalid file name")
        }
        return fs.readFile(fspath.join(this.path,filePath),"utf8");
    }
};

Project.prototype.revertFile = function (filePath) {
    var self = this;
    return gitTools.revertFile(this.path, filePath).then(function() {
        return self.load();
    });
};

Project.prototype.status = function(user, includeRemote) {
    var self = this;

    var fetchPromise;
    if (this.remotes && includeRemote) {
        fetchPromise = gitTools.getRemoteBranch(self.path).then(function(remoteBranch) {
            if (remoteBranch) {
                var allRemotes = Object.keys(self.remotes);
                var match = "";
                allRemotes.forEach(function(remote) {
                    if (remoteBranch.indexOf(remote) === 0 && match.length < remote.length) {
                        match = remote;
                    }
                })
                return self.fetch(user, match);
            }
        });
    } else {
        fetchPromise = Promise.resolve();
    }

    var completeStatus = function(fetchError) {
        var promises = [
            gitTools.getStatus(self.path),
            fs.exists(fspath.join(self.path,".git","MERGE_HEAD"))
        ];
        return Promise.all(promises).then(function(results) {
            var result = results[0];
            if (results[1]) {
                result.merging = true;
                if (!self.merging) {
                    self.merging = true;
                    events.emit("runtime-event",{
                        id:"runtime-state",
                        payload:{
                            type:"warning",
                            error:"git_merge_conflict",
                            project:self.name,
                            text:"notification.warnings.git_merge_conflict"
                        },
                        retain:true}
                    );
                }
            } else {
                self.merging = false;
            }
            self.branches.local = result.branches.local;
            self.branches.remote = result.branches.remote;
            if (fetchError && !/ambiguous argument/.test(fetchError.message)) {
                result.branches.remoteError = {
                    remote: fetchError.remote,
                    code: fetchError.code
                }
            }
            if (result.commits.total === 0 && Object.keys(result.files).length === 0) {
                if (!self.empty) {
                    events.emit("runtime-event",{
                        id:"runtime-state",
                        payload:{
                            type:"warning",
                            error:"project_empty",
                            text:"notification.warnings.project_empty"},
                            retain:true
                        }
                    );
                }
                self.empty = true;
            } else {
                if (self.empty) {
                    if (self.paths.flowFile) {
                        events.emit("runtime-event",{id:"runtime-state",retain:true});
                    } else {
                        events.emit("runtime-event",{
                            id:"runtime-state",
                            payload:{
                                type:"warning",
                                error:"missing_flow_file",
                                text:"notification.warnings.missing_flow_file"},
                                retain:true
                            }
                        );
                    }
                }
                delete self.empty;
            }
            return result;
        }).catch(function(err) {
            if (/ambiguous argument/.test(err.message)) {
                return {
                    files:{},
                    commits:{total:0},
                    branches:{}
                };
            }
            throw err;
        });
    }
    return fetchPromise.then(completeStatus).catch(function(e) {
        // if (e.code !== 'git_auth_failed') {
        //     console.log("Fetch failed");
        //     console.log(e);
        // }
        return completeStatus(e);
    })
};

Project.prototype.push = function (user,remoteBranchName,setRemote) {
    var username;
    if (!user) {
        username = "_";
    } else {
        username = user.username;
    }
    var remote = this.parseRemoteBranch(remoteBranchName||this.branches.remote);
    return gitTools.push(this.path, remote.remote || this.currentRemote,remote.branch, setRemote, authCache.get(this.name,this.remotes[remote.remote || this.currentRemote].fetch,username));
};

Project.prototype.pull = function (user,remoteBranchName,setRemote,allowUnrelatedHistories) {
    var username;
    if (!user) {
        username = "_";
    } else {
        username = user.username;
    }
    var self = this;
    if (setRemote) {
        return gitTools.setUpstream(this.path, remoteBranchName).then(function() {
            self.currentRemote = self.parseRemoteBranch(remoteBranchName).remote;
            return gitTools.pull(self.path, null, null, allowUnrelatedHistories, authCache.get(self.name,self.remotes[self.currentRemote].fetch,username),getGitUser(user));
        })
    } else {
        var remote = this.parseRemoteBranch(remoteBranchName);
        return gitTools.pull(this.path, remote.remote, remote.branch, allowUnrelatedHistories, authCache.get(this.name,this.remotes[remote.remote||self.currentRemote].fetch,username),getGitUser(user));
    }
};

Project.prototype.resolveMerge = function (file,resolutions) {
    var filePath = fspath.join(this.path,file);

    if (/^\.\./.test(fspath.relative(this.path,filePath))) {
        throw new Error("Invalid file name")
    }

    var self = this;
    if (typeof resolutions === 'string') {
        return util.writeFile(filePath, resolutions).then(function() {
            return self.stageFile(file);
        })
    }
    return fs.readFile(filePath,"utf8").then(function(content) {
        var lines = content.split("\n");
        var result = [];
        var ignoreBlock = false;
        var currentBlock;
        for (var i=1;i<=lines.length;i++) {
            if (resolutions.hasOwnProperty(i)) {
                currentBlock = resolutions[i];
                if (currentBlock.selection === "A") {
                    ignoreBlock = false;
                } else {
                    ignoreBlock = true;
                }
                continue;
            }
            if (currentBlock) {
                if (currentBlock.separator === i) {
                    if (currentBlock.selection === "A") {
                        ignoreBlock = true;
                    } else {
                        ignoreBlock = false;
                    }
                    continue;
                } else if (currentBlock.changeEnd === i) {
                    currentBlock = null;
                    continue;
                } else if (ignoreBlock) {
                    continue;
                }
            }
            result.push(lines[i-1]);
        }
        var finalResult = result.join("\n");
        return util.writeFile(filePath,finalResult).then(function() {
            return self.stageFile(file);
        })
    });
};

Project.prototype.abortMerge = function () {
    var self = this;
    return gitTools.abortMerge(this.path).then(function() {
        self.merging = false;
    })
};

Project.prototype.getBranches = function (user, isRemote) {
    var self = this;
    var fetchPromise;
    if (isRemote) {
        fetchPromise = self.fetch(user);
    } else {
        fetchPromise = Promise.resolve();
    }
    return fetchPromise.then(function() {
        return gitTools.getBranches(self.path,isRemote);
    });
};

Project.prototype.deleteBranch = function (user, branch, isRemote, force) {
    // TODO: isRemote==true support
    // TODO: make sure we don't try to delete active branch
    return gitTools.deleteBranch(this.path,branch,isRemote, force);
};

Project.prototype.fetch = function(user,remoteName) {
    var username;
    if (!user) {
        username = "_";
    } else {
        username = user.username;
    }
    var project = this;
    if (remoteName) {
        return gitTools.fetch(project.path,remoteName,authCache.get(project.name,project.remotes[remoteName].fetch,username)).catch(function(err) {
            err.remote = remoteName;
            throw err;
        })
    } else {
        var remotes = Object.keys(this.remotes);
        var promise = Promise.resolve();
        remotes.forEach(function(remote) {
            promise = promise.then(function() {
                return gitTools.fetch(project.path,remote,authCache.get(project.name,project.remotes[remote].fetch,username))
            }).catch(function(err) {
                if (!err.remote) {
                    err.remote = remote;
                }
                throw err;
            })
        });
        return promise;
    }
}

Project.prototype.setBranch = function (branchName, isCreate) {
    var self = this;
    return gitTools.checkoutBranch(this.path, branchName, isCreate).then(function() {
        return self.load();
    })
};

Project.prototype.getBranchStatus = function (branchName) {
    return gitTools.getBranchStatus(this.path,branchName);
};

Project.prototype.getRemotes = function (user) {
    return gitTools.getRemotes(this.path).then(function(remotes) {
        var result = [];
        for (var name in remotes) {
            if (remotes.hasOwnProperty(name)) {
                remotes[name].name = name;
                result.push(remotes[name]);
            }
        }
        return {remotes:result};
    })
};

Project.prototype.addRemote = function(user,remote,options) {
    var project = this;
    return gitTools.addRemote(this.path,remote,options).then(function() {
        return project.loadRemotes()
    });
}

Project.prototype.updateRemote = function(user,remote,options) {
    var username;
    if (!user) {
        username = "_";
    } else {
        username = user.username;
    }

    if (options.auth) {
        var url = this.remotes[remote].fetch;
        if (options.auth.keyFile) {
            options.auth.key_path = sshKeys.getPrivateKeyPath(getSSHKeyUsername(user), options.auth.keyFile);
        }
        authCache.set(this.name,url,username,options.auth);
    }
    return Promise.resolve();
}

Project.prototype.removeRemote = function(user, remote) {
    // TODO: if this was the last remote using this url, then remove the authCache
    // details.
    var project = this;
    return gitTools.removeRemote(this.path,remote).then(function() {
        return project.loadRemotes()
    });
}

Project.prototype.getFlowFile = function() {
    // console.log("Project.getFlowFile = ",this.paths.flowFile);
    if (this.paths.flowFile) {
        return fspath.join(this.path,this.paths.flowFile);
    } else {
        return null;
    }
}

Project.prototype.getFlowFileBackup = function() {
    var flowFile = this.getFlowFile();
    if (flowFile) {
        return getBackupFilename(flowFile);
    }
    return null;
}

Project.prototype.getCredentialsFile = function() {
    // console.log("Project.getCredentialsFile = ",this.paths.credentialsFile);
    if (this.paths.credentialsFile) {
        return fspath.join(this.path,this.paths.credentialsFile);
    } else {
        return this.paths.credentialsFile;
    }
}

Project.prototype.getCredentialsFileBackup = function() {
    return getBackupFilename(this.getCredentialsFile());
}

Project.prototype.export = function () {

    return {
        name: this.name,
        summary: this.package.description,
        version: this.package.version,
        description: this.description,
        dependencies: this.package.dependencies||{},
        empty: this.empty,
        settings: {
            credentialsEncrypted: (typeof this.credentialSecret === "string") && this.credentialSecret.length > 0,
            credentialSecretInvalid: this.credentialSecretInvalid
        },
        files: {
            package: this.paths['package.json'],
            flow: this.paths.flowFile,
            credentials: this.paths.credentialsFile
        },
        git: {
            remotes: this.remotes,
            branches: this.branches
        }
    }
};

function getCredentialsFilename(filename) {
    filename = filename || "undefined";
    // TODO: DRY - ./index.js
    var ffDir = fspath.dirname(filename);
    var ffExt = fspath.extname(filename);
    var ffBase = fspath.basename(filename,ffExt);
    return fspath.join(ffDir,ffBase+"_cred"+ffExt);
}
function getBackupFilename(filename) {
    // TODO: DRY - ./index.js
    filename = filename || "undefined";
    var ffName = fspath.basename(filename);
    var ffDir = fspath.dirname(filename);
    return fspath.join(ffDir,"."+ffName+".backup");
}
function checkProjectExists(projectPath) {
    return fs.pathExists(projectPath).then(function(exists) {
        if (!exists) {
            var e = new Error("Project not found");
            e.code = "project_not_found";
            var name = fspath.basename(projectPath);
            e.project = name;
            throw e;
        }
    });
}
function createDefaultProject(user, project) {
    var projectPath = fspath.join(projectsDir,project.name);
    // Create a basic skeleton of a project
    return gitTools.initRepo(projectPath).then(function() {
        var promises = [];
        var files = Object.keys(defaultFileSet);
        if (project.files) {
            if (project.files.flow && !/\.\./.test(project.files.flow)) {
                var flowFilePath;
                var credsFilePath;

                if (project.migrateFiles) {
                    var baseFlowFileName = project.files.flow || fspath.basename(project.files.oldFlow);
                    var baseCredentialFileName = project.files.credentials || fspath.basename(project.files.oldCredentials);
                    files.push(baseFlowFileName);
                    files.push(baseCredentialFileName);
                    flowFilePath = fspath.join(projectPath,baseFlowFileName);
                    credsFilePath = fspath.join(projectPath,baseCredentialFileName);
                    if (fs.existsSync(project.files.oldFlow)) {
                        log.trace("Migrating "+project.files.oldFlow+" to "+flowFilePath);
                        promises.push(fs.copy(project.files.oldFlow,flowFilePath));
                    } else {
                        log.trace(project.files.oldFlow+" does not exist - creating blank file");
                        promises.push(util.writeFile(flowFilePath,"[]"));
                    }
                    log.trace("Migrating "+project.files.oldCredentials+" to "+credsFilePath);
                    runtime.nodes.setCredentialSecret(project.credentialSecret);
                    promises.push(runtime.nodes.exportCredentials().then(function(creds) {
                        var credentialData;
                        if (settings.flowFilePretty) {
                            credentialData = JSON.stringify(creds,null,4);
                        } else {
                            credentialData = JSON.stringify(creds);
                        }
                        return util.writeFile(credsFilePath,credentialData);
                    }));
                    delete project.migrateFiles;
                    project.files.flow = baseFlowFileName;
                    project.files.credentials = baseCredentialFileName;
                } else {
                    project.files.credentials = project.files.credentials || getCredentialsFilename(project.files.flow);
                    files.push(project.files.flow);
                    files.push(project.files.credentials);
                    flowFilePath = fspath.join(projectPath,project.files.flow);
                    credsFilePath = getCredentialsFilename(flowFilePath);
                    promises.push(util.writeFile(flowFilePath,"[]"));
                    promises.push(util.writeFile(credsFilePath,"{}"));
                }
            }
        }
        for (var file in defaultFileSet) {
            if (defaultFileSet.hasOwnProperty(file)) {
                promises.push(util.writeFile(fspath.join(projectPath,file),defaultFileSet[file](project)));
            }
        }

        return Promise.all(promises).then(function() {
            return gitTools.stageFile(projectPath,files);
        }).then(function() {
            return gitTools.commit(projectPath,"Create project",getGitUser(user));
        })
    });
}
function checkProjectFiles(project) {
    var promises = [];
    var missing = [];
    for (var file in defaultFileSet) {
        if (defaultFileSet.hasOwnProperty(file)) {
            (function(f) {
                promises.push(fs.stat(fspath.join(project.path,project.paths.root,f)).catch(err => {
                    missing.push(f);
                }));
            })(file);
        }
    }
    return Promise.all(promises).then(() => missing);
}
function createProject(user, metadata) {
    var username;
    if (!user) {
        username = "_";
    } else {
        username = user.username;
    }
    if (!metadata.path) {
        throw new Error("Project missing path property");
    }
    if (!metadata.name) {
        throw new Error("Project missing name property");
    }

    var project = metadata.name;
    var projectPath = metadata.path;
    return new Promise(function(resolve,reject) {
        fs.stat(projectPath, function(err,stat) {
            if (!err) {
                var e = new Error("NLS: Project already exists");
                e.code = "project_exists";
                return reject(e);
            }
            fs.ensureDir(projectPath).then(function() {
                var projects = settings.get('projects');
                if (!projects) {
                    projects = {
                        projects:{}
                    }
                }
                projects.projects[project] = {};
                if (metadata.hasOwnProperty('credentialSecret')) {
                    if (metadata.credentialSecret === "") {
                        metadata.credentialSecret = false;
                    }
                    projects.projects[project].credentialSecret = metadata.credentialSecret;
                }
                return settings.set('projects',projects);
            }).then(function() {
                if (metadata.git && metadata.git.remotes && metadata.git.remotes.origin) {
                    var originRemote = metadata.git.remotes.origin;
                    var auth;
                    if (originRemote.hasOwnProperty("username") && originRemote.hasOwnProperty("password")) {
                        authCache.set(project,originRemote.url,username,{ // TODO: hardcoded remote name
                                username: originRemote.username,
                                password: originRemote.password
                            }
                        );
                        auth = authCache.get(project,originRemote.url,username);
                    }
                    else if (originRemote.hasOwnProperty("keyFile") && originRemote.hasOwnProperty("passphrase")) {
                        authCache.set(project,originRemote.url,username,{ // TODO: hardcoded remote name
                                key_path: sshKeys.getPrivateKeyPath(getSSHKeyUsername(user), originRemote.keyFile),
                                passphrase: originRemote.passphrase
                            }
                        );
                        auth = authCache.get(project,originRemote.url,username);
                    }
                    return gitTools.clone(originRemote,auth,projectPath);
                } else {
                    return createDefaultProject(user, metadata);
                }
            }).then(function() {
                resolve(loadProject(projectPath))
            }).catch(function(err) {
                fs.remove(projectPath,function() {
                    reject(err);
                });
            });
        })
    })
}
function deleteProject(user, projectPath) {
    return checkProjectExists(projectPath).then(function() {
        return fs.remove(projectPath).then(function() {
            var name = fspath.basename(projectPath);
            var projects = settings.get('projects');
            delete projects.projects[name];
            return settings.set('projects', projects);
        });
    });
}
function loadProject(projectPath) {
    return checkProjectExists(projectPath).then(function() {
        var project = new Project(projectPath);
        return project.load();
    });
}
function init(_settings, _runtime) {
    settings = _settings;
    runtime = _runtime;
    projectsDir = fspath.resolve(fspath.join(settings.userDir,"projects"));
    if(settings.editorTheme.projects.path) {
        projectsDir = settings.editorTheme.projects.path;
    }
    authCache.init();
}

module.exports = {
    init: init,
    load: loadProject,
    create: createProject,
    delete: deleteProject
}
