"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const yaml = require("js-yaml");
const errors_1 = require("./errors");
const shortcode_1 = require("./shortcode");
const util_1 = require("./util");
/**
 * Thrown once the GrantNotifier says the the grant is cancelled,
 * in Profile.grant().
 */
class GrantCancelledError extends Error {
}
exports.GrantCancelledError = GrantCancelledError;
/**
 * Profile represents a user profile for miix, stored in the .miixrc file.
 */
class Profile {
    constructor(file, oauthClient = new shortcode_1.OAuthClient({
            clientId: '9789aae60656644524be9530889ba8884c0095834ae75f50',
            scopes: Profile.necessaryScopes,
        }), requester = new util_1.Fetcher(), host = util_1.api()) {
        this.file = file;
        this.oauthClient = oauthClient;
        this.requester = requester;
        this.host = host;
        this.profile = {
            hosts: {},
        };
    }
    get hostProfile() {
        return this.profile.hosts[this.host];
    }
    /**
     * Returns valid OAuth tokens for the session. It will prompt the user to
     * grant access if the tokens don't exist or are expired.
     */
    tokens() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.ensureProfile();
            return this.tokensObj;
        });
    }
    /**
     * Returns data about the currently authenticated user.
     */
    user() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.ensureProfile();
            return this.hostProfile.userdata;
        });
    }
    /**
     * Returns whether the user currently has valid credentials.
     */
    hasAuthenticated() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.hostProfile) {
                yield this.tryLoadFile();
            }
            return this.tokensObj !== undefined && !this.tokensObj.expired();
        });
    }
    /**
     * Logs out of the current session, if the user is authenticated.
     */
    logout() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.tryLoadFile();
            delete this.profile.hosts[this.host];
            this.tokensObj = undefined;
            yield this.save();
        });
    }
    /**
     * Prompts the user to grant the tokens anew, and saves the profile.
     */
    grant(notifier) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.tryLoadFile();
            let tokens;
            do {
                if (notifier.isCancelled()) {
                    throw new GrantCancelledError();
                }
                const code = yield this.oauthClient.getCode();
                notifier.prompt(code.code);
                try {
                    tokens = yield code.waitForAccept();
                }
                catch (e) {
                    if (!(e instanceof errors_1.ShortCodeExpireError)) {
                        throw e;
                    }
                }
            } while (!tokens);
            const udata = yield this.requester
                .with(tokens)
                .json('get', '/users/current?fields=id,username,channel')
                .then((res) => __awaiter(this, void 0, void 0, function* () { return res.json(); }));
            this.tokensObj = tokens;
            this.profile.hosts[this.host] = {
                tokens: this.tokensObj.data,
                userdata: {
                    id: udata.id,
                    username: udata.username,
                    channel: udata.channel.id,
                },
            };
            yield this.save();
            return tokens;
        });
    }
    /**
     * Ensures that the profile exists and has valid grants.
     */
    ensureProfile() {
        return __awaiter(this, void 0, void 0, function* () {
            const hostProfile = this.profile && this.profile.hosts[this.host];
            if (hostProfile) {
                return hostProfile;
            }
            if (!(yield this.tryLoadFile())) {
                throw new errors_1.NoAuthenticationError();
            }
            else if (!this.tokensObj.granted(Profile.necessaryScopes)) {
                throw new errors_1.NoAuthenticationError();
            }
            else if (this.tokensObj.expired()) {
                try {
                    yield this.refresh();
                }
                catch (err) {
                    if (err instanceof errors_1.UnexpectedHttpError && err.tryJson().error === 'invalid_grant') {
                        throw new errors_1.NoAuthenticationError();
                    }
                    else {
                        throw err;
                    }
                }
            }
            return this.profile.hosts[this.host];
        });
    }
    /**
     * Persists the saved tokens to disk.
     */
    save() {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => {
                fs.writeFile(this.file, yaml.safeDump(this.profile), err => {
                    if (err) {
                        reject(err);
                    }
                    else {
                        resolve();
                    }
                });
            });
        });
    }
    /**
     * Refreshes the token grants and saves the profile.
     */
    refresh() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.tokensObj) {
                throw new Error('Cannot refresh without an available tokensObj');
            }
            this.tokensObj = yield this.oauthClient.refresh(this.tokensObj);
            this.hostProfile.tokens = this.tokensObj.data;
            yield this.save();
        });
    }
    /**
     * Attempts to load existing data from the profile file, returning whether
     * it was able to do so successfully.
     */
    tryLoadFile() {
        return __awaiter(this, void 0, void 0, function* () {
            return util_1.exists(this.file).then(ok => {
                if (!ok) {
                    return false;
                }
                return util_1.readFile(this.file).then(contents => {
                    try {
                        this.profile = yaml.safeLoad(contents);
                    }
                    catch (err) {
                        throw util_1.wrapErr(err, `Error parsing profile from ${this.file}`);
                    }
                    if (!this.hostProfile) {
                        return false;
                    }
                    this.tokensObj = new shortcode_1.OAuthTokens(Object.assign({}, this.hostProfile.tokens, { expiresAt: new Date(this.hostProfile.tokens.expiresAt) }));
                    return true;
                });
            });
        });
    }
}
Profile.necessaryScopes = [
    'interactive:manage:self',
    'interactive:play',
    'channel:teststream:view:self',
];
exports.Profile = Profile;
