import { fromJS, Set, List } from 'immutable';
import {
    call, put, select, fork, take, takeLatest,
} from 'redux-saga/effects';
import { api } from '@hiveio/hive-js';
import { PrivateKey, Signature, hash } from '@hiveio/hive-js/lib/auth/ecc';

import { accountAuthLookup } from 'app/redux/AuthSaga';
import { getAccount } from 'app/redux/SagaShared';
import * as userActions from 'app/redux/UserReducer';
import { packLoginData, extractLoginData } from 'app/utils/UserUtil';
import { browserHistory } from 'react-router';
import {
    serverApiLogin,
    serverApiLogout,
    serverApiRecordEvent,
    isTosAccepted,
    acceptTos,
} from 'app/utils/ServerApiClient';
import { loadFollows } from 'app/redux/FollowSaga';
import { setHiveSignerAccessToken } from 'app/utils/HiveSigner';
import HiveAuthUtils from 'app/utils/HiveAuthUtils';

// eslint-disable-next-line import/prefer-default-export
export const userWatches = [
    // keep first to remove keys early when a page change happens
    takeLatest('@@router/LOCATION_CHANGE', removeHighSecurityKeys),
    takeLatest('user/lookupPreviousOwnerAuthority', lookupPreviousOwnerAuthority),
    // takeLeading https://redux-saga.js.org/docs/api/#notes-5
    fork(function* () {
        while (true) {
            const action = yield take(userActions.USERNAME_PASSWORD_LOGIN);
            yield call(usernamePasswordLogin, action);
        }
    }),
    takeLatest(userActions.SAVE_LOGIN, saveLogin_localStorage),
    takeLatest(userActions.LOGOUT, logout),
    takeLatest(userActions.GET_VESTING_DELEGATIONS, getVestingDelegationsSaga),
    takeLatest(userActions.LOGIN_ERROR, loginError),
    takeLatest(userActions.LOAD_SAVINGS_WITHDRAW, loadSavingsWithdraw),
    takeLatest(userActions.ACCEPT_TERMS, function* () {
        try {
            yield call(acceptTos);
        } catch (e) {
            // TODO: log error to server, conveyor is unavailable
        }
    }),
    function* getLatestFeedPrice() {
        try {
            const history = yield call([api, api.getFeedHistoryAsync]);
            const feed = history.price_history;
            const last = fromJS(feed[feed.length - 1]);
            yield put(userActions.setLatestFeedPrice(last));
        } catch (error) {
            // (exceedingly rare) ignore, UI will fall back to feed_price
        }
    },
];

const highSecurityPages = [
    /\/market/,
    /\/@.+\/(transfers|permissions|password|communities|delegations)/,
    /\/~witnesses/,
    /\/proposals/,
];

function* getVestingDelegationsSaga(action) {
    console.log(action);
    try {
        yield call(
            [api, api.getVestingDelegations],
            action.payload.account,
            '',
            100,
            action.payload.successCallback
        );
    } catch (error) {
        // Nothing
    }
}

function* loadSavingsWithdraw() {
    const username = yield select((state) => {
        return state.user.getIn(['current', 'username']);
    });
    const to = yield call([api, api.getSavingsWithdrawToAsync], username);
    const fro = yield call([api, api.getSavingsWithdrawFromAsync], username);

    const m = {};
    for (const v of to) m[v.id] = v;
    for (const v of fro) m[v.id] = v;

    const withdraws = List(fromJS(m).values()).sort((a, b) => {
        return strCmp(a.get('complete'), b.get('complete'));
    });

    yield put(
        userActions.set({
            key: 'savings_withdraws',
            value: withdraws,
        })
    );
}

const strCmp = (a, b) => (a > b ? 1 : a < b ? -1 : 0);

function* removeHighSecurityKeys({ payload: { pathname } }) {
    const highSecurityPage = highSecurityPages.find((p) => p.test(pathname)) != null;
    // Let the user keep the active key when going from one high security page to another.  This helps when
    // the user logins into the Wallet then the Permissions tab appears (it was hidden).  This keeps them
    // from getting logged out when they click on Permissions (which is really bad because that tab
    // disappears again).
    if (!highSecurityPage) yield put(userActions.removeHighSecurityKeys());
}

const clean = (value) => {
    return value == null || value === '' || /null|undefined/.test(value)
        ? undefined
        : value;
};

/**
 * @arg {object} action.username - Unless a WIF is provided, this is hashed with the password and key_type to create private keys.
 * @arg {object} action.password - Password or WIF private key.  A WIF becomes the posting key, a password can create all three
 * key_types: active, owner, posting keys.
*/
function* usernamePasswordLogin({ payload }) {
    let {
        username,
        password,
        access_token,
        expires_in,
        hiveauth_key,
        hiveauth_token,
        hiveauth_token_expires,
    } = payload;

    const {
        useKeychain,
        useHiveSigner,
        useHiveAuth,
        lastPath,
        saveLogin,
        operationType /*high security*/,
    } = payload;

    const current = yield select((state) => state.user.get('current'));
    if (current) {
        const currentUsername = current.get('username');
        yield fork(loadFollows, currentUsername, 'blog');
        yield fork(loadFollows, currentUsername, 'ignore');
    }

    const user = yield select((state) => state.user);
    const loginType = user.get('login_type');
    const justLoggedIn = loginType === 'basic';
    console.log(
        'Login type:',
        loginType,
        'Operation type:',
        operationType,
        'Just logged in?',
        justLoggedIn,
        'username:',
        username
    );

    // login, using saved password
    let autopost,
        memoWif,
        login_owner_pubkey,
        login_with_keychain,
        login_with_hivesigner,
        login_with_hiveauth;
    if (!username && !password) {
        const data = localStorage.getItem('autopost2');
        if (data) {
            // auto-login with a low security key (like a posting key)
            autopost = true; // must use simi-colon
            // The 'password' in this case must be the posting private wif .. See setItme('autopost')
            [
                username,
                password,
                memoWif,
                login_owner_pubkey,
                login_with_keychain,
                login_with_hivesigner,
                access_token,
                expires_in,
                login_with_hiveauth,
                hiveauth_key,
                hiveauth_token,
                hiveauth_token_expires,
            ] = extractLoginData(data);
            memoWif = clean(memoWif);
            login_owner_pubkey = clean(login_owner_pubkey);
        }
    }
    // no saved password
    if (
        !username
        || !(
            password
            || useKeychain
            || login_with_keychain
            || useHiveAuth
            || login_with_hiveauth
            || useHiveSigner
            || login_with_hivesigner
        )
    ) {
        console.log('No saved password');
        const offchain_account = yield select((state) => {
            return state.offchain.get('account');
        });
        if (offchain_account) {
            serverApiLogout();
        }
        return;
    }

    let userProvidedRole; // login via:  username/owner
    if (username.indexOf('/') > -1) {
        // "alice/active" will login only with Alices active key
        [username, userProvidedRole] = username.split('/');
    }

    const isRole = (role, fn) => {
        return !userProvidedRole || role === userProvidedRole ? fn() : undefined;
    };

    const account = yield call(getAccount, username);
    if (!account) {
        console.log('No account');
        yield put(userActions.loginError({ error: 'Username does not exist' }));
        return;
    }

    // return if already logged in using hive keychain
    if (login_with_keychain) {
        console.log('Logged in using hive keychain');
        yield put(
            userActions.setUser({
                username,
                login_with_keychain: true,
                vesting_shares: account.get('vesting_shares'),
                received_vesting_shares: account.get('received_vesting_shares'),
                delegated_vesting_shares: account.get(
                    'delegated_vesting_shares'
                ),
            })
        );
        return;
    }

    // return if already logged in using HiveAuth
    if (login_with_hiveauth) {
        const now = new Date().getTime();
        const isTokenValid = now < hiveauth_token_expires;

        if (
            hiveauth_key
            && hiveauth_token
            && isTokenValid
        ) {
            console.log('Logged in using HiveAuth');
            HiveAuthUtils.setUsername(username);
            HiveAuthUtils.setKey(hiveauth_key);
            HiveAuthUtils.setToken(hiveauth_token);
            HiveAuthUtils.setExpire(hiveauth_token_expires);
            yield put(
                userActions.setUser({
                    username,
                    login_with_hiveauth: true,
                    vesting_shares: account.get('vesting_shares'),
                    received_vesting_shares: account.get('received_vesting_shares'),
                    delegated_vesting_shares: account.get(
                        'delegated_vesting_shares'
                    ),
                })
            );
        } else {
            console.log('HiveAuth token has expired');
            HiveAuthUtils.logout();
            yield put(
                userActions.logout({ type: 'default' })
            );
        }

        return;
    }

    // return if already logged in using HiveSigner
    if (login_with_hivesigner) {
        console.log('Logged in using HiveSigner');
        if (access_token) {
            setHiveSignerAccessToken(username, access_token, expires_in);
            yield put(
                userActions.setUser({
                    username,
                    login_with_hivesigner: true,
                    access_token,
                    expires_in,
                    vesting_shares: account.get('vesting_shares'),
                    received_vesting_shares: account.get(
                        'received_vesting_shares'
                    ),
                    delegated_vesting_shares: account.get(
                        'delegated_vesting_shares'
                    ),
                })
            );
        }
        return;
    }

    let private_keys;
    if (!useKeychain && !useHiveSigner && !useHiveAuth) {
        try {
            const private_key = PrivateKey.fromWif(password);
            private_keys = fromJS({
                posting_private: isRole('posting', () => private_key),
                active_private: isRole('active', () => private_key),
                owner_private: isRole('owner', () => private_key),
                memo_private: private_key,
            });
        } catch (e) {
            // Password (non wif)
            login_owner_pubkey = PrivateKey.fromSeed(
                username + 'owner' + password
            )
                .toPublicKey()
                .toString();
            private_keys = fromJS({
                posting_private: isRole('posting', () => {
                    return PrivateKey.fromSeed(username + 'posting' + password);
                }),
                active_private: isRole('active', () => {
                    return PrivateKey.fromSeed(username + 'active' + password);
                }),
                owner_private: isRole('owner', () => {
                    return PrivateKey.fromSeed(username + 'owner' + password);
                }),
                memo_private: PrivateKey.fromSeed(username + 'memo' + password),
            });
        }

        if (memoWif) {
            private_keys = private_keys.set(
                'memo_private',
                PrivateKey.fromWif(memoWif)
            );
        }

        yield call(accountAuthLookup, {
            payload: {
                account,
                private_keys,
                login_owner_pubkey,
            },
        });
        const authority = yield select((state) => {
            return state.user.getIn(['authority', username]);
        });

        const fullAuths = authority.reduce(
            (r, auth, type) => (auth === 'full' ? r.add(type) : r),
            Set()
        );
        if (!fullAuths.size) {
            console.log('No full auths');
            localStorage.removeItem('autopost2');
            const generated_type = password[0] === 'P' && password.length > 40;
            const owner_pub_key = account.getIn(['owner', 'key_auths', 0, 0]);
            serverApiRecordEvent(
                'login_attempt',
                JSON.stringify({
                    name: username,
                    login_owner_pubkey,
                    owner_pub_key,
                    generated_type,
                })
            );
            yield put(userActions.loginError({ error: 'Incorrect Password' }));
            return;
        }

        if (authority.get('posting') !== 'full') {
            private_keys = private_keys.remove('posting_private');
        }
        if (authority.get('active') !== 'full') {
            private_keys = private_keys.remove('active_private');
        }
        if (authority.get('owner') !== 'full') {
            private_keys = private_keys.remove('owner_private');
        }
        if (authority.get('memo') !== 'full') {
            private_keys = private_keys.remove('memo_private');
        }

        // If user is signing operation by operaion and has no saved login, don't save to RAM
        if (!operationType || saveLogin) {
            // Keep the posting key in RAM but only when not signing an operation.
            // No operation or the user has checked: Keep me logged in...
            yield put(
                userActions.setUser({
                    username,
                    private_keys,
                    login_owner_pubkey,
                    vesting_shares: account.get('vesting_shares'),
                    received_vesting_shares: account.get(
                        'received_vesting_shares'
                    ),
                    delegated_vesting_shares: account.get(
                        'delegated_vesting_shares'
                    ),
                })
            );
        } else {
            yield put(
                userActions.setUser({
                    username,
                    vesting_shares: account.get('vesting_shares'),
                    received_vesting_shares: account.get(
                        'received_vesting_shares'
                    ),
                    delegated_vesting_shares: account.get(
                        'delegated_vesting_shares'
                    ),
                })
            );
        }
    }

    try {
        // const challengeString = yield serverApiLoginChallenge()
        const offchainData = yield select((state) => state.offchain);
        const serverAccount = offchainData.get('account');
        const challengeString = offchainData.get('login_challenge');
        if (!serverAccount && challengeString) {
            console.log('No server account, but challenge string');
            const signatures = {};
            const challenge = { token: challengeString };
            const buf = JSON.stringify(challenge, null, 0);
            const bufSha = hash.sha256(buf);
            if (useKeychain) {
                const response = yield new Promise((resolve) => {
                    window.hive_keychain.requestSignBuffer(
                        username,
                        buf,
                        'Posting',
                        (_response) => {
                            resolve(_response);
                        }
                    );
                });
                if (response.success) {
                    signatures.posting = response.result;
                } else {
                    yield put(
                        userActions.loginError({ error: response.message })
                    );
                    return;
                }
                yield put(
                    userActions.setUser({
                        username,
                        login_with_keychain: true,
                        vesting_shares: account.get('vesting_shares'),
                        received_vesting_shares: account.get(
                            'received_vesting_shares'
                        ),
                        delegated_vesting_shares: account.get(
                            'delegated_vesting_shares'
                        ),
                    })
                );
            } else if (useHiveAuth) {
                const authResponse = yield new Promise((resolve) => {
                    HiveAuthUtils.login(username, buf, (res) => {
                        resolve(res);
                    });
                });

                if (authResponse.success) {
                    const {
                        token, expire, key, challengeHex,
                    } = authResponse.hiveAuthData;
                    signatures.posting = challengeHex;

                    yield put(
                        userActions.setUser({
                            username,
                            login_with_hiveauth: true,
                            hiveauth_key: key,
                            hiveauth_token: token,
                            hiveauth_token_expires: expire,
                            vesting_shares: account.get('vesting_shares'),
                            received_vesting_shares: account.get(
                                'received_vesting_shares'
                            ),
                            delegated_vesting_shares: account.get(
                                'delegated_vesting_shares'
                            ),
                        })
                    );
                } else {
                    yield put(userActions.loginError({
                        error: authResponse.error,
                    }));
                    return;
                }
            } else if (useHiveSigner) {
                if (access_token) {
                    // set access setHiveSignerAccessToken
                    setHiveSignerAccessToken(
                        username,
                        access_token,
                        expires_in
                    );
                    // set user data
                    yield put(
                        userActions.setUser({
                            username,
                            login_with_hivesigner: true,
                            access_token,
                            expires_in,
                            vesting_shares: account.get('vesting_shares'),
                            received_vesting_shares: account.get(
                                'received_vesting_shares'
                            ),
                            delegated_vesting_shares: account.get(
                                'delegated_vesting_shares'
                            ),
                        })
                    );
                }
            } else {
                const sign = (role, d) => {
                    console.log('Sign before');
                    if (!d) return;
                    console.log('Sign after');
                    const sig = Signature.signBufferSha256(bufSha, d);
                    signatures[role] = sig.toHex();
                };
                sign('posting', private_keys.get('posting_private'));
                // sign('active', private_keys.get('active_private'))
            }

            console.log('Logging in as', username);
            const response = yield serverApiLogin(username, signatures);
            yield response.json();
        }
    } catch (error) {
        // Does not need to be fatal
        console.error('Server Login Error', error);
    }

    if (!autopost && saveLogin) yield put(userActions.saveLogin());

    /*
    // Feature Flags
    if (useKeychain || private_keys.get('posting_private')) {
        yield fork(
            getFeatureFlags,
            username,
            useKeychain ? null : private_keys.get('posting_private').toString()
        );
    }
    */

    // TOS acceptance
    yield fork(promptTosAcceptance, username);

    // Redirect user to the appropriate page after login.
    const path = useHiveSigner ? lastPath : document.location.pathname;
    if (path === '/login.html' || path === '/') {
        browserHistory.push(`/@${username}/transfers`);
    } else if (useHiveSigner && lastPath) {
        browserHistory.push(lastPath);
    }
}

function* promptTosAcceptance(username) {
    try {
        const accepted = yield call(isTosAccepted, username);
        if (!accepted) {
            yield put(userActions.showTerms());
        }
    } catch (e) {
        // TODO: log error to server, conveyor is unavailable
    }
}

function* saveLogin_localStorage() {
    if (!process.env.BROWSER) {
        console.error('Non-browser environment, skipping localstorage');
        return;
    }
    localStorage.removeItem('autopost2');
    const [
        username,
        private_keys,
        login_owner_pubkey,
        login_with_keychain,
        login_with_hivesigner,
        access_token,
        expires_in,
        login_with_hiveauth,
        hiveauth_key,
        hiveauth_token,
        hiveauth_token_expires,
    ] = yield select((state) => [
        state.user.getIn(['current', 'username']),
        state.user.getIn(['current', 'private_keys']),
        state.user.getIn(['current', 'login_owner_pubkey']),
        state.user.getIn(['current', 'login_with_keychain']),
        state.user.getIn(['current', 'login_with_hivesigner']),
        state.user.getIn(['current', 'access_token']),
        state.user.getIn(['current', 'expires_in']),
        state.user.getIn(['current', 'login_with_hiveauth']),
        state.user.getIn(['current', 'hiveauth_key']),
        state.user.getIn(['current', 'hiveauth_token']),
        state.user.getIn(['current', 'hiveauth_token_expires']),
    ]);
    if (!username) {
        console.error('Not logged in');
        return;
    }
    // Save the lowest security key
    const posting_private = private_keys && private_keys.get('posting_private');
    if (!login_with_keychain && !login_with_hivesigner && !login_with_hiveauth && !posting_private) {
        console.error('No posting key to save?');
        return;
    }
    const account = yield select((state) => {
        return state.global.getIn(['accounts', username]);
    });
    if (!account) {
        console.error('Missing global.accounts[' + username + ']');
        return;
    }
    const postingPubkey = posting_private
        ? posting_private.toPublicKey().toString()
        : 'none';
    try {
        account.getIn(['active', 'key_auths']).forEach((auth) => {
            if (auth.get(0) === postingPubkey) {
                throw 'Login will not be saved, posting key is the same as active key';
            }
        });
        account.getIn(['owner', 'key_auths']).forEach((auth) => {
            if (auth.get(0) === postingPubkey) {
                throw 'Login will not be saved, posting key is the same as owner key';
            }
        });
    } catch (e) {
        console.error(e);
        return;
    }

    const memoKey = private_keys ? private_keys.get('memo_private') : null;
    const memoWif = memoKey && memoKey.toWif();
    const postingPrivateWif = posting_private
        ? posting_private.toWif()
        : 'none';
    const data = packLoginData(
        username,
        postingPrivateWif,
        memoWif,
        login_owner_pubkey,
        login_with_keychain,
        login_with_hivesigner,
        access_token,
        expires_in,
        login_with_hiveauth,
        hiveauth_key,
        hiveauth_token,
        hiveauth_token_expires,
    );
    // autopost is a auto login for a low security key (like the posting key)
    localStorage.setItem('autopost2', data);
}

function* logout(action) {
    const payload = (action || {}).payload || {};
    const logoutType = payload.type || 'default';
    // eslint-disable-next-line prefer-rest-params
    console.log('Logging out', arguments, 'logout type', logoutType);

    // Just in case it is still showing
    yield put(userActions.saveLoginConfirm(false));

    if (process.env.BROWSER) {
        sessionStorage.removeItem('username');
        localStorage.removeItem('autopost2');
    }

    HiveAuthUtils.logout();
    yield serverApiLogout();
}

// eslint-disable-next-line require-yield
function* loginError({
                         // eslint-disable-next-line no-empty-pattern
                         payload: {
                             /*error*/
                         },
                     }) {
    serverApiLogout();
}

/**
 * If the owner key was changed after the login owner key,
 * this function will find the next owner key history record after the change
 * and store it under user.previous_owner_authority.
 */
function* lookupPreviousOwnerAuthority() {
    const current = yield select((state) => state.user.getIn(['current']));
    if (!current) return;

    const login_owner_pubkey = current.get('login_owner_pubkey');
    if (!login_owner_pubkey) return;

    const username = current.get('username');
    const key_auths = yield select((state) => {
        state.global.getIn(['accounts', username, 'owner', 'key_auths']);
    });
    if (
        key_auths
        && key_auths.find((key) => key.get(0) === login_owner_pubkey)
    ) {
        // console.log('UserSaga ---> Login matches current account owner');
        return;
    }
    // Owner history since this index was installed July 14
    let owner_history = fromJS(
        yield call([api, api.getOwnerHistoryAsync], username)
    );
    if (owner_history.count() === 0) return;
    owner_history = owner_history.sort((b, a) => {
        //sort decending
        const aa = a.get('last_valid_time');
        const bb = b.get('last_valid_time');
        return aa < bb ? -1 : aa > bb ? 1 : 0;
    });
    // console.log('UserSaga ---> owner_history', owner_history.toJS())
    const previous_owner_authority = owner_history.find((o) => {
        const auth = o.get('previous_owner_authority');
        const weight_threshold = auth.get('weight_threshold');
        const key3 = auth
            .get('key_auths')
            .find((key2) => {
                return key2.get(0) === login_owner_pubkey && key2.get(1) >= weight_threshold;
            });
        return key3 ? auth : null;
    });
    if (!previous_owner_authority) {
        console.log('UserSaga ---> Login owner does not match owner history');
        return;
    }
    // console.log('UserSage ---> previous_owner_authority', previous_owner_authority.toJS())
    yield put(userActions.setUser({ previous_owner_authority }));
}
