import {collection, getDocs, limit, orderBy, query, QueryConstraint, where} from "firebase/firestore";
import {getDownloadURL, listAll, ref, StorageReference} from "firebase/storage";
import {SetStateAction} from "react";
import {BillingPackage, CohortData, InstituteData, NotificationObject, PlacementListing, ProviderData, UserData, UserGroupData} from "../typeDefinitions";
import {db, storage} from "./firebaseConfig";
import FirebaseQuery from "./firebaseQuery";
import {pathToArr, sortByReverseStringLength} from "./util";

const firebaseQuery = new FirebaseQuery();

export const getStaffRolesForStudent = async (studentFields:string[], product:string, userObject:UserData) => {
    const roleWithStudent = await firebaseQuery.getDocsWhere(
        ["staffRoles"],
        where("filters.students", "array-contains", userObject.id));

    const roleWithFilters = await studentFields.reduce(async (acc, field) => {
        if (!userObject.details[field as keyof typeof userObject]) {
            return acc;
        }
        const roles = await firebaseQuery.getDocsWhere(
            ["staffRoles"],
            where(`filters.fields.${field}`, "==", userObject.details[field as keyof typeof userObject])) as {[key:string]: unknown};

        if (Object.keys(roles).length > 0) {
            acc = {...acc, ...roles};
        }
        return acc;
    }, Promise.resolve({}));

    return {...roleWithFilters, ...roleWithStudent};
};

export const searchUsers = async (userType:"Staff"|"Students", name:string, user:UserData, lim=5, constraints:QueryConstraint[]=[]) => {
    if (name.length < 3) {
        return {};
    }
    // This means we search for the longest string first, thereby probably getting the fewest results.
    const sortedNameList = sortByReverseStringLength(name.split(" "));

    const unfilteredUsersByForename = await firebaseQuery.getDocsWhere(
        "users",
        [where("userType", "==", userType),
            where("oId", "==", user.oId),
            where("product", "==", user.product),
            where("status", "==", "active"),
            ...constraints,
            where("details.forename", ">=", sortedNameList[0]),
            where("details.forename", "<=", sortedNameList[0]+ "\uf8ff"),
            limit(lim)]) as {[key:string]: UserData};
    const unfilteredUsersBySurname = await firebaseQuery.getDocsWhere(
        "users",
        [where("userType", "==", userType),
            where("product", "==", user.product),
            where("oId", "==", user.oId),
            where("status", "==", "active"),
            ...constraints,
            where("details.surname", ">=", sortedNameList[0]),
            where("details.surname", "<=", sortedNameList[0]+ "\uf8ff"),
            limit(lim)]) as {[key:string]: UserData};

    let userResults = {...unfilteredUsersByForename, ...unfilteredUsersBySurname};
    sortedNameList.shift();
    sortedNameList.forEach((name) => {
        userResults = Object.fromEntries(Object.entries(userResults).filter(([, user]) => user.details.forename.includes(name) || user.details.surname.includes(name)));
    });

    return userResults;
};

type OldUserData = {
    userGroup?: string,
    cohort?: string,
}

export const getUser = async (uid: string, setState: SetStateAction<any>) => {
    let oldUser:OldUserData = {};
    let groupData = {};
    let cohortData = {};
    let packageData = {};

    console.log("Fetching user");

    firebaseQuery.documentSnapshot(["users", uid], async (user: UserData) => {
        console.log("user", user);
        if (user.userGroup && user.userGroup !== "admin") {
            // Always set the groupData, but only get it if the user group has changed.keyof
            if (oldUser?.userGroup !== user.userGroup) {
                groupData = await firebaseQuery.getDocData(["userGroups", user.userGroup]);
            }
        }
        user.groupData = groupData as UserGroupData;

        // if (user.product === "providers") {
        //     // Always set the groupData, but only get it if the user group has changed.keyof
        //     if (oldUser?.userGroup !== user.userGroup) {
        //         const provider = await firebaseQuery.getDocData(["providers", user.oId]) as ProviderData;
        //         packageData = await firebaseQuery.getDocData(["billing", provider.package]) as BillingPackage;
        //     }
        // }
        user.packageData = packageData as BillingPackage;

        if (user.cohort) {
            // Always set the groupData, but only get it if the user group has changed.keyof
            if (oldUser?.cohort !== user.cohort) {
                cohortData = await firebaseQuery.getDocData(["cohorts", user.cohort]);
            }
        }
        user.cohortData = cohortData as CohortData;

        oldUser = user;
        setState(user)
    });
};

export const getNotifications = async (user: UserData, setState: SetStateAction<any>) => {
    const updateNotifications = (notifications:{[key:string]: NotificationObject}, type:"user"|"org") => {
        setState((old:{[key:string]: NotificationObject}) => {
            const concatenatedNotifications = {
                ...old,
                ...Object.fromEntries(
                    Object.entries(notifications)
                        .map(([k, v]) => [k, {...v, notifType: type}])
                ),
            };
            return Object.fromEntries(Object.entries(concatenatedNotifications).sort(([, a], [, b]) => b.created.toMillis() - a.created.toMillis()));
        });
    };

    type NotifQuery = [string[], QueryConstraint|QueryConstraint[]];

    const userNotifications:NotifQuery = [["users", user.id, "notifications"], [orderBy("created", "desc"), limit(10)]]; /* , limit(10) */
    firebaseQuery.getDocsWhere(userNotifications[0], userNotifications[1]).then((s) => {
        updateNotifications(s as {[key:string]: NotificationObject}, "user");
    });

    if (user.oId) {
        let notificationsQuery:NotifQuery;

        if (user.userGroup === "admin") {
            notificationsQuery = [[user.product, user.oId, "notifications"], [orderBy("created", "desc"), limit(10)]]; /* , limit(10) */
        } else {
            notificationsQuery = [[user.product, user.oId, "notifications"], [where("viewableBy", "array-contains-any", [user.id, user.userGroup]), orderBy("created", "desc")]]; /* , limit(10) */
        }
        firebaseQuery.getDocsWhere(notificationsQuery[0], notificationsQuery[1]).then((s) => {
            updateNotifications(s as {[key:string]: NotificationObject}, "org");
        });
    }
    // Only gets notifications that user can view, either by ID or usergroup. Once user not in group, can no longer see notifications.

    // getCollectionSnapshot(notificationsQuery[0], setState, notificationsQuery[1])
};


export const getFiles = async (path: string[]|string) => {
    const files = await listAll(ref(storage, ...pathToArr(path)));

    return (await ((Array.from(files.items) as Array<StorageReference>).reduce(async (acc, item) => {
        const url = await getDownloadURL(item);
        const entry = {name: item.name, url: url};


        if (!(await acc).includes(entry)) {
            (await acc).push(entry);
        }
        return acc;
    }, Promise.resolve([] as Array<{name: string, url: string}>))));
};


export const getFormsFromId = async (path: string[]|string, forms: string[]) => {
    return (await (Array.from(forms).reduce(async (acc: {[k: string]: any}, item: string) => {
        const formData = typeof item === "object" ? item : await firebaseQuery.getDocData([...pathToArr(path), item]);
        console.log("formData", formData);

        acc.then((acc: {[k: string]: any}) => {
            acc[item] = formData;
        });

        return acc;
    }, Promise.resolve({}))));
};

export const getUserById = async (id: string, setState?: SetStateAction<any>, getGroupData=true) => {
    if (!setState) {
        const user =  await firebaseQuery.getDocData(["users", id]).catch(() => false) as UserData|false;
        if (!user) return;
        if (getGroupData) {
            user.groupData = user.userGroup && user.userGroup !== "admin" ? await firebaseQuery.getDocData(["userGroups", user.userGroup]) as UserGroupData : undefined
        }
        return user;
    }
    firebaseQuery.documentSnapshot(["users", id], async (user: UserData) => {
        if (getGroupData) {
            user.groupData = user.userGroup && user.userGroup !== "admin" ? await firebaseQuery.getDocData(["userGroups", user.userGroup]) as UserGroupData : undefined
        }
        setState(user);
    });
    return;
};

/* export const getUsersByType = async (q: Query, setState:SetStateAction<any>, uid:string, lim:number, limitType?:"start"|"end") => {
    if (limitType === "end") {
        q = query(q, endBefore(uid), limitToLast(lim));
    } else {
        q = query(q, startAfter(uid), limit(lim));
    }
    return firebaseQuery.collectionSnapshot(q, setState);
};*/

export const getPlacementbyId = async (pid: string, setState:SetStateAction<any>) => {
    return firebaseQuery.documentSnapshot(["placements", pid], setState);
};

export const getOrganisation = async (user: UserData, setOrg: (data: InstituteData|ProviderData) => void) => {
    return firebaseQuery.documentSnapshot([user.product, user.oId], setOrg);
};

export const getPlacementsWhere = async ({w=[], oId, uid, raw=false}:{w:QueryConstraint[]|QueryConstraint, oId?:string, uid?:string, raw?:boolean}) => {
    return await firebaseQuery.getDocsWhere("placements", [...(oId ? [where("oId", "==", oId)] : []), ...(uid ? [where("uid", "==", uid)] : []), ...pathToArr(w)], raw);
};

export const checkPlacementConflicts = async (instituteId:string, userGroupId:string) => {
    const q = query(collection(db, "placements"), where("oId", "==", instituteId), where("userGroup", "==", userGroupId));

    return (await getDocs(q)).size;
};

export const getPlacementListingsById = async (placementListings: string[]) => {
    const placements =  await Promise.all(placementListings.map(async (listingId) => {
        return [listingId, await firebaseQuery.getDocData(["placementListings", listingId]).catch(() => false) as PlacementListing];
    })) as [string, PlacementListing|false][];

    return placements.filter(([_, v]) => v !== false) as [string, PlacementListing][];
}


