import makeEmitter from "./core/makeEmitter";
import { STATUS, CATEGORIES, CATEGORIES_VALUES } from "./utils/enums";
import makeCallbackRegistry from "@adobe/callback-registry-factory";
import {
    toArray, areAllValuesTrue, areValidCategories, toObject, clone,
    parse, areValidPermissions, noop, normalize, handleInvalidPermissions
} from "./utils";
import makeStorage from "./core/storage";
import makeCommand from "./core/makeCommand";
import TimeoutError from "./core/TimeoutError";

const REGISTRY_KEY = "fetchPermissions";
const REGISTER_ERROR = "[OptIn#registerPlugin] Plugin is invalid.";

function OptIn(
    {
        doesOptInApply,
        previousPermissions,
        preOptInApprovals,
        isOptInStorageEnabled,
        optInCookieDomain,
        optInStorageExpiry,
        isIabContext
    } = {},
    {
        cookies
    } = {}) {

    const previousPermissionsValue = normalize(previousPermissions);
    // TODO Rethink this whole validation then parsing. Maybe they should be one step.
    handleInvalidPermissions(previousPermissionsValue, "Invalid `previousPermissions`!");
    handleInvalidPermissions(preOptInApprovals, "Invalid `preOptInApprovals`!");

    const storage = makeStorage({
        isEnabled: !!isOptInStorageEnabled,
        cookieName: "adobeujs-optin"
    }, {
        cookies: cookies
    });

    const self = this;
    const publish = makeEmitter(self);
    const registry = makeCallbackRegistry();
    const previousPermissionsObj = parse(previousPermissionsValue);
    const preOptInApprovalsObj = parse(preOptInApprovals);
    const cookieApprovalsObj = storage.get();
    const plugins = {};

    let status = initializeStatus(previousPermissionsObj, cookieApprovalsObj);

    // Format: { [category value]: boolean }
    let permissions = initializePermissions(preOptInApprovalsObj, previousPermissionsObj, cookieApprovalsObj);
    // preCompletePermissions: Used as intermediary permissions between pending and complete statuses.
    let preCompletePermissions = clone(permissions);

    const setStatus = newStatus => status = newStatus;
    const setPermissions = newPermissions => permissions = newPermissions;

    self.deny = updatePermissions(false);
    self.approve = updatePermissions(true);

    self.denyAll = self.deny.bind(self, CATEGORIES_VALUES);
    self.approveAll = self.approve.bind(self, CATEGORIES_VALUES);

    self.isApproved = function (categories) {
        return checkForApproval(categories, self.permissions);
    };

    self.isPreApproved = function (categories) {
        return checkForApproval(categories, preOptInApprovalsObj);
    };

    // Returns a function:
    // - 'shouldAutoSubscribe' = false: noop
    // - 'shouldAutoSubscribe' = true: optIn.off (To unsubscribe)
    self.fetchPermissions = function (callback, shouldAutoSubscribe = false) {
        const result = shouldAutoSubscribe ? self.on(STATUS.COMPLETE, callback) : noop;
        const hasPermissions = !doesOptInApply || (doesOptInApply && self.isComplete) || !!preOptInApprovals;
        // Call the callback if `preOptInApprovals`, even if status `pending`.
        if (hasPermissions) {
            callback(self.permissions);

        } else if (!shouldAutoSubscribe) {
            registry.add(REGISTRY_KEY, () => callback(self.permissions));
        }

        return result;
    };

    self.complete = function () {
        if (self.status === STATUS.CHANGED) {
            completeAndPublish();
        }
    };

    self.registerPlugin = plugin => {
        if (!plugin || !plugin.name || typeof plugin.onRegister !== "function") {
            throw new Error(REGISTER_ERROR);
        }

        if (plugins[plugin.name]) {
            return;
        }

        plugins[plugin.name] = plugin;
        plugin.onRegister.call(plugin, self);
    };

    self.execute = makeCommand(plugins);

    Object.defineProperties(self, {
        "permissions": { get() { return permissions; } },
        "status": { get() { return status; } },
        "Categories": { get() { return CATEGORIES; } },
        "doesOptInApply": { get() { return !!doesOptInApply; } },
        "isPending": { get() { return self.status === STATUS.PENDING; } },
        "isComplete": { get() { return self.status === STATUS.COMPLETE; } },
        "__plugins": { get() { return Object.keys(plugins); } },
        "isIabContext": { get() { return isIabContext; } }
    });

    function checkForApproval(categories, permissionsSource) {
        const categoryList = toArray(categories);
        return !categoryList.length ?
            areAllValuesTrue(permissionsSource) :
            categoryList.every(function (category) {
                return !!permissionsSource[category];
            });
    }

    function initializeStatus(previousPermissionsObj, cookieApprovalsObj) {
        return areValidPermissions(previousPermissionsObj) || 
            (!!cookieApprovalsObj && areValidPermissions(cookieApprovalsObj)) ? 
            STATUS.COMPLETE : STATUS.PENDING;
    }

    // NOTE: previousPermissions will overrwrite pre-opt in an default permissions.
    function initializePermissions(preOptInApprovalsObj, previousPermissionsObj, cookieApprovalsObj) {
        // If `doesOptInApply` is true, default all permissions to false; otherwise default to true.
        let defaultPermissions = toObject(CATEGORIES_VALUES, !doesOptInApply);
        // If `doesOptInApply` is false, always return default permissions. Otherwise assign in priority order
        return !doesOptInApply ?
            defaultPermissions :
            Object.assign({}, defaultPermissions, preOptInApprovalsObj, previousPermissionsObj, cookieApprovalsObj);
    }

    function completeAndPublish() {
        setPermissions(preCompletePermissions);
        setStatus(STATUS.COMPLETE);
        publish(self.status, self.permissions); // TODO Consider publishing a `change` event always.
        storage.set(self.permissions, { optInCookieDomain, optInStorageExpiry });
        registry.execute(REGISTRY_KEY);
    }

    function updatePermissions(isApproved) {
        return function (categories, shouldWaitForComplete) {
            if (!areValidCategories(categories)) {
                throw new Error("[OptIn] Invalid category(-ies). Please use the `OptIn.Categories` enum.");
            }

            setStatus(STATUS.CHANGED);
            Object.assign(preCompletePermissions, toObject(toArray(categories), isApproved));

            if (!shouldWaitForComplete) { // Complete flow.
                completeAndPublish();
            }

            return self;
        };
    }
}

// TODO: Document this enum!
OptIn.Categories = CATEGORIES;
OptIn.TimeoutError = TimeoutError;
export default OptIn;
