/*
 * Copyright (c) 2022.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import Vue from 'vue';
import {ActionTree, GetterTree, MutationTree} from "vuex";

import {RootState} from "~/store/index";

import {loadAndApplyContactData} from "~/domains/contact-data/utils";

import {Oauth2TokenResponse} from "@typescript-auth/core";

export type AuthStoreToken = Oauth2TokenResponse & {
    expire_date: string
}

export const AuthStoreKey = {
    user: 'user',
    permissions: 'permissions',
    token: 'token',
    provider: 'provider'
}

export interface AuthState {
    provider: string | undefined,
    user: Record<string, any> | undefined,
    permissions: Record<string, any>[],

    token: AuthStoreToken | undefined,
    tokenPromise: Promise<any> | undefined,

    required: boolean,
    inProgress: boolean,
    error: undefined | { code: string, message: string },
}
const state = () : AuthState => ({
        provider: undefined,
        user: undefined,
        permissions: [],

        token: undefined,
        tokenPromise: undefined,

        required: false,
        inProgress: false,
        error: undefined
});

export const getters : GetterTree<AuthState, RootState> = {
    user: (state: AuthState) => {
        return state.user
    },
    userId: (state: AuthState) => {
        return state.user ? state.user.id : undefined;
    },
    provider: (state: AuthState) => {
        return state.provider;
    },
    permissions: (state: AuthState) => {
        return state.permissions
    },
    permission: (state: AuthState) => (id: number | string) => {
        let items =  state.permissions.filter((item: Record<string, any>) => {
            if(typeof id === 'number') {
                return item.id === id;
            } else {
                return item.name === id;
            }
        });

        return items.length === 1 ? items[0] : undefined;
    },
    loggedIn: (state : AuthState) => {
        return !!state.token;
    },
    token: (state: AuthState) => {
        return state.token;
    }
};

export const actions : ActionTree<AuthState, RootState> = {
    // --------------------------------------------------------------------

    triggerSetLoginRequired({commit}, required: boolean) {
        commit('setLoginRequired', required);
    },
    // --------------------------------------------------------------------

    triggerSetProvider({commit}, provider) {
        this.$authWarehouse.set(AuthStoreKey.provider, provider);

        commit('setProvider', provider);
    },
    triggerUnsetProvider({commit}) {
        this.$authWarehouse.remove(AuthStoreKey.provider);

        commit('unsetProvider');
    },

    // --------------------------------------------------------------------

    triggerSetToken({commit}, token) {
        this.$authWarehouse.set(AuthStoreKey.token, token);

        if(typeof token === 'object' && token.hasOwnProperty('accessToken')) {
            this.$auth.setRequestToken(token.accessToken);
        }

        commit('setToken', token);
    },
    triggerUnsetToken({commit}) {
        this.$authWarehouse.remove(AuthStoreKey.token);
        this.$auth.unsetRequestToken();

        commit('unsetToken');
    },

    // --------------------------------------------------------------------

    triggerSetUser({commit}, user) {
        if(typeof user.avatarMeta !== 'undefined') delete user.avatarMeta;
        if(typeof user.coverMeta !== 'undefined') delete user.coverMeta;

        this.$authWarehouse.set(AuthStoreKey.user, user);
        commit('setUser', user);
    },
    triggerUnsetUser({commit}) {
        this.$authWarehouse.remove(AuthStoreKey.user);
        commit('unsetUser');
    },

    // --------------------------------------------------------------------

    triggerSetPermissions({commit}, permissions) {
        this.$authWarehouse.setLocalStorageItem(AuthStoreKey.permissions, permissions);
        commit('setPermissions', permissions);
    },
    triggerUnsetPermissions({commit}) {
        this.$authWarehouse.removeLocalStorageItem(AuthStoreKey.permissions);
        commit('unsetPermissions');
    },

    // --------------------------------------------------------------------

    /**
     * Try to trigger user refresh.
     *
     * @param state
     * @param dispatch
     *
     * @returns {Promise<boolean>}
     */
    async triggerRefreshMe ({ state, dispatch }) {
        if (state.token) {
            try {
                const { permissions, ...user } = await this.$auth.getUserInfo(state.token.access_token);

                await loadAndApplyContactData(user.id);

                dispatch('triggerUnsetUser');

                dispatch('triggerSetUser', user);
                dispatch('triggerSetPermissions', permissions);
            } catch (e) {
                dispatch('triggerLogout');
                throw e;
            }
        }
    },
    // --------------------------------------------------------------------

    /**
     * Try to login the user with given credentials.
     *
     * @return {Promise<boolean>}
     */
    async triggerLogin ({ commit, dispatch }, credentials: {[key: string] : any}) {
        commit('loginRequest');

        try {
            const token = await this.$auth.getTokenWithPassword(credentials.name, credentials.password);
            commit('loginSuccess');

            dispatch('triggerSetToken', token);

            await dispatch('triggerRefreshMe');
            this.dispatch('layout/update');
        } catch (e) {
            dispatch('triggerUnsetToken');

            throw e;
        }
    },

    // --------------------------------------------------------------------

    triggerRefreshToken ({ commit, state, dispatch }) {
        if(
            typeof state.provider === 'undefined' ||
            typeof state.token === 'undefined' ||
            typeof state.token.refresh_token === 'undefined'
        ) {
            throw new Error('It is not possible to receive a new access token');
        }

        if (!state.tokenPromise) {
            commit('loginRequest');

            try {
                const p = this.$auth.getTokenWithRefreshToken(
                    state.token.refresh_token
                );

                commit('setTokenPromise', p);

                p.then(
                    (token) => {
                        commit('setTokenPromise', null);
                        commit('loginSuccess');

                        dispatch('triggerSetToken', token);
                        dispatch('triggerRefreshMe');
                    },
                    () => {
                        commit('setTokenPromise', null)
                    }
                )
            } catch (e) {
                commit('setTokenPromise', null);
                dispatch('triggerAuthError', e.message);

                throw new Error('An error occured on the token refresh request.');
            }
        }

        return state.tokenPromise
    },

    // --------------------------------------------------------------------

    async triggerTokenExpired({dispatch, commit}) {
        try {
            await dispatch('triggerRefreshToken');
        } catch (e) {
            dispatch('triggerSetLoginRequired', true);
        }
    },

    // --------------------------------------------------------------------

    /**
     * Try to logout the user.
     * @param commit
     */
    triggerLogout ({ dispatch }) {
        dispatch('triggerUnsetToken');
        dispatch('triggerUnsetUser');
        dispatch('triggerUnsetPermissions');

        dispatch('triggerSetLoginRequired', false);

        this.dispatch('layout/update');
    },

    // --------------------------------------------------------------------

    /**
     * Trigger custom authentication error by
     * another service or component.
     *
     * @param commit
     * @param message
     */
    triggerAuthError ({ commit }, message) {
        commit('loginError', { errorCode: 'internal', errorMessage: message })
    },

    //--------------------------------------------------------

    /**
     * Trigger user property change.
     *
     * @param commit
     * @param property
     * @param value
     */
    triggerSetUserProperty ({ commit, state }, {property, value}) {
        commit('setUserProperty', { property, value});
        this.$authWarehouse.remove(AuthStoreKey.user);
        this.$authWarehouse.set(AuthStoreKey.user, state.user);
    }
};

export const mutations : MutationTree<AuthState> = {
    // Login mutations
    loginRequest (state) {
        state.inProgress = true;

        state.error = undefined
    },
    loginSuccess (state) {
        state.inProgress = false;
        state.required = false;
    },
    loginError (state, { errorCode, errorMessage }) {
        state.inProgress = false;

        state.error = {
            code: errorCode,
            message: errorMessage
        }
    },
    setLoginRequired (state, required) {
        state.required = required;
    },

    // --------------------------------------------------------------------

    setProvider(state,provider) {
        state.provider = provider;
    },

    // --------------------------------------------------------------------

    setTokenPromise (state, promise) {
        state.tokenPromise = promise;
    },

    // --------------------------------------------------------------------

    setUser (state, user) {
        state.user = user;
    },
    unsetUser(state) {
        state.user = undefined;
    },

    // --------------------------------------------------------------------

    setUserProperty(state, {property, value}) {
        if(typeof state.user === 'undefined') return;

        Vue.set(state.user, property, value);
    },

    // --------------------------------------------------------------------

    setPermissions (state, permissions) {
        state.permissions = permissions;
    },
    unsetPermissions(state) {
        state.permissions = [];
    },

    // --------------------------------------------------------------------
    setToken (state, token) {
        state.token = token;
    },
    unsetToken(state) {
        state.token = undefined;
    }
};

export default {
    namespaced: true,
    state,
    getters,
    actions,
    mutations
}
