/**
 * Auth: login & registration workflow.
 * This file contains abstractions over auth concepts.
 * @module soapbox/actions/auth
 * @see module:soapbox/actions/apps
 * @see module:soapbox/actions/oauth
 * @see module:soapbox/actions/security
 */

import { defineMessages } from 'react-intl';

import { createAccount } from 'soapbox/actions/accounts';
import { createApp } from 'soapbox/actions/apps';
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
import { startOnboarding } from 'soapbox/actions/onboarding';
import snackbar from 'soapbox/actions/snackbar';
import { custom } from 'soapbox/custom';
import KVStore from 'soapbox/storage/kv_store';
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
import sourceCode from 'soapbox/utils/code';
import { getFeatures } from 'soapbox/utils/features';
import { isStandalone } from 'soapbox/utils/state';

import api, { baseClient } from '../api';

import { importFetchedAccount } from './importer';

export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';

export const AUTH_APP_CREATED    = 'AUTH_APP_CREATED';
export const AUTH_APP_AUTHORIZED = 'AUTH_APP_AUTHORIZED';
export const AUTH_LOGGED_IN      = 'AUTH_LOGGED_IN';
export const AUTH_LOGGED_OUT     = 'AUTH_LOGGED_OUT';

export const VERIFY_CREDENTIALS_REQUEST = 'VERIFY_CREDENTIALS_REQUEST';
export const VERIFY_CREDENTIALS_SUCCESS = 'VERIFY_CREDENTIALS_SUCCESS';
export const VERIFY_CREDENTIALS_FAIL    = 'VERIFY_CREDENTIALS_FAIL';

export const AUTH_ACCOUNT_REMEMBER_REQUEST = 'AUTH_ACCOUNT_REMEMBER_REQUEST';
export const AUTH_ACCOUNT_REMEMBER_SUCCESS = 'AUTH_ACCOUNT_REMEMBER_SUCCESS';
export const AUTH_ACCOUNT_REMEMBER_FAIL    = 'AUTH_ACCOUNT_REMEMBER_FAIL';

const customApp = custom('app');

export const messages = defineMessages({
  loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
  invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
});

const noOp = () => new Promise(f => f());

const getScopes = state => {
  const instance = state.get('instance');
  const { scopes } = getFeatures(instance);
  return scopes;
};

function createAppAndToken() {
  return (dispatch, getState) => {
    return dispatch(getAuthApp()).then(() => {
      return dispatch(createAppToken());
    });
  };
}

/** Create an auth app, or use it from build config */
function getAuthApp() {
  return (dispatch, getState) => {
    if (customApp?.client_secret) {
      return noOp().then(() => dispatch({ type: AUTH_APP_CREATED, app: customApp }));
    } else {
      return dispatch(createAuthApp());
    }
  };
}

function createAuthApp() {
  return (dispatch, getState) => {
    const params = {
      client_name:   sourceCode.displayName,
      redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
      scopes:        getScopes(getState()),
      website:       sourceCode.homepage,
    };

    return dispatch(createApp(params)).then(app => {
      return dispatch({ type: AUTH_APP_CREATED, app });
    });
  };
}

function createAppToken() {
  return (dispatch, getState) => {
    const app = getState().getIn(['auth', 'app']);

    const params = {
      client_id:     app.get('client_id'),
      client_secret: app.get('client_secret'),
      redirect_uri:  'urn:ietf:wg:oauth:2.0:oob',
      grant_type:    'client_credentials',
      scope:         getScopes(getState()),
    };

    return dispatch(obtainOAuthToken(params)).then(token => {
      return dispatch({ type: AUTH_APP_AUTHORIZED, app, token });
    });
  };
}

function createUserToken(username, password) {
  return (dispatch, getState) => {
    const app = getState().getIn(['auth', 'app']);

    const params = {
      client_id:     app.get('client_id'),
      client_secret: app.get('client_secret'),
      redirect_uri:  'urn:ietf:wg:oauth:2.0:oob',
      grant_type:    'password',
      username:      username,
      password:      password,
      scope:         getScopes(getState()),
    };

    return dispatch(obtainOAuthToken(params))
      .then(token => dispatch(authLoggedIn(token)));
  };
}

export function refreshUserToken() {
  return (dispatch, getState) => {
    const refreshToken = getState().getIn(['auth', 'user', 'refresh_token']);
    const app = getState().getIn(['auth', 'app']);

    if (!refreshToken) return dispatch(noOp);

    const params = {
      client_id:     app.get('client_id'),
      client_secret: app.get('client_secret'),
      refresh_token: refreshToken,
      redirect_uri:  'urn:ietf:wg:oauth:2.0:oob',
      grant_type:    'refresh_token',
      scope:         getScopes(getState()),
    };

    return dispatch(obtainOAuthToken(params))
      .then(token => dispatch(authLoggedIn(token)));
  };
}

export function otpVerify(code, mfa_token) {
  return (dispatch, getState) => {
    const app = getState().getIn(['auth', 'app']);
    return api(getState, 'app').post('/oauth/mfa/challenge', {
      client_id: app.get('client_id'),
      client_secret: app.get('client_secret'),
      mfa_token: mfa_token,
      code: code,
      challenge_type: 'totp',
      redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
      scope: getScopes(getState()),
    }).then(({ data: token }) => dispatch(authLoggedIn(token)));
  };
}

export function verifyCredentials(token, accountUrl) {
  const baseURL = parseBaseURL(accountUrl);

  return (dispatch, getState) => {
    dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token });

    return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => {
      dispatch(importFetchedAccount(account));
      dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account });
      if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account));
      return account;
    }).catch(error => {
      if (error?.response?.status === 403 && error?.response?.data?.id) {
        // The user is waitlisted
        const account = error.response.data;
        dispatch(importFetchedAccount(account));
        dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account });
        if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account));
        return account;
      } else {
        if (getState().get('me') === null) dispatch(fetchMeFail(error));
        dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error, skipAlert: true });
        return error;
      }
    });
  };
}

export function rememberAuthAccount(accountUrl) {
  return (dispatch, getState) => {
    dispatch({ type: AUTH_ACCOUNT_REMEMBER_REQUEST, accountUrl });
    return KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => {
      dispatch(importFetchedAccount(account));
      dispatch({ type: AUTH_ACCOUNT_REMEMBER_SUCCESS, account, accountUrl });
      if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account));
      return account;
    }).catch(error => {
      dispatch({ type: AUTH_ACCOUNT_REMEMBER_FAIL, error, accountUrl, skipAlert: true });
    });
  };
}

export function loadCredentials(token, accountUrl) {
  return (dispatch, getState) => {
    return dispatch(rememberAuthAccount(accountUrl)).finally(() => {
      return dispatch(verifyCredentials(token, accountUrl));
    });
  };
}

export function logIn(intl, username, password) {
  return (dispatch, getState) => {
    return dispatch(getAuthApp()).then(() => {
      return dispatch(createUserToken(username, password));
    }).catch(error => {
      if (error.response.data.error === 'mfa_required') {
        // If MFA is required, throw the error and handle it in the component.
        throw error;
      } else if (error.response.data.error === 'invalid_grant') {
        // Mastodon returns this user-unfriendly error as a catch-all
        // for everything from "bad request" to "wrong password".
        // Assume our code is correct and it's a wrong password.
        dispatch(snackbar.error(intl.formatMessage(messages.invalidCredentials)));
      } else if (error.response.data.error) {
        // If the backend returns an error, display it.
        dispatch(snackbar.error(error.response.data.error));
      } else {
        // Return "wrong password" message.
        dispatch(snackbar.error(intl.formatMessage(messages.invalidCredentials)));
      }
      throw error;
    });
  };
}

export function deleteSession() {
  return (dispatch, getState) => {
    return api(getState).delete('/api/sign_out');
  };
}

export function logOut(intl) {
  return (dispatch, getState) => {
    const state = getState();
    const account = getLoggedInAccount(state);
    const standalone = isStandalone(state);

    if (!account) return dispatch(noOp);

    const params = {
      client_id: state.getIn(['auth', 'app', 'client_id']),
      client_secret: state.getIn(['auth', 'app', 'client_secret']),
      token: state.getIn(['auth', 'users', account.url, 'access_token']),
    };

    return Promise.all([
      dispatch(revokeOAuthToken(params)),
      dispatch(deleteSession()),
    ]).finally(() => {
      dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
      dispatch(snackbar.success(intl.formatMessage(messages.loggedOut)));
    });
  };
}

export function switchAccount(accountId, background = false) {
  return (dispatch, getState) => {
    const account = getState().getIn(['accounts', accountId]);
    dispatch({ type: SWITCH_ACCOUNT, account, background });
  };
}

export function fetchOwnAccounts() {
  return (dispatch, getState) => {
    const state = getState();
    state.getIn(['auth', 'users']).forEach(user => {
      const account = state.getIn(['accounts', user.get('id')]);
      if (!account) {
        dispatch(verifyCredentials(user.get('access_token'), user.get('url')));
      }
    });
  };
}

export function register(params) {
  return (dispatch, getState) => {
    params.fullname = params.username;

    return dispatch(createAppAndToken())
      .then(() => dispatch(createAccount(params)))
      .then(({ token }) => {
        dispatch(startOnboarding());
        return dispatch(authLoggedIn(token));
      });
  };
}

export function fetchCaptcha() {
  return (dispatch, getState) => {
    return api(getState).get('/api/pleroma/captcha');
  };
}

export function authLoggedIn(token) {
  return (dispatch, getState) => {
    dispatch({ type: AUTH_LOGGED_IN, token });
    return token;
  };
}