import React, {
	createContext,
	useEffect,
	useRef,
	useState,
	FunctionComponent,
} from 'react';

import { Apps } from '../../../app/apps/client/orchestrator';
import { AppEvents } from '../../../app/apps/client/communication';
import { handleAPIError } from './helpers';

type App = {
	id: string;
	name: string;
	status: unknown;
	installed: boolean;
	marketplace: unknown;
	version: unknown;
	marketplaceVersion: unknown;
	bundledIn: unknown;
};

export type AppDataContextValue = {
	data: App[];
	dataCache: any;
	finishedLoading: boolean;
}

export const AppDataContext = createContext<AppDataContextValue>({
	data: [],
	dataCache: [],
	finishedLoading: false,
});

type ListenersMapping = {
	readonly [P in keyof typeof AppEvents]?: (...args: any[]) => void;
};

const registerListeners = (listeners: ListenersMapping): (() => void) => {
	const entries = Object.entries(listeners) as [keyof typeof AppEvents, () => void][];

	for (const [event, callback] of entries) {
		Apps.getWsListener()?.registerListener(AppEvents[event], callback);
	}

	return (): void => {
		for (const [event, callback] of entries) {
			Apps.getWsListener()?.unregisterListener(AppEvents[event], callback);
		}
	};
};

const AppProvider: FunctionComponent = ({ children }) => {
	const [data, setData] = useState<App[]>(() => []);
	const [finishedLoading, setFinishedLoading] = useState<boolean>(() => false);
	const [dataCache, setDataCache] = useState<any>(() => []);

	const ref = useRef(data);
	ref.current = data;

	const invalidateData = (): void => {
		setDataCache(() => []);
	};

	const getDataCopy = (): typeof ref.current => ref.current.slice(0);

	useEffect(() => {
		const updateData = (data: App[]): void => {
			setData(data.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)));
			invalidateData();
		};

		const handleAppAddedOrUpdated = async (appId: string): Promise<void> => {
			try {
				const { status, version } = await Apps.getApp(appId);
				const app = await Apps.getAppFromMarketplace(appId, version);
				const updatedData = getDataCopy();
				const index = updatedData.findIndex(({ id }: {id: string}) => id === appId);
				updatedData[index] = {
					...app,
					installed: true,
					status,
					version,
					marketplaceVersion: app.version,
				};
				setData(updatedData);
				invalidateData();
			} catch (error) {
				handleAPIError(error);
			}
		};

		const listeners = {
			APP_ADDED: handleAppAddedOrUpdated,
			APP_UPDATED: handleAppAddedOrUpdated,
			APP_REMOVED: (appId: string): void => {
				const updatedData = getDataCopy();
				const index = updatedData.findIndex(({ id }: {id: string}) => id === appId);
				if (!updatedData[index]) {
					return;
				}

				if (updatedData[index].marketplace !== false) {
					updatedData.splice(index, 1);
				} else {
					delete updatedData[index].installed;
					delete updatedData[index].status;
					updatedData[index].version = updatedData[index].marketplaceVersion;
				}

				setData(updatedData);
				invalidateData();
			},
			APP_STATUS_CHANGE: ({ appId, status }: {appId: string; status: unknown}): void => {
				const updatedData = getDataCopy();
				const app = updatedData.find(({ id }: {id: string}) => id === appId);

				if (!app) {
					return;
				}
				app.status = status;
				setData(updatedData);
				invalidateData();
			},
			APP_SETTING_UPDATED: (): void => {
				invalidateData();
			},
		};

		const unregisterListeners = registerListeners(listeners);

		(async (): Promise<void> => {
			try {
				const installedApps = await Apps.getApps().then((result) => {
					let apps: App[] = [];
					if (result.length) {
						apps = result.map((current: App) => ({ ...current, installed: true, marketplace: false }));
						updateData(apps);
					}
					return apps;
				}) as App[];

				const marketplaceApps = await Apps.getAppsFromMarketplace() as App[];

				const appsData = marketplaceApps.length ? marketplaceApps.map((app) => {
					const appIndex = installedApps.findIndex(({ id }) => id === app.id);
					if (!installedApps[appIndex]) {
						return {
							...app,
							status: undefined,
							marketplaceVersion: app.version,
							bundledIn: app.bundledIn,
						};
					}

					const installedApp = installedApps.splice(appIndex, 1).pop();
					return {
						...app,
						installed: true,
						status: installedApp?.status,
						version: installedApp?.version,
						bundledIn: app.bundledIn,
						marketplaceVersion: app.version,
					};
				}) : [];

				if (installedApps.length) {
					appsData.push(...installedApps);
				}

				updateData(appsData);
				setFinishedLoading(true);
			} catch (e) {
				handleAPIError(e);
				unregisterListeners();
			}
		})();

		return unregisterListeners;
	}, [setData, setFinishedLoading]);

	return <AppDataContext.Provider children={children} value={{ data, dataCache, finishedLoading }} />;
};

export default AppProvider;
