// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {Platform} from 'react-native';
import {Sentry} from 'react-native-sentry';

import Config from 'assets/config';

import {Client4} from 'mattermost-redux/client';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeam, getCurrentTeamMembership} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';

export const LOGGER_EXTENSION = 'extension';
export const LOGGER_JAVASCRIPT = 'javascript';
export const LOGGER_JAVASCRIPT_WARNING = 'javascript_warning';
export const LOGGER_NATIVE = 'native';
export const LOGGER_REDUX = 'redux';

export const BREADCRUMB_UNCAUGHT_APP_ERROR = 'uncaught-app-error';
export const BREADCRUMB_UNCAUGHT_NON_ERROR = 'uncaught-non-error';

export function initializeSentry() {
    if (!Config.SentryEnabled) {
        // Still allow Sentry to configure itself in case other code tries to call it
        Sentry.config('');

        return;
    }

    const dsn = getDsn();

    if (!dsn) {
        console.warn('Sentry is enabled, but not configured on this platform'); // eslint-disable-line no-console
    }

    Sentry.config(dsn, Config.SentryOptions).install();
}

function getDsn() {
    if (Platform.OS === 'android') {
        return Config.SentryDsnAndroid;
    } else if (Platform.OS === 'ios') {
        return Config.SentryDsnIos;
    }

    return '';
}

export function captureJSException(error, isFatal, store) {
    if (!error || !store) {
        console.warn('captureJSException called with missing arguments', error, store); // eslint-disable-line no-console
        return;
    }

    if (error instanceof Error) {
        captureException(error, LOGGER_JAVASCRIPT, store);
    } else {
        captureNonErrorAsBreadcrumb(error, isFatal);
    }
}

export function captureException(error, logger, store) {
    if (!error || !logger || !store) {
        console.warn('captureException called with missing arguments', error, logger, store); // eslint-disable-line no-console
        return;
    }

    capture(() => {
        Sentry.captureException(error, {logger});
    }, store);
}

export function captureExceptionWithoutState(err, logger) {
    if (!Config.SentryEnabled) {
        return;
    }

    if (err && logger) {
        try {
            Sentry.captureException(err, {logger});
        } catch (error) {
            // do nothing...
        }
    }
}

export function captureMessage(message, logger, store) {
    if (message && logger && store) {
        capture(() => {
            Sentry.captureMessage(message, {logger});
        }, store);
    }
}

export function captureNonErrorAsBreadcrumb(obj, isFatal) {
    if (!obj || typeof obj !== 'object') {
        console.warning('Invalid object passed to captureNonErrorAsBreadcrumb', obj); // eslint-disable-line no-console
        return;
    }

    const isAppError = Boolean(obj.server_error_id);

    const breadcrumb = {
        category: isAppError ? BREADCRUMB_UNCAUGHT_APP_ERROR : BREADCRUMB_UNCAUGHT_NON_ERROR,
        data: {
            isFatal: String(isFatal),
        },
        level: 'warn',
    };

    if (obj.message) {
        breadcrumb.message = obj.message;
    } else if (obj.intl && obj.intl.defaultMessage) {
        breadcrumb.message = obj.intl.defaultMessage;
    } else {
        breadcrumb.message = 'no message provided';
    }

    if (obj.server_error_id) {
        breadcrumb.data.server_error_id = obj.server_error_id;
    }

    if (obj.status_code) {
        breadcrumb.data.status_code = obj.status_code;
    }

    if (obj.url) {
        const match = (/^(?:https?:\/\/)[^/]+(\/.*)$/);

        if (match && match.length >= 2) {
            breadcrumb.data.url = match[1];
        }
    }

    try {
        Sentry.captureBreadcrumb(breadcrumb);
    } catch (e) {
        // Do nothing since this is only here to make sure we don't crash when handling an exception
        console.warn('Failed to capture breadcrumb of non-error', e); // eslint-disable-line no-console
    }
}

// Wrapper function to any calls to Sentry so that we can gather any necessary extra data
// before sending.
function capture(captureFunc, store) {
    if (!Config.SentryEnabled) {
        return;
    }

    try {
        const state = store.getState();
        const config = getConfig(state);

        // Don't contact Sentry if we're connected to a server with diagnostics disabled. Note that this will
        // still log if we're not connected to any server.
        if (config.EnableDiagnostics != null && config.EnableDiagnostics !== 'true') {
            return;
        }

        Sentry.setUserContext(getUserContext(state));
        Sentry.setExtraContext(getExtraContext(state));
        Sentry.setTagsContext(getBuildTags(state));

        console.warn('Capturing with Sentry at ' + getDsn() + '...'); // eslint-disable-line no-console

        captureFunc();
    } catch (e) {
        // Don't want this to get into an infinite loop again...
        console.warn('Exception occured while sending to Sentry'); // eslint-disable-line no-console
        console.warn(e); // eslint-disable-line no-console
    }
}

function getUserContext(state) {
    const currentUser = getCurrentUser(state);

    if (!currentUser) {
        return {};
    }

    return {
        userID: currentUser.id, // This can be changed to id after we upgrade to Sentry >= 0.14.10,
        email: '',
        username: '',
        extra: {
            locale: currentUser.locale,
            roles: currentUser.roles,
        },
    };
}

function getExtraContext(state) {
    const context = {};

    const currentTeam = getCurrentTeam(state);
    if (currentTeam) {
        context.currentTeam = {
            id: currentTeam.id,
        };
    }

    const currentTeamMember = getCurrentTeamMembership(state);
    if (currentTeamMember) {
        context.currentTeamMember = {
            roles: currentTeamMember.roles,
        };
    }

    const currentChannel = getCurrentChannel(state);
    if (currentChannel) {
        context.currentChannel = {
            id: currentChannel.id,
            type: currentChannel.type,
        };
    }

    const currentChannelMember = getMyCurrentChannelMembership(state);
    if (currentChannelMember) {
        context.currentChannelMember = {
            roles: currentChannelMember.roles,
        };
    }

    const config = getConfig(state);
    if (config) {
        context.config = {
            BuildDate: config.BuildDate,
            BuildEnterpriseReady: config.BuildEnterpriseReady,
            BuildHash: config.BuildHash,
            BuildHashEnterprise: config.BuildHashEnterprise,
            BuildNumber: config.BuildNumber,
        };
    }

    return context;
}

function getBuildTags(state) {
    const tags = {};

    const config = getConfig(state);
    if (config) {
        tags.serverBuildHash = config.BuildHash;
        tags.serverBuildNumber = config.BuildNumber;
    }

    return tags;
}

// Given a URL from an API request, return a URL that has any parts removed that are either sensitive or that would
// prevent properly grouping the messages in Sentry.
export function cleanUrlForLogging(original) {
    let url = original;

    // Trim the host name
    url = url.substring(Client4.getUrl().length);

    // Filter the query string
    const index = url.indexOf('?');
    if (index !== -1) {
        url = url.substring(0, index);
    }

    // A non-exhaustive whitelist to exclude parts of the URL that are unimportant (eg IDs) or may be sentsitive
    // (eg email addresses). We prefer filtering out fields that aren't recognized because there should generally
    // be enough left over for debugging.
    //
    // Note that new API routes don't need to be added here since this shouldn't be happening for newly added routes.
    const whitelist = [
        'api', 'v4', 'users', 'teams', 'scheme', 'name', 'members', 'channels', 'posts', 'reactions', 'commands',
        'files', 'preferences', 'hooks', 'incoming', 'outgoing', 'oauth', 'apps', 'emoji', 'brand', 'image',
        'data_retention', 'jobs', 'plugins', 'roles', 'system', 'timezones', 'schemes', 'redirect_location', 'patch',
        'mfa', 'password', 'reset', 'send', 'active', 'verify', 'terms_of_service', 'login', 'logout', 'ids',
        'usernames', 'me', 'username', 'email', 'default', 'sessions', 'revoke', 'all', 'audits', 'device', 'status',
        'search', 'switch', 'authorized', 'authorize', 'deauthorize', 'tokens', 'disable', 'enable', 'exists', 'unread',
        'invite', 'batch', 'stats', 'import', 'schemeRoles', 'direct', 'group', 'convert', 'view', 'search_autocomplete',
        'thread', 'info', 'flagged', 'pinned', 'pin', 'unpin', 'opengraph', 'actions', 'thumbnail', 'preview', 'link',
        'delete', 'logs', 'ping', 'config', 'client', 'license', 'websocket', 'webrtc', 'token', 'regen_token',
        'autocomplete', 'execute', 'regen_secret', 'policy', 'type', 'cancel', 'reload', 'environment', 's3_test', 'file',
        'caches', 'invalidate', 'database', 'recycle', 'compliance', 'reports', 'cluster', 'ldap', 'test', 'sync', 'saml',
        'certificate', 'public', 'private', 'idp', 'elasticsearch', 'purge_indexes', 'analytics', 'old', 'webapp', 'fake',
    ];

    url = url.split('/').map((part) => {
        if (part !== '' && whitelist.indexOf(part) === -1) {
            return '<filtered>';
        }

        return part;
    }).join('/');

    if (index !== -1) {
        // Add this on afterwards since it wouldn't pass the whitelist
        url += '?<filtered>';
    }

    return url;
}
