// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {
	Amplify,
	ConsoleLogger as Logger,
	Hub,
	parseAWSExports,
} from '@aws-amplify/core';
import { AWSPinpointProvider } from './Providers/AWSPinpointProvider';

import {
	AnalyticsProvider,
	EventAttributes,
	EventMetrics,
	AnalyticsEvent,
	AutoTrackSessionOpts,
	AutoTrackPageViewOpts,
	AutoTrackEventOpts,
	PersonalizeAnalyticsEvent,
	KinesisAnalyticsEvent,
} from './types';
import { PageViewTracker, EventTracker, SessionTracker } from './trackers';

const logger = new Logger('AnalyticsClass');

const AMPLIFY_SYMBOL = (
	typeof Symbol !== 'undefined' && typeof Symbol.for === 'function'
		? Symbol.for('amplify_default')
		: '@@amplify_default'
) as Symbol;

const dispatchAnalyticsEvent = (event: string, data: any, message: string) => {
	Hub.dispatch(
		'analytics',
		{ event, data, message },
		'Analytics',
		AMPLIFY_SYMBOL
	);
};

const trackers = {
	pageView: PageViewTracker,
	event: EventTracker,
	session: SessionTracker,
};

type TrackerTypes = keyof typeof trackers;
type Trackers = typeof trackers[TrackerTypes];
let _instance = null;

/**
 * Provide mobile analytics client functions
 */
export class AnalyticsClass {
	private _config;
	private _pluggables: AnalyticsProvider[];
	private _disabled: boolean;
	private _trackers: Trackers | {};

	/**
	 * Initialize Analtyics
	 * @param config - Configuration of the Analytics
	 */
	constructor() {
		this._config = {};
		this._pluggables = [];
		this._disabled = false;
		this._trackers = {};
		_instance = this;

		this.record = this.record.bind(this);
		Hub.listen('auth', listener);
		Hub.listen('storage', listener);
		Hub.listen('analytics', listener);
		Hub.listen('core', listener);
	}

	public getModuleName() {
		return 'Analytics';
	}
	/**
	 * configure Analytics
	 * @param {Object} config - Configuration of the Analytics
	 */
	public configure(config?) {
		if (!config) return this._config;
		logger.debug('configure Analytics', config);
		const amplifyConfig = parseAWSExports(config);
		this._config = Object.assign(
			{},
			this._config,
			amplifyConfig.Analytics,
			config
		);

		if (this._config['disabled']) {
			this._disabled = true;
		}

		// turn on the autoSessionRecord if not specified
		if (this._config['autoSessionRecord'] === undefined) {
			this._config['autoSessionRecord'] = true;
		}

		this._pluggables.forEach(pluggable => {
			// for backward compatibility
			const providerConfig =
				pluggable.getProviderName() === 'AWSPinpoint' &&
				!this._config['AWSPinpoint']
					? this._config
					: this._config[pluggable.getProviderName()];

			pluggable.configure({
				disabled: this._config['disabled'],
				autoSessionRecord: this._config['autoSessionRecord'],
				...providerConfig,
			});
		});

		if (this._pluggables.length === 0) {
			this.addPluggable(new AWSPinpointProvider());
		}

		dispatchAnalyticsEvent(
			'configured',
			null,
			`The Analytics category has been configured successfully`
		);
		logger.debug('current configuration', this._config);
		return this._config;
	}

	/**
	 * add plugin into Analytics category
	 * @param pluggable - an instance of the plugin
	 */
	public addPluggable(pluggable: AnalyticsProvider) {
		if (pluggable && pluggable.getCategory() === 'Analytics') {
			this._pluggables.push(pluggable);
			// for backward compatibility
			const providerConfig =
				pluggable.getProviderName() === 'AWSPinpoint' &&
				!this._config['AWSPinpoint']
					? this._config
					: this._config[pluggable.getProviderName()];
			const config = { disabled: this._config['disabled'], ...providerConfig };
			pluggable.configure(config);
			return config;
		}
	}

	/**
	 * Get the plugin object
	 * @param providerName - the name of the provider to be removed
	 */
	public getPluggable(providerName: string): AnalyticsProvider {
		for (let i = 0; i < this._pluggables.length; i += 1) {
			const pluggable = this._pluggables[i];
			if (pluggable.getProviderName() === providerName) {
				return pluggable;
			}
		}

		logger.debug('No plugin found with providerName', providerName);
		return null;
	}

	/**
	 * Remove the plugin object
	 * @param providerName - the name of the provider to be removed
	 */
	public removePluggable(providerName: string): void {
		let idx = 0;
		while (idx < this._pluggables.length) {
			if (this._pluggables[idx].getProviderName() === providerName) {
				break;
			}
			idx += 1;
		}

		if (idx === this._pluggables.length) {
			logger.debug('No plugin found with providerName', providerName);
			return;
		} else {
			this._pluggables.splice(idx, idx + 1);
			return;
		}
	}

	/**
	 * stop sending events
	 */
	public disable() {
		this._disabled = true;
	}

	/**
	 * start sending events
	 */
	public enable() {
		this._disabled = false;
	}

	/**
	 * Record Session start
	 * @param [provider] - name of the provider.
	 * @return - A promise which resolves if buffer doesn't overflow
	 */
	public async startSession(provider?: string) {
		const event = { name: '_session.start' };
		const params = { event, provider };

		dispatchAnalyticsEvent(
			'record',
			event,
			'Recording Analytics session start event'
		);

		return this._sendEvent(params);
	}

	/**
	 * Record Session stop
	 * @param [provider] - name of the provider.
	 * @return - A promise which resolves if buffer doesn't overflow
	 */
	public async stopSession(provider?: string) {
		const event = { name: '_session.stop' };
		const params = { event, provider };

		dispatchAnalyticsEvent(
			'record',
			event,
			'Recording Analytics session stop event'
		);

		return this._sendEvent(params);
	}

	/**
	 * Record one analytic event and send it to Pinpoint
	 * @param event - An object with the name of the event, attributes of the event and event metrics.
	 * @param [provider] - name of the provider.
	 */
	public async record(
		event: AnalyticsEvent | PersonalizeAnalyticsEvent | KinesisAnalyticsEvent,
		provider?: string
	) {
		const params = { event, provider };

		dispatchAnalyticsEvent('record', params.event, 'Recording Analytics event');

		return this._sendEvent(params);
	}

	public async updateEndpoint(
		attrs: { [key: string]: any },
		provider?: string
	) {
		const event = { ...attrs, name: '_update_endpoint' };

		return this.record(event, provider);
	}

	private _sendEvent(params: {
		event: AnalyticsEvent | PersonalizeAnalyticsEvent | KinesisAnalyticsEvent;
		provider?: string;
	}) {
		if (this._disabled) {
			logger.debug('Analytics has been disabled');
			return Promise.resolve();
		}

		const provider = params.provider ? params.provider : 'AWSPinpoint';

		return new Promise((resolve, reject) => {
			this._pluggables.forEach(pluggable => {
				if (pluggable.getProviderName() === provider) {
					pluggable.record(params, { resolve, reject });
				}
			});
		});
	}

	/**
	 * Enable or disable auto tracking
	 * @param trackerType - The type of tracker to activate.
	 * @param [opts] - Auto tracking options.
	 */
	public autoTrack(trackerType: 'session', opts: AutoTrackSessionOpts);
	public autoTrack(trackerType: 'pageView', opts: AutoTrackPageViewOpts);
	public autoTrack(trackerType: 'event', opts: AutoTrackEventOpts);
	// ensures backwards compatibility for non-pinpoint provider users
	public autoTrack(
		trackerType: TrackerTypes,
		opts: { provider: string; [key: string]: any }
	);
	public autoTrack(trackerType: TrackerTypes, opts: { [key: string]: any }) {
		if (!trackers[trackerType]) {
			logger.debug('invalid tracker type');
			return;
		}

		// to sync up two different configuration ways of auto session tracking
		if (trackerType === 'session') {
			this._config['autoSessionRecord'] = opts['enable'];
		}

		const tracker = this._trackers[trackerType];
		if (!tracker) {
			this._trackers[trackerType] = new trackers[trackerType](
				this.record,
				opts
			);
		} else {
			tracker.configure(opts);
		}
	}
}

let endpointUpdated = false;
let authConfigured = false;
let analyticsConfigured = false;
let credentialsConfigured = false;

const listener = capsule => {
	const { channel, payload } = capsule;
	logger.debug('on hub capsule ' + channel, payload);

	switch (channel) {
		case 'auth':
			authEvent(payload);
			break;
		case 'storage':
			storageEvent(payload);
			break;
		case 'analytics':
			analyticsEvent(payload);
			break;
		case 'core':
			coreEvent(payload);
			break;
		default:
			break;
	}
};

const storageEvent = payload => {
	const {
		data: { attrs, metrics },
	} = payload;
	if (!attrs) return;

	if (analyticsConfigured) {
		_instance
			.record({
				name: 'Storage',
				attributes: attrs,
				metrics,
			})
			.catch(e => {
				logger.debug('Failed to send the storage event automatically', e);
			});
	}
};

const authEvent = payload => {
	const { event } = payload;
	if (!event) {
		return;
	}

	const recordAuthEvent = async eventName => {
		if (authConfigured && analyticsConfigured) {
			try {
				return await _instance.record({ name: `_userauth.${eventName}` });
			} catch (err) {
				logger.debug(
					`Failed to send the ${eventName} event automatically`,
					err
				);
			}
		}
	};

	switch (event) {
		case 'signIn':
			return recordAuthEvent('sign_in');
		case 'signUp':
			return recordAuthEvent('sign_up');
		case 'signOut':
			return recordAuthEvent('sign_out');
		case 'signIn_failure':
			return recordAuthEvent('auth_fail');
		case 'configured':
			authConfigured = true;
			if (analyticsConfigured) {
				sendEvents();
			}
			break;
	}
};

const analyticsEvent = payload => {
	const { event } = payload;
	if (!event) return;

	switch (event) {
		case 'pinpointProvider_configured':
			analyticsConfigured = true;
			if (authConfigured || credentialsConfigured) {
				sendEvents();
			}
			break;
	}
};

const coreEvent = payload => {
	const { event } = payload;
	if (!event) return;

	switch (event) {
		case 'credentials_configured':
			credentialsConfigured = true;
			if (analyticsConfigured) {
				sendEvents();
			}
			break;
	}
};

const sendEvents = () => {
	const config = _instance.configure();
	if (!endpointUpdated && config['autoSessionRecord']) {
		_instance.updateEndpoint({ immediate: true }).catch(e => {
			logger.debug('Failed to update the endpoint', e);
		});
		endpointUpdated = true;
	}
	_instance.autoTrack('session', {
		enable: config['autoSessionRecord'],
	});
};

export const Analytics = new AnalyticsClass();
Amplify.register(Analytics);
