import { batch, createContext, createSignal, JSX } from 'solid-js';
import { createStore } from 'solid-js/store';

import { Line, Lines, Passage, Passages, Stop, StopShape, StopShapes, Stops } from './types';


export type StopDestinations = Record<string, string[]>;

export interface BusinessDataStore {
	getLine: (lineId: string) => Promise<Line>;
	getLinePassages: (lineId: string) => Record<string, Passage[]>;
	getLineDestinations: (lineId: string) => string[];
	getDestinationPassages: (lineId: string, destination: string) => Passage[];

	passages: () => Passages;
	getPassagesLineIds: () => string[];
	refreshPassages: (stopId: number) => Promise<void>;
	addPassages: (passages: Passages) => void;
	clearPassages: () => void;

	getStop: (stopId: number) => Stop | undefined;
	searchStopByName: (name: string) => Promise<Stops>;

	getStopDestinations: (stopId: number) => Promise<StopDestinations | undefined>;
	getStopShape: (stopId: number) => Promise<StopShape | undefined>;
};

export const BusinessDataContext = createContext<BusinessDataStore>();

export function BusinessDataProvider(props: { children: JSX.Element }) {

	const [serverUrl] = createSignal<string>("https://localhost:4443");

	type Store = {
		lines: Lines;
		passages: Passages;
		stops: Stops;
		stopShapes: StopShapes;
	};

	const [store, setStore] = createStore<Store>({ lines: {}, passages: {}, stops: {}, stopShapes: {} });

	const getLine = async (lineId: string): Promise<Line> => {
		let line = store.lines[lineId];
		if (line === undefined) {
			console.log(`${lineId} not found... fetch it from backend.`);

			const response = await fetch(`${serverUrl()}/line/${lineId}`, {
				headers: { 'Content-Type': 'application/json' }
			});

			const json = await response.json();

			if (response.ok) {
				setStore('lines', lineId, json);
				line = json;
			}
			else {
				console.warn(`No line found for ${lineId} line id:`, json);
			}
		}
		return line;
	}

	const getLinePassages = (lineId: string): Record<string, Passage[]> => {
		return store.passages[lineId];
	}

	const getLineDestinations = (lineId: string): string[] => {
		return Object.keys(store.passages[lineId]);
	}

	// TODO: Remove this method: it's based on the next passages and return nothing until the refreshPassages is called.
	const getDestinationPassages = (lineId: string, destination: string): Passage[] => {
		return store.passages[lineId][destination];
	}

	const passages = (): Passages => {
		return store.passages;
	}

	const getPassagesLineIds = (): string[] => {
		return Object.keys(store.passages);
	}

	const _cleanupPassages = (passages: Passages): void => {
		const deadline = Math.floor(Date.now() / 1000) - 60;
		for (const linePassages of Object.values(passages)) {
			for (const destination of Object.keys(linePassages)) {
				const destinationPassages = linePassages[destination];
				const cleaned = [];
				for (const passage of destinationPassages) {
					if (passage.expectedDepartTs > deadline) {
						cleaned.push(passage);
					}
				}
				linePassages[destination] = cleaned;
			}
		}
	}

	const refreshPassages = async (stopId: number): Promise<void> => {
		console.log(`Fetching data for ${stopId}`);
		const httpOptions = { headers: { "Content-Type": "application/json" } };
		const response = await fetch(`${serverUrl()}/stop/${stopId}/nextPassages`, httpOptions);

		const json = await response.json();

		if (response.ok) {
			_cleanupPassages(json.passages);
			addPassages(json.passages);
		}
		else {
			console.warn(`No passage found for ${stopId} stop:`, json);
		}
	}

	const addPassages = (passages: Passages): void => {
		batch(() => {
			const storePassages = store.passages;
			for (const lineId of Object.keys(passages)) {
				const newLinePassages = passages[lineId];
				const linePassages = storePassages[lineId];
				if (linePassages === undefined) {
					setStore('passages', lineId, newLinePassages);
				}
				else {
					for (const destination of Object.keys(newLinePassages)) {
						const newLinePassagesDestination = newLinePassages[destination];
						const linePassagesDestination = linePassages[destination];
						if (linePassagesDestination === undefined) {
							setStore('passages', lineId, destination, newLinePassagesDestination);
						}
						else {
							if (linePassagesDestination.length - newLinePassagesDestination.length != 0) {
								console.log(`Server provides ${newLinePassagesDestination.length} passages, \
${linePassagesDestination.length} here... refresh all them.`);
								setStore('passages', lineId, destination, newLinePassagesDestination);
							}
							else {
								linePassagesDestination.forEach((passage, index) => {
									const newPassage = newLinePassagesDestination[index];
									if (passage.expectedDepartTs != newPassage.expectedDepartTs) {
										console.log(`Refresh expectedDepartTs (${passage.expectedDepartTs} -> ${newPassage.expectedDepartTs}`);
										setStore('passages', lineId, destination, index, 'expectedDepartTs', newPassage.expectedDepartTs);
									}
									if (passage.expectedArrivalTs != newPassage.expectedArrivalTs) {
										console.log(`Refresh expectedArrivalTs (${passage.expectedArrivalTs} -> ${newPassage.expectedArrivalTs}`);
										setStore('passages', lineId, destination, index, 'expectedArrivalTs', newPassage.expectedArrivalTs);
									}
								});
							}
						}
					}
				}
			}
		});
	}

	const clearPassages = (): void => {
		setStore((s: Store): Store => {
			for (const lineId of Object.keys(s.passages)) {
				setStore('passages', lineId, undefined);
			}
			return s;
		});
	}

	const getStop = (stopId: number): Stop | undefined => {
		return store.stops[stopId];
	}

	const searchStopByName = async (name: string): Promise<Stops> => {
		const byIdStops: Stops = {};

		const response = await fetch(`${serverUrl()}/stop/?name=${name}`, {
			headers: { 'Content-Type': 'application/json' }
		});

		const json = await response.json();

		if (response.ok) {
			for (const stop of json) {
				byIdStops[stop.id] = stop;
				setStore('stops', stop.id, stop);

				if (stop.stops !== undefined) {
					for (const innerStop of stop.stops) {
						setStore('stops', innerStop.id, innerStop);
					}
				}
			}
		}
		else {
			console.warn(`No stop found for '${name}' query:`, json);
		}

		return byIdStops;
	}

	const getStopDestinations = async (stopId: number): Promise<StopDestinations | undefined> => {
		const response = await fetch(`${serverUrl()}/stop/${stopId}/destinations`, {
			headers: { 'Content-Type': 'application/json' }
		});
		const destinations = response.ok ? await response.json() : undefined;
		return destinations;
	}

	const getStopShape = async (stopId: number): Promise<StopShape | undefined> => {
		let shape = store.stopShapes[stopId];
		if (shape === undefined) {
			console.log(`No shape found for ${stopId} stop... fetch it from backend.`);
			const response = await fetch(`${serverUrl()}/stop/${stopId}/shape`, {
				headers: { 'Content-Type': 'application/json' }
			});

			const json = await response.json();

			if (response.ok) {
				setStore('stopShapes', stopId, json);
				shape = json;
			}
			else {
				console.warn(`No shape found for ${stopId} stop:`, json);
			}
		}
		return shape;
	}

	return (
		<BusinessDataContext.Provider value={{
			getLine, getLinePassages, getLineDestinations, getDestinationPassages, passages, getPassagesLineIds,
			refreshPassages, addPassages, clearPassages,
			getStop, getStopDestinations, getStopShape, searchStopByName
		}}>
			{props.children}
		</BusinessDataContext.Provider>
	);
}

export interface BusinessDataStore {
	getLine: (lineId: string) => Promise<Line>;
	getLinePassages: (lineId: string) => Record<string, Passage[]>;

	passages: () => Passages;
	refreshPassages: (stopId: number) => Promise<void>;
	addPassages: (passages: Passages) => void;
	clearPassages: () => void;

	getStop: (stopId: number) => Stop | undefined;
	searchStopByName: (name: string) => Promise<Stops>;
};
