"use strict";

const cookies = require("js-cookie");
const request = require("superagent");
const events = require("./events.js");
const progress = require("./util/progress.js");
const uri = require("./util/uri.js");

let fileTokens = {};
let remoteConfig = null;

class Api extends events.EventTarget {
    constructor() {
        super();
        this.user = null;
        this.userName = null;
        this.userPassword = null;
        this.token = null;
        this.cache = {};
        this.allRanks = [
            "anonymous",
            "restricted",
            "regular",
            "power",
            "moderator",
            "administrator",
            "nobody",
        ];
        this.rankNames = new Map([
            ["anonymous", "Anonymous"],
            ["restricted", "Restricted user"],
            ["regular", "Regular user"],
            ["power", "Power user"],
            ["moderator", "Moderator"],
            ["administrator", "Administrator"],
            ["nobody", "Nobody"],
        ]);
    }

    get(url, options) {
        if (url in this.cache) {
            return new Promise((resolve, reject) => {
                resolve(this.cache[url]);
            });
        }
        return this._wrappedRequest(url, request.get, {}, {}, options).then(
            (response) => {
                this.cache[url] = response;
                return Promise.resolve(response);
            }
        );
    }

    post(url, data, files, options) {
        this.cache = {};
        return this._wrappedRequest(url, request.post, data, files, options);
    }

    put(url, data, files, options) {
        this.cache = {};
        return this._wrappedRequest(url, request.put, data, files, options);
    }

    delete(url, data, options) {
        this.cache = {};
        return this._wrappedRequest(url, request.delete, data, {}, options);
    }

    fetchConfig() {
        if (remoteConfig === null) {
            return this.get(uri.formatApiLink("info")).then((response) => {
                remoteConfig = response.config;
            });
        } else {
            return Promise.resolve();
        }
    }

    getName() {
        return remoteConfig.name;
    }

    getTagNameRegex() {
        return remoteConfig.tagNameRegex;
    }

    getPoolNameRegex() {
        return remoteConfig.poolNameRegex;
    }

    getPasswordRegex() {
        return remoteConfig.passwordRegex;
    }

    getUserNameRegex() {
        return remoteConfig.userNameRegex;
    }

    getContactEmail() {
        return remoteConfig.contactEmail;
    }

    canSendMails() {
        return !!remoteConfig.canSendMails;
    }

    safetyEnabled() {
        return !!remoteConfig.enableSafety;
    }

    hasPrivilege(lookup) {
        let minViableRank = null;
        for (let p of Object.keys(remoteConfig.privileges)) {
            if (!p.startsWith(lookup)) {
                continue;
            }
            const rankIndex = this.allRanks.indexOf(
                remoteConfig.privileges[p]
            );
            if (minViableRank === null || rankIndex < minViableRank) {
                minViableRank = rankIndex;
            }
        }
        if (minViableRank === null) {
            throw `Bad privilege name: ${lookup}`;
        }
        let myRank =
            this.user !== null ? this.allRanks.indexOf(this.user.rank) : 0;
        return myRank >= minViableRank;
    }

    loginFromCookies() {
        const auth = cookies.getJSON("auth");
        return auth && auth.user && auth.token
            ? this.loginWithToken(auth.user, auth.token, true)
            : Promise.resolve();
    }

    loginWithToken(userName, token, doRemember) {
        this.cache = {};
        return new Promise((resolve, reject) => {
            this.userName = userName;
            this.token = token;
            this.get("/user/" + userName + "?bump-login=true").then(
                (response) => {
                    const options = {};
                    if (doRemember) {
                        options.expires = 365;
                    }
                    cookies.set(
                        "auth",
                        { user: userName, token: token },
                        options
                    );
                    this.user = response;
                    resolve();
                    this.dispatchEvent(new CustomEvent("login"));
                },
                (error) => {
                    reject(error);
                    this.logout();
                }
            );
        });
    }

    createToken(userName, options) {
        let userTokenRequest = {
            enabled: true,
            note: "Web Login Token",
        };
        if (typeof options.expires !== "undefined") {
            userTokenRequest.expirationTime = new Date()
                .addDays(options.expires)
                .toISOString();
        }
        return new Promise((resolve, reject) => {
            this.post("/user-token/" + userName, userTokenRequest).then(
                (response) => {
                    cookies.set(
                        "auth",
                        { user: userName, token: response.token },
                        options
                    );
                    this.userName = userName;
                    this.token = response.token;
                    this.userPassword = null;
                },
                (error) => {
                    reject(error);
                }
            );
        });
    }

    deleteToken(userName, userToken) {
        return new Promise((resolve, reject) => {
            this.delete("/user-token/" + userName + "/" + userToken, {}).then(
                (response) => {
                    const options = {};
                    cookies.set(
                        "auth",
                        { user: userName, token: null },
                        options
                    );
                    resolve();
                },
                (error) => {
                    reject(error);
                }
            );
        });
    }

    login(userName, userPassword, doRemember) {
        this.cache = {};
        return new Promise((resolve, reject) => {
            this.userName = userName;
            this.userPassword = userPassword;
            this.get("/user/" + userName + "?bump-login=true").then(
                (response) => {
                    const options = {};
                    if (doRemember) {
                        options.expires = 365;
                    }
                    this.createToken(this.userName, options);
                    this.user = response;
                    resolve();
                    this.dispatchEvent(new CustomEvent("login"));
                },
                (error) => {
                    reject(error);
                    this.logout();
                }
            );
        });
    }

    logout() {
        let self = this;
        this.deleteToken(this.userName, this.token).then(
            (response) => {
                self._logout();
            },
            (error) => {
                self._logout();
            }
        );
    }

    _logout() {
        this.user = null;
        this.userName = null;
        this.userPassword = null;
        this.token = null;
        this.dispatchEvent(new CustomEvent("logout"));
    }

    forget() {
        cookies.remove("auth");
    }

    isLoggedIn(user) {
        if (user) {
            return (
                this.userName !== null &&
                this.userName.toLowerCase() === user.name.toLowerCase()
            );
        } else {
            return this.userName !== null;
        }
    }

    isCurrentAuthToken(userToken) {
        return userToken.token === this.token;
    }

    _getFullUrl(url) {
        const fullUrl = ("api/" + url).replace(/([^:])\/+/g, "$1/");
        const matches = fullUrl.match(/^([^?]*)\??(.*)$/);
        const baseUrl = matches[1];
        const request = matches[2];
        return [baseUrl, request];
    }

    _getFileId(file) {
        if (file.constructor === String) {
            return file;
        }
        return file.name + file.size;
    }

    _wrappedRequest(url, requestFactory, data, files, options) {
        // transform the request: upload each file, then make the request use
        // its tokens.
        data = Object.assign({}, data);
        let abortFunction = () => {};
        let promise = Promise.resolve();
        if (files) {
            for (let key of Object.keys(files)) {
                const file = files[key];
                const fileId = this._getFileId(file);
                if (fileTokens[fileId]) {
                    data[key + "Token"] = fileTokens[fileId];
                } else {
                    promise = promise
                        .then(() => {
                            let uploadPromise = this._upload(file);
                            abortFunction = () => uploadPromise.abort();
                            return uploadPromise;
                        })
                        .then((token) => {
                            abortFunction = () => {};
                            fileTokens[fileId] = token;
                            data[key + "Token"] = token;
                            return Promise.resolve();
                        });
                }
            }
        }
        promise = promise
            .then(() => {
                let requestPromise = this._rawRequest(
                    url,
                    requestFactory,
                    data,
                    {},
                    options
                );
                abortFunction = () => requestPromise.abort();
                return requestPromise;
            })
            .catch((error) => {
                if (
                    error.response &&
                    error.response.name === "MissingOrExpiredRequiredFileError"
                ) {
                    for (let key of Object.keys(files)) {
                        const file = files[key];
                        const fileId = this._getFileId(file);
                        fileTokens[fileId] = null;
                    }
                    error.message =
                        "The uploaded file has expired; " +
                        "please resend the form to reupload.";
                }
                return Promise.reject(error);
            });
        promise.abort = () => abortFunction();
        return promise;
    }

    _upload(file, options) {
        let abortFunction = () => {};
        let returnedPromise = new Promise((resolve, reject) => {
            let uploadPromise = this._rawRequest(
                "uploads",
                request.post,
                {},
                { content: file },
                options
            );
            abortFunction = () => uploadPromise.abort();
            return uploadPromise.then((response) => {
                abortFunction = () => {};
                return resolve(response.token);
            }, reject);
        });
        returnedPromise.abort = () => abortFunction();
        return returnedPromise;
    }

    _rawRequest(url, requestFactory, data, files, options) {
        options = options || {};
        data = Object.assign({}, data);
        const [fullUrl, query] = this._getFullUrl(url);

        let abortFunction = () => {};
        let returnedPromise = new Promise((resolve, reject) => {
            let req = requestFactory(fullUrl);

            req.set("Accept", "application/json");

            if (query) {
                req.query(query);
            }

            if (files) {
                for (let key of Object.keys(files)) {
                    const value = files[key];
                    if (value.constructor === String) {
                        data[key + "Url"] = value;
                    } else {
                        req.attach(key, value || new Blob());
                    }
                }
            }

            if (data) {
                if (files && Object.keys(files).length) {
                    req.attach("metadata", new Blob([JSON.stringify(data)]));
                } else {
                    req.set("Content-Type", "application/json");
                    req.send(data);
                }
            }

            try {
                if (this.userName && this.token) {
                    req.auth = null;
                    // eslint-disable-next-line no-undef
                    req.set(
                        "Authorization",
                        "Token " +
                            new Buffer(
                                this.userName + ":" + this.token
                            ).toString("base64")
                    );
                } else if (this.userName && this.userPassword) {
                    req.auth(
                        this.userName,
                        encodeURIComponent(this.userPassword).replace(
                            /%([0-9A-F]{2})/g,
                            (match, p1) => {
                                return String.fromCharCode("0x" + p1);
                            }
                        )
                    );
                }
            } catch (e) {
                reject(
                    new Error("Authentication error (malformed credentials)")
                );
            }

            if (!options.noProgress) {
                progress.start();
            }

            abortFunction = () => {
                req.abort(); // does *NOT* call the callback passed in .end()
                progress.done();
                reject(
                    new Error("The request was aborted due to user cancel.")
                );
            };

            req.end((error, response) => {
                progress.done();
                abortFunction = () => {};
                if (error) {
                    if (response && response.body) {
                        error = new Error(
                            response.body.description || "Unknown error"
                        );
                        error.response = response.body;
                    }
                    reject(error);
                } else {
                    resolve(response.body);
                }
            });
        });
        returnedPromise.abort = () => abortFunction();
        return returnedPromise;
    }
}

module.exports = new Api();
