"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const errors_1 = require("@oclif/errors");
const os = require("os");
const path = require("path");
const url_1 = require("url");
const util_1 = require("util");
const debug_1 = require("./debug");
const Plugin = require("./plugin");
const ts_node_1 = require("./ts-node");
const util_2 = require("./util");
// eslint-disable-next-line new-cap
const debug = debug_1.default();
const _pjson = require('../package.json');
function channelFromVersion(version) {
    const m = version.match(/[^-]+(?:-([^.]+))?/);
    return (m && m[1]) || 'stable';
}
function hasManifest(p) {
    try {
        require(p);
        return true;
    }
    catch (_a) {
        return false;
    }
}
const WSL = require('is-wsl');
class Config {
    // eslint-disable-next-line no-useless-constructor
    constructor(options) {
        this.options = options;
        this._base = `${_pjson.name}@${_pjson.version}`;
        this.debug = 0;
        this.plugins = [];
        this.warned = false;
    }
    // eslint-disable-next-line complexity
    async load() {
        const plugin = new Plugin.Plugin({ root: this.options.root });
        await plugin.load();
        this.plugins.push(plugin);
        this.root = plugin.root;
        this.pjson = plugin.pjson;
        this.name = this.pjson.name;
        this.version = this.options.version || this.pjson.version || '0.0.0';
        this.channel = this.options.channel || channelFromVersion(this.version);
        this.valid = plugin.valid;
        this.arch = (os.arch() === 'ia32' ? 'x86' : os.arch());
        this.platform = WSL ? 'wsl' : os.platform();
        this.windows = this.platform === 'win32';
        this.bin = this.pjson.oclif.bin || this.name;
        this.dirname = this.pjson.oclif.dirname || this.name;
        if (this.platform === 'win32')
            this.dirname = this.dirname.replace('/', '\\');
        this.userAgent = `${this.name}/${this.version} ${this.platform}-${this.arch} node-${process.version}`;
        this.shell = this._shell();
        this.debug = this._debug();
        this.home = process.env.HOME || (this.windows && this.windowsHome()) || os.homedir() || os.tmpdir();
        this.cacheDir = this.scopedEnvVar('CACHE_DIR') || this.macosCacheDir() || this.dir('cache');
        this.configDir = this.scopedEnvVar('CONFIG_DIR') || this.dir('config');
        this.dataDir = this.scopedEnvVar('DATA_DIR') || this.dir('data');
        this.errlog = path.join(this.cacheDir, 'error.log');
        this.binPath = this.scopedEnvVar('BINPATH');
        this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.oclif.npmRegistry;
        this.pjson.oclif.update = this.pjson.oclif.update || {};
        this.pjson.oclif.update.node = this.pjson.oclif.update.node || {};
        const s3 = this.pjson.oclif.update.s3 || {};
        this.pjson.oclif.update.s3 = s3;
        s3.bucket = this.scopedEnvVar('S3_BUCKET') || s3.bucket;
        if (s3.bucket && !s3.host)
            s3.host = `https://${s3.bucket}.s3.amazonaws.com`;
        s3.templates = Object.assign(Object.assign({}, s3.templates), { target: Object.assign({ baseDir: '<%- bin %>', unversioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-<%- platform %>-<%- arch %><%- ext %>", versioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>", manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- platform %>-<%- arch %>" }, s3.templates && s3.templates.target), vanilla: Object.assign({ unversioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %><%- ext %>", versioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %><%- ext %>", baseDir: '<%- bin %>', manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %>version" }, s3.templates && s3.templates.vanilla) });
        await this.loadUserPlugins();
        await this.loadDevPlugins();
        await this.loadCorePlugins();
        debug('config done');
    }
    async loadCorePlugins() {
        if (this.pjson.oclif.plugins) {
            await this.loadPlugins(this.root, 'core', this.pjson.oclif.plugins);
        }
    }
    async loadDevPlugins() {
        if (this.options.devPlugins !== false) {
            // do not load oclif.devPlugins in production
            if (hasManifest(path.join(this.root, 'oclif.manifest.json')))
                return;
            try {
                const devPlugins = this.pjson.oclif.devPlugins;
                if (devPlugins)
                    await this.loadPlugins(this.root, 'dev', devPlugins);
            }
            catch (error) {
                process.emitWarning(error);
            }
        }
    }
    async loadUserPlugins() {
        if (this.options.userPlugins !== false) {
            try {
                const userPJSONPath = path.join(this.dataDir, 'package.json');
                debug('reading user plugins pjson %s', userPJSONPath);
                const pjson = await util_2.loadJSON(userPJSONPath);
                this.userPJSON = pjson;
                if (!pjson.oclif)
                    pjson.oclif = { schema: 1 };
                if (!pjson.oclif.plugins)
                    pjson.oclif.plugins = [];
                await this.loadPlugins(userPJSONPath, 'user', pjson.oclif.plugins.filter((p) => p.type === 'user'));
                await this.loadPlugins(userPJSONPath, 'link', pjson.oclif.plugins.filter((p) => p.type === 'link'));
            }
            catch (error) {
                if (error.code !== 'ENOENT')
                    process.emitWarning(error);
            }
        }
    }
    async runHook(event, opts) {
        debug('start %s hook', event);
        const promises = this.plugins.map(p => {
            const debug = require('debug')([this.bin, p.name, 'hooks', event].join(':'));
            const context = {
                config: this,
                debug,
                exit(code = 0) {
                    errors_1.exit(code);
                },
                log(message, ...args) {
                    process.stdout.write(util_1.format(message, ...args) + '\n');
                },
                error(message, options = {}) {
                    errors_1.error(message, options);
                },
                warn(message) {
                    errors_1.warn(message);
                },
            };
            return Promise.all((p.hooks[event] || [])
                .map(async (hook) => {
                try {
                    const f = ts_node_1.tsPath(p.root, hook);
                    debug('start', f);
                    const search = (m) => {
                        if (typeof m === 'function')
                            return m;
                        if (m.default && typeof m.default === 'function')
                            return m.default;
                        return Object.values(m).find((m) => typeof m === 'function');
                    };
                    await search(require(f)).call(context, Object.assign(Object.assign({}, opts), { config: this }));
                    debug('done');
                }
                catch (error) {
                    if (error && error.oclif && error.oclif.exit !== undefined)
                        throw error;
                    this.warn(error, `runHook ${event}`);
                }
            }));
        });
        await Promise.all(promises);
        debug('%s hook done', event);
    }
    async runCommand(id, argv = []) {
        debug('runCommand %s %o', id, argv);
        const c = this.findCommand(id);
        if (!c) {
            await this.runHook('command_not_found', { id });
            throw new errors_1.CLIError(`command ${id} not found`);
        }
        const command = c.load();
        await this.runHook('prerun', { Command: command, argv });
        const result = await command.run(argv, this);
        await this.runHook('postrun', { Command: command, result: result, argv });
    }
    scopedEnvVar(k) {
        return process.env[this.scopedEnvVarKey(k)];
    }
    scopedEnvVarTrue(k) {
        const v = process.env[this.scopedEnvVarKey(k)];
        return v === '1' || v === 'true';
    }
    scopedEnvVarKey(k) {
        return [this.bin, k]
            // eslint-disable-next-line no-useless-escape
            .map(p => p.replace(/@/g, '').replace(/[-\/]/g, '_'))
            .join('_')
            .toUpperCase();
    }
    findCommand(id, opts = {}) {
        const command = this.commands.find(c => c.id === id || c.aliases.includes(id));
        if (command)
            return command;
        if (opts.must)
            errors_1.error(`command ${id} not found`);
    }
    findTopic(name, opts = {}) {
        const topic = this.topics.find(t => t.name === name);
        if (topic)
            return topic;
        if (opts.must)
            throw new Error(`topic ${name} not found`);
    }
    get commands() {
        return util_2.flatMap(this.plugins, p => p.commands);
    }
    get commandIDs() {
        return util_2.uniq(this.commands.map(c => c.id));
    }
    get topics() {
        const topics = [];
        for (const plugin of this.plugins) {
            for (const topic of util_2.compact(plugin.topics)) {
                const existing = topics.find(t => t.name === topic.name);
                if (existing) {
                    existing.description = topic.description || existing.description;
                    existing.hidden = existing.hidden || topic.hidden;
                }
                else
                    topics.push(topic);
            }
        }
        // add missing topics
        for (const c of this.commands.filter(c => !c.hidden)) {
            const parts = c.id.split(':');
            while (parts.length) {
                const name = parts.join(':');
                if (name && !topics.find(t => t.name === name)) {
                    topics.push({ name, description: c.description });
                }
                parts.pop();
            }
        }
        return topics;
    }
    s3Key(type, ext, options = {}) {
        if (typeof ext === 'object')
            options = ext;
        else if (ext)
            options.ext = ext;
        const _ = require('lodash');
        return _.template(this.pjson.oclif.update.s3.templates[options.platform ? 'target' : 'vanilla'][type])(Object.assign(Object.assign({}, this), options));
    }
    s3Url(key) {
        const host = this.pjson.oclif.update.s3.host;
        if (!host)
            throw new Error('no s3 host is set');
        const url = new url_1.URL(host);
        url.pathname = path.join(url.pathname, key);
        return url.toString();
    }
    dir(category) {
        const base = process.env[`XDG_${category.toUpperCase()}_HOME`] ||
            (this.windows && process.env.LOCALAPPDATA) ||
            path.join(this.home, category === 'data' ? '.local/share' : '.' + category);
        return path.join(base, this.dirname);
    }
    windowsHome() {
        return this.windowsHomedriveHome() || this.windowsUserprofileHome();
    }
    windowsHomedriveHome() {
        return (process.env.HOMEDRIVE && process.env.HOMEPATH && path.join(process.env.HOMEDRIVE, process.env.HOMEPATH));
    }
    windowsUserprofileHome() {
        return process.env.USERPROFILE;
    }
    macosCacheDir() {
        return (this.platform === 'darwin' && path.join(this.home, 'Library', 'Caches', this.dirname)) || undefined;
    }
    _shell() {
        let shellPath;
        const { SHELL, COMSPEC } = process.env;
        if (SHELL) {
            shellPath = SHELL.split('/');
        }
        else if (this.windows && COMSPEC) {
            shellPath = COMSPEC.split(/\\|\//);
        }
        else {
            shellPath = ['unknown'];
        }
        return shellPath[shellPath.length - 1];
    }
    _debug() {
        if (this.scopedEnvVarTrue('DEBUG'))
            return 1;
        try {
            const { enabled } = require('debug')(this.bin);
            if (enabled)
                return 1;
        }
        catch (_a) { }
        return 0;
    }
    async loadPlugins(root, type, plugins, parent) {
        if (!plugins || plugins.length === 0)
            return;
        debug('loading plugins', plugins);
        await Promise.all((plugins || []).map(async (plugin) => {
            try {
                const opts = { type, root };
                if (typeof plugin === 'string') {
                    opts.name = plugin;
                }
                else {
                    opts.name = plugin.name || opts.name;
                    opts.tag = plugin.tag || opts.tag;
                    opts.root = plugin.root || opts.root;
                }
                const instance = new Plugin.Plugin(opts);
                await instance.load();
                if (this.plugins.find(p => p.name === instance.name))
                    return;
                this.plugins.push(instance);
                if (parent) {
                    // eslint-disable-next-line require-atomic-updates
                    instance.parent = parent;
                    if (!parent.children)
                        parent.children = [];
                    parent.children.push(instance);
                }
                await this.loadPlugins(instance.root, type, instance.pjson.oclif.plugins || [], instance);
            }
            catch (error) {
                this.warn(error, 'loadPlugins');
            }
        }));
    }
    warn(err, scope) {
        if (this.warned)
            return;
        if (typeof err === 'string') {
            process.emitWarning(err);
            return;
        }
        if (err instanceof Error) {
            const modifiedErr = err;
            modifiedErr.name = `${err.name} Plugin: ${this.name}`;
            modifiedErr.detail = util_2.compact([
                err.detail,
                `module: ${this._base}`,
                scope && `task: ${scope}`,
                `plugin: ${this.name}`,
                `root: ${this.root}`,
                'See more details with DEBUG=*',
            ]).join('\n');
            process.emitWarning(err);
            return;
        }
        // err is an object
        process.emitWarning('Config.warn expected either a string or Error, but instead received an object');
        err.name = `${err.name} Plugin: ${this.name}`;
        err.detail = util_2.compact([
            err.detail,
            `module: ${this._base}`,
            scope && `task: ${scope}`,
            `plugin: ${this.name}`,
            `root: ${this.root}`,
            'See more details with DEBUG=*',
        ]).join('\n');
        process.emitWarning(JSON.stringify(err));
    }
}
exports.Config = Config;
function isConfig(o) {
    return o && Boolean(o._base);
}
async function load(opts = (module.parent && module.parent && module.parent.parent && module.parent.parent.filename) || __dirname) {
    if (typeof opts === 'string')
        opts = { root: opts };
    if (isConfig(opts))
        return opts;
    const config = new Config(opts);
    await config.load();
    return config;
}
exports.load = load;
