// import { getStores, putStore } from "./lnv";

import { openDB, type DBSchema, type IDBPDatabase } from "idb";
import { authFetch, hasCapability, ping } from "./lnv";
import { LNV_SERVER } from "./hosts";

export interface Store {
	id: string; // UUID
	name: string;
	type: StoreType;
	owner_id: string; // User ID
	data: string; // JSON string
	modified_at: string; // ISO date string
}
export type StoreType = "route" | "location" | "vehicle";
export interface StoreInfo {
	name: string;
	type: StoreType;
}
export interface WrappedValue<T> {
	current: T;
}
export type StoreValue<T> = {
	data: T;
} & Omit<Store, "data">;

interface TCDB extends DBSchema {
	stores: {
		key: string;
		value: Store;
		indexes: {
			"by-name-and-type": [string, StoreType];
			"by-type": StoreType;
			"by-owner": string;
			"by-type-and-owner": [StoreType, string];
		};
	};
	changes: {
		key: string;
		value: {
			id: string; // UUID of store
			operation: "create" | "update" | "delete";
		};
	};
}

export async function getDB(): Promise<IDBPDatabase<TCDB>> {
	if (_db) return _db;
	_db = await openDB<TCDB>("tc", 1, {
		upgrade(db, _oldVersion, _newVersion, _transaction, _event) {
			if (!db.objectStoreNames.contains("stores")) {
				const store = db.createObjectStore("stores", { keyPath: "id" });
				store.createIndex("by-type", "type");
				store.createIndex("by-owner", "owner_id");
				store.createIndex("by-type-and-owner", ["type", "owner_id"]);
				store.createIndex("by-name-and-type", ["name", "type"]);
			}
			if (!db.objectStoreNames.contains("changes")) {
				db.createObjectStore("changes", { keyPath: "id" });
			}
		},
	});
	return _db;
}

let _db: IDBPDatabase<TCDB>;

const eventTarget = new EventTarget();

export async function trySync() {
	const pingResult = await ping();
	if (!pingResult) {
		console.warn(
			"[STORES] [trySync] LNV server is not reachable, skipping sync",
		);
		return { success: false, message: "LNV server is not reachable" };
	}
	await syncStores();
	return { success: true };
}

export async function syncStores() {
	if (!(await hasCapability("stores"))) {
		return;
	}
	if (!localStorage.getItem("lnv-token")) {
		return;
	}
	const db = await getDB();
	const changes = await Promise.all(
		await db.getAll("changes").then((changes) =>
			changes.map(async (change) => {
				const storeData = await db.get("stores", change.id);
				return {
					id: change.id,
					operation: change.operation,
					data: storeData?.data || null,
					modified_at: storeData?.modified_at || new Date().toISOString(),
					type: storeData?.type || "location",
					name: storeData?.name || "",
				};
			}),
		),
	);
	const stores = await db
		.getAll("stores")
		.then((stores) =>
			stores.map((store) => ({ id: store.id, modified_at: store.modified_at })),
		);
	const res = await authFetch(LNV_SERVER + "/stores/sync", {
		method: "POST",
		headers: {
			"Content-Type": "application/json",
		},
		body: JSON.stringify({
			changes,
			stores,
		}),
	});
	if (!res.ok) {
		console.error(
			"[STORES] [syncStores] Failed to sync stores:",
			await res.text(),
		);
		return;
	}
	const data = (await res.json()) as { changes: Store[] };
	const tx = db.transaction(["stores", "changes"], "readwrite");
	// Apply all changes the server sent us
	for (const store of data.changes) {
		if (store.data === null) {
			await tx.objectStore("stores").delete(store.id);
		} else {
			await tx.objectStore("stores").put(store);
		}
		eventTarget.dispatchEvent(
			new CustomEvent("store-updated", { detail: store }),
		);
	}
	// Delete all changes
	await tx.objectStore("changes").clear();
	await tx.done;
}

async function createStore(info: StoreInfo, data: object) {
	const db = await getDB();
	const id = crypto.randomUUID();
	const store: Store = {
		id,
		name: info.name, // TODO: do we need a name?
		type: info.type,
		owner_id: "", // TODO
		data: JSON.stringify(data),
		modified_at: new Date().toISOString(),
	};
	const tx = db.transaction(["stores", "changes"], "readwrite");
	await tx.objectStore("stores").add(store);
	await tx.objectStore("changes").add({ id, operation: "create" });
	await tx.done;
	eventTarget.dispatchEvent(
		new CustomEvent("store-updated", { detail: store }),
	);
	await trySync();
	return store;
}

export async function updateStore(info: StoreInfo, data: object | null) {
	const db = await getDB();
	const store = await db.getFromIndex("stores", "by-name-and-type", [
		info.name,
		info.type,
	]);
	if (!store) {
		if (data === null) {
			console.warn(
				"[STORES] [updateStore] Tried to delete non-existing store",
				info,
			);
			return;
		}
		return await createStore(info, data);
	}
	// Update the store data
	store.data = JSON.stringify(data);
	store.modified_at = new Date().toISOString();
	const tx = db.transaction(["stores", "changes"], "readwrite");
	await tx.objectStore("stores").put(store);
	await tx.objectStore("changes").add({ id: store.id, operation: "update" });
	await tx.done;
	eventTarget.dispatchEvent(
		new CustomEvent("store-updated", { detail: store }),
	);
	await trySync();
	return store;
}

export async function hasStore(info: StoreInfo) {
	const db = await getDB();
	const store = await db.getFromIndex("stores", "by-name-and-type", [
		info.name,
		info.type,
	]);
	return store !== undefined;
}

// export async function store<T extends object>(info: StoreInfo) {
// 	const store = await db.getFromIndex("stores", "by-name-and-type", [info.name, info.type]);
// 	if (!store) {
// 		return null;
// 	}
// 	const state = $state<T>(JSON.parse(store.data) as T);
// 	$effect(() => {
// 		updateStore(info, state);
// 	})
// 	eventTarget.addEventListener("store-updated", (event) => {
// 		const customEvent = event as CustomEvent;
// 		if(customEvent.detail.id === store.id) {
// 			const updatedStore = customEvent.detail as Store;
// 			Object.assign(state, JSON.parse(updatedStore.data));
// 		}
// 	});
// 	return state;
// }

export function stores<T extends object>(
	type: StoreType,
): WrappedValue<StoreValue<T>[]> {
	const state = $state<StoreValue<T>[]>([]);
	eventTarget.addEventListener("store-updated", async (event) => {
		const customEvent = event as CustomEvent;
		const updatedStore = customEvent.detail as Store;
		if (updatedStore.type === type) {
			const db = await getDB();
			const stores = await db.getAllFromIndex("stores", "by-type", type);
			state.splice(
				0,
				state.length,
				...stores
					.map((store) => ({ ...store, data: JSON.parse(store.data) as T }))
					.filter((store) => store.data !== null),
			);
		}
	});
	(async () => {
		const db = await getDB();
		const stores = await db.getAllFromIndex("stores", "by-type", type);
		state.splice(
			0,
			state.length,
			...stores
				.map((store) => ({ ...store, data: JSON.parse(store.data) as T }))
				.filter((store) => store.data !== null),
		);
	})();
	return {
		get current() {
			return state;
		},
		set current(newValue: StoreValue<T>[]) {
			state.splice(0, state.length, ...newValue);
		},
	};
}
