import {
    DocumentData, QueryConstraint, QueryDocumentSnapshot, QueryFieldFilterConstraint, QueryOrderByConstraint, QuerySnapshot, Unsubscribe, arrayRemove, collection, documentId,
    endAt, endBefore, getDocs, limit, limitToLast, onSnapshot, orderBy, query, startAfter, where
} from "firebase/firestore";
import { useCallback, useEffect, useRef, useState } from "react";
import { defaultApplicantWorkflow, defaultInstituteWorkflow, defaultStudentWorkflow } from "./constants";
import { executeCallable, useExecuteCallableJob } from "./firebase/firebase";
import { db, storage } from "./firebase/firebaseConfig";
import FirebaseQuery from "./firebase/firebaseQuery";
import { getFiles, getFormsFromId, getPlacementbyId, getPlacementsWhere, getUserById } from "./firebase/readDatabase";
import { arrayUniqueValues, arraysEqual, convertDate, editNestedObject, getAccess, getDateDiff, getUniqueId, objectsEqual, quoteAlgoliaFilterIfNeeded, validateEmail } from "./firebase/util";
import { addPlacement, editPlacementStage, uploadFiles } from "./firebase/writeDatabase";
import {
    AlumniConvoUser,
    ApplicantStage,
    Application,
    ArrowObject, CohortData, CustomFormSchema, ExternalEvent, ExternalEventAttendee, FileItem, FlagCodes, InstituteData, OnboardingDocs, OrganisationAddress, PlacementListing, Products,
    ProviderContactData,
    ProviderData,
    QueryObject, QueryObjectConstraint, SchoolData, Sorts, StudentPlacementData, UserData, UserGroupData, WorkflowStage
} from "./typeDefinitions";
import algoliasearch from "algoliasearch";
import {getDownloadURL, ref} from "firebase/storage";
import {objectsEqualNew} from "./util";


type StudentPlacementListParams = {
    user: UserData,
    student?: UserData,
    queryConstraint?: QueryConstraint[],
    ql?: number // query limit
}

const DEFAULTQUERYLIMIT = 5;

export function useStudentPlacementList({user, student, queryConstraint, ql=DEFAULTQUERYLIMIT}:StudentPlacementListParams) {
    const [loadMoreIcon, setLoadMoreIcon] = useState(true);
    const [query, setQuery] = useState("");
    const [initialQueryLimit, setInitialQueryLimit] = useState(ql);
    const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
    const [placements, setPlacements] = useState<{[key:string]:StudentPlacementData}>();
    const [startPlacementAfter, setStartPlacementAfter] = useState<QueryDocumentSnapshot<DocumentData>>(); // uid, pId
    const [studentId, setStudentId] = useState<string>();

    const reset = () => {
        setPlacements(undefined);
        setStartPlacementAfter(undefined);
    }; 

    useEffect(() => {
       if (user.userType === "Students") {
        setStudentId(user.id);
        return;
       }
       if (user.oId !== student?.oId || user.userType !== "Staff") {
        setStudentId(undefined);
        return;
       }
       if (user.userGroup === "admin") {
        setStudentId(student.id);
        return;
       }
       if (!user.viewCohorts || !user.viewStudents || user.viewCohorts === "none") {
        setStudentId(undefined);
        return;
       }
       if (user.viewCohorts === "all" && user.viewStudents == "all") {
        setStudentId(student.id);
       }
       if (user.viewCohorts === "some" && user.visibleCohorts?.includes(student.cohort || "")) {
        if (user.viewStudents === "all") {
            setStudentId(student.id);
            return;
        }
        
        if (user.studentFilterValues?.includes(student.details[user.studentFilter || ""])) {
            setStudentId(student.id);
            return;
        }
        setStudentId(undefined);
       }

    }, []);

    const changeQueryConstraints = (e: QueryConstraint[]) => {
        setQueryConstraints([...(queryConstraint || []), ...e]);
    };

    useEffect(() => {
        if (!studentId) return;
        setPlacements(undefined);
        setStartPlacementAfter(undefined);
        loadMorePlacements(undefined);
    }, [studentId, query, queryConstraints]);

    const loadMorePlacements = async (
        fStartPlacementAfter:QueryDocumentSnapshot<DocumentData>|undefined=startPlacementAfter,
    ) => {
        if (!studentId) return;
        setLoadMoreIcon(true);

        // If can view all, query placements directly. Otherwise query students.

        let fPlacements:{[key:string]: StudentPlacementData} = {};

        const queryConstraintOrdered = Boolean(queryConstraints && queryConstraints.find((v) => v.type === "orderBy"));

        const constraints:QueryConstraint[] = fStartPlacementAfter ?
            [limit(placements ? DEFAULTQUERYLIMIT : initialQueryLimit),
                ...(queryConstraintOrdered ? [] : [orderBy(documentId())]),
                startAfter(fStartPlacementAfter)] :
            [limit(placements ? DEFAULTQUERYLIMIT : initialQueryLimit), ...(queryConstraintOrdered ? [] : [orderBy(documentId())])];

        queryConstraints && constraints.unshift(...queryConstraints);
        query !== "" && constraints.unshift(where("name", "==", query));
        constraints.unshift(where("uid", "==", studentId));

        const placementsQuery = await getPlacementsWhere({w: constraints, oId: user?.oId, raw: true}) as QuerySnapshot;

        const placementsWithStudentData:[string, StudentPlacementData][] = placementsQuery.empty ? [] : placementsQuery.docs.map((placement) => 
            [placement.id, {...placement.data() as StudentPlacementData, id: placement.id}]
        );

        fPlacements = {...fPlacements, ...Object.fromEntries(placementsWithStudentData)};


        if (Object.keys(fPlacements).length === (placements ? DEFAULTQUERYLIMIT : initialQueryLimit)) {
            const lastPlacement = placementsQuery.docs[placementsQuery.docs.length-1];
            if (lastPlacement) {
                setStartPlacementAfter(lastPlacement);
            }
            setPlacements((p) => ({...p, ...fPlacements}));
            return;
        }

        setPlacements((p) => ({...p, ...fPlacements}));
        setLoadMoreIcon(false);
        return;
    };

    return ({...{placements, loadMoreIcon, loadMorePlacements, setQuery, setInitialQueryLimit, reset, changeQueryConstraints}});
}


type InstitutePlacementParams = {
    id?: string,
    user: UserData,
    cohort?: string,
    queryConstraint?: QueryConstraint[],
    ql?: number,
    inProgress?: boolean
}


type NewInstitutePlacementParams = {
    id?: string,
    user: UserData,
    oId?: string,
    cohort?: string,
    queryConstraints?: QueryObjectConstraint[],
    ql?: number,
    inProgress?: boolean,
    view: "list"|"table",
    filters?: FilterObject,
    initialSort?: string,
    initialSearch?: string
    uid?: string,
}





export function useOldInstitutePlacementList({id, user, cohort, queryConstraint, ql=DEFAULTQUERYLIMIT, inProgress}:InstitutePlacementParams) {
    const [loadMoreIcon, setLoadMoreIcon] = useState(true);
    const [query, setQuery] = useState<string>();
    const [initialQueryLimit, setInitialQueryLimit] = useState(ql);
    const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
    const [oId, setOId] = useState<string>();

    const [placements, setPlacements] = useState<{[key:string]:StudentPlacementData & {student: UserData}}>();

    const [startPlacementAfter, setStartPlacementAfter] = useState<[QueryDocumentSnapshot<DocumentData>|undefined, number]>(); // uid, pId

    const algoliaClient = algoliasearch(process.env.NODE_ENV === "development" ? "A0ZB50I7VS" : "A0ZB50I7VS", user.algoliaKey);
    const placementsIndex = algoliaClient.initIndex("placements");

    useEffect(() => {
        if (user.product !== "institutes" || user.userType !== "Staff") {
            setOId(undefined);
        }

        setOId(user.oId);
    }, [user]);

    useEffect(() => {
        if (query === undefined) return;

        if (!query) {
            reset();
            loadMorePlacements([undefined, 0]);
        }

        

        const searchPlacements = async () => {
            let placementsFound:{[key: string]: StudentPlacementData& {
                student: UserData;
            }} = {}

            let placementSearchString = `oId:${user.oId} AND ` + (inProgress !== undefined ? (inProgress ? "inProgress:true" : "completed:true" ) : "")
            if (cohort) {
                placementSearchString = placementSearchString + ` AND cohort:${cohort}`;
            }
            const searchPlacementHits = await placementsIndex.search<StudentPlacementData>(query, {
                filters: placementSearchString
            });
            
            const i = (searchPlacementHits ? (await Promise.all(searchPlacementHits.hits.map(async (hit) => {
                    const student = hit.uid === user.id ?
                        user : (await getUserById(hit.uid)
                        .catch(() => false)) as UserData;
                        
                    if (!student) return;
                    console.log("STUDENNT", hit.objectID, student)

                    const finalData = {...(hit as StudentPlacementData), student: student};
                    return [hit.objectID, finalData];
                }))) : []).filter((e) => e !== undefined) as [string, StudentPlacementData&{student: UserData}][];
    
                console.log("FOUND", i.length, "placement");
    
            placementsFound = {...Object.fromEntries(i), ...placementsFound}

            console.log("found", placementsFound);
            setPlacements(placementsFound);
            setLoadMoreIcon(false);
        }
        searchPlacements()
    }, [query]);

    const reset = () => {
        setPlacements(undefined);
        setStartPlacementAfter([undefined, 0]);
    };

    const changeQueryConstraints = (e: QueryConstraint[]) => {
        setQueryConstraints([...(queryConstraint || []), ...e]);
    };

    useEffect(() => {
        setPlacements(undefined);
        setStartPlacementAfter([undefined, 0]);
        loadMorePlacements([undefined, 0]);
    }, [queryConstraints]);

    const loadMorePlacements = async (
        fStartPlacementAfter:[QueryDocumentSnapshot<DocumentData>|undefined, number]|undefined=startPlacementAfter,
    ) => {
        if (query || user.viewCohorts === "none" || !oId) {
            setLoadMoreIcon(false);
            return;
        }



        setLoadMoreIcon(true);

        let fPlacements:{[key:string]: StudentPlacementData&{student:UserData}} = {};

        if (((user.userGroup === "admin" || (cohort && user.viewCohorts === "some" && user?.visibleCohorts?.includes(cohort as string)) || (user.viewCohorts === "all" && user.viewStudents === "all")))) {

            const queryConstraintOrdered = Boolean(queryConstraints && queryConstraints.find((v) => v.type === "orderBy"));

            const constraints:QueryConstraint[] = fStartPlacementAfter?.length === 2 && fStartPlacementAfter[0] ?
                [limit(placements ? DEFAULTQUERYLIMIT : initialQueryLimit),
                    ...(queryConstraintOrdered ? [] : [orderBy(documentId())]),
                    startAfter(fStartPlacementAfter[0])] :
                [limit(placements ? DEFAULTQUERYLIMIT : initialQueryLimit), ...(queryConstraintOrdered ? [] : [orderBy(documentId())])];

            queryConstraints && constraints.unshift(...queryConstraints);
            // query && constraints.unshift(where("name", "==", query));
            cohort && constraints.unshift(where("cohort", "==", cohort));


            const placementsQuery = await getPlacementsWhere({w: constraints, oId: oId, raw: true}) as QuerySnapshot;
            console.log("PLACEMENTS RETRIEVED", placementsQuery.size);
            
            const placementsWithStudentData:([string, StudentPlacementData&{student:UserData}]|false)[] = placementsQuery.empty ? [] : await Promise.all(placementsQuery.docs.map(async (placement) => {
                const pData = placement.data() as StudentPlacementData;
                const student = pData.uid === user.id ? user : (await getUserById(pData.uid).catch(() => false)) as UserData;
                console.log("STUDENT", student, "PLACEMENT", id);
                // if (!student) return false;
                if (user.viewStudents === "some") {
                    if (!(user.studentFilter && user.studentFilterValues)) return false;

                    if (!user.studentFilterValues.includes(student.details[user.studentFilter])) {
                        return false;
                    }
                }
                return [placement.id, {...pData, student: student, id: placement.id}];
            }));

            const filtered = placementsWithStudentData.filter((el) => el) as [string, StudentPlacementData & {student: UserData}][];

            fPlacements = {...fPlacements, ...Object.fromEntries(filtered)};


            if (Object.keys(fPlacements).length === (placements ? DEFAULTQUERYLIMIT : initialQueryLimit)) {
                const lastPlacement = placementsQuery.docs[placementsQuery.docs.length-1];
                if (lastPlacement) {
                    setStartPlacementAfter(() => ([lastPlacement, 0]));
                }
                setPlacements((p) => ({...p, ...fPlacements}));
                return;
            }
            if (!query) {
                setPlacements((p) => ({...p, ...fPlacements}));
                setLoadMoreIcon(false);
                return;
            }
        }
    };


    return ({...{placements, loadMoreIcon, loadMorePlacements, setQuery, setInitialQueryLimit, reset, changeQueryConstraints}});
}


export function useNewInstitutePlacementList({id, user, oId, uid, filters, initialSort, initialSearch, view, cohort, queryConstraints, ql=DEFAULTQUERYLIMIT, inProgress}:NewInstitutePlacementParams) {
    const [query, setQuery] = useState<QueryObject[]>();

    const sorts:Sorts = {
        ["Student Forename - Asc"]: {
            value: "studentForename",
            direction: "asc",
        },
        ["Student Forename - Desc"]: {
            value: "studentForename",
            direction: "desc",
        },
        ["Student Surname - Asc"]: {
            value: "studentSurname",
            direction: "asc",
        },
        ["Student Surname - Desc"]: {
            value: "studentSurname",
            direction: "desc",
        },
        ["Student Email - Asc"]: {
            value: "studentEmail",
            direction: "asc",
        },
        ["Student Email - Desc"]: {
            value: "studentEmail",
            direction: "desc",
        },
        ["Provider email - Asc"]: {
            value: "providerEmail",
            direction: "asc",
        },
        ["Provider email - Desc"]: {
            value: "providerEmail",
            direction: "desc",
        }
    }

    const additionalProcessing = async (k: string, placement:StudentPlacementData) : Promise<[string, unknown]|undefined> => {
        if (user.userGroup !== "admin" && user.viewStudents === "some") {
            if (!(user.studentFilter && user.studentFilterValues)) return undefined;

            const student = await getUserById(placement.uid).catch(() => false) as UserData|false;

            if (!student) {
                console.log("No student");
                return
            };
    
            if (!user.studentFilterValues.includes(student.details[user.studentFilter])) {
                console.log("filter not included. Filteres: ", user.studentFilterValues, "value", user.studentFilter, "-", student.details[user.studentFilter]);
                return;
            }
        }
        return [k, {...placement, id: k}];
    }

    const {tableData, pageUp, pageDown, setFilters, page, setView, loading, updateSearch, updateSort, sort} = useDataViewerPaginator({view, filters, sorts, queryLimit: ql, initialSort, initialSearch, data: query, additionalEntryProcessing: additionalProcessing, onSearch: async (s, sort, page, filters, limit) => await algoliaPlacementSearch(query || [], user, s, sort, page, filters, limit, cohort, inProgress)})

    useEffect(() => {
        console.log("SET QUERY", user, queryConstraints, cohort);
        // Sets the query of for the DataViewerPaginator
        if(user.product !== "institutes" || user.userType !== "Staff") {
            setQuery(undefined);
            return;
        }
        if (((user.userGroup === "admin" || (cohort && user.viewCohorts === "some" && user?.visibleCohorts?.includes(cohort as string)) || (user.viewCohorts === "all" && user.viewStudents !== "none")))) {
            const constraints:QueryObjectConstraint[] = [["oId", "==", oId || user.oId], ["draft", "==", false]];

            cohort && constraints.push(["cohort", "==", cohort]);
            uid && constraints.push(["uid", "==", uid]);

            queryConstraints && constraints.unshift(...queryConstraints);
            inProgress !== undefined && constraints.push(["inProgress", "==", inProgress]);
            
            console.log("PLACEMENT CONSTRAINTS", constraints);
            setQuery([{
                path: ["placements"],
                where: constraints
            }])
            return;
        }
        setQuery(undefined)
    }, [user, queryConstraints, cohort])

    return {tableData, page, loading, updateSearch, setFilters, setView, pageUp, pageDown, sorts, updateSort, sort}
}



type AlumniPaginatorParams = {
    user?: UserData,
    alumniConvoUser?: AlumniConvoUser,
    school?: string,
    queryConstraints?: QueryObjectConstraint[],
    ql?: number,
    view: "list"|"table",
    filters?: FilterObject,
}



export function useAlumniPaginator({user, alumniConvoUser, filters, view, school, queryConstraints, ql=DEFAULTQUERYLIMIT}:AlumniPaginatorParams) {
    const [query, setQuery] = useState<QueryObject[]>();

    const sorts:Sorts = {}


    const {tableData, pageUp, pageDown, setFilters, page, setView, loading} = useDataViewerPaginator({view, filters, sorts, queryLimit: ql, data: query})
    const firebaseQuery = new FirebaseQuery();

    useEffect(() => {
        const createQuery = async () => {
            // Sets the query of for the DataViewerPaginator


            const getQueryAccess = async () => {
                const constraints:QueryObjectConstraint[] = [];

                if (user) {
                    constraints.push(["oId", "==", user.oId]);

                    if (user.userGroup === "admin" && user.userType === "Staff") return constraints;
        
                    if (user.userType === "Staff") {
                        if (user.viewSchools === "all") return constraints;
        
                        if (user.viewSchools === "none") return false;
        
                        if (user.viewSchools === "some") {
                            if (!school) return false;
        
                            if (user?.visibleSchools?.includes(school as string)) return constraints;
                        }
                        return false;
                    }
                    return false;
                }

                if (alumniConvoUser) {
                    constraints.push(["oId", "==", alumniConvoUser.oId]);
                    console.log("ALUMNI CONVO USER");

                    if ((school || alumniConvoUser.schoolId) && school === alumniConvoUser.schoolId) return constraints;
                    console.log("ALUMNI ACCESS TRUE");
                    if (alumniConvoUser.schoolId) { 
                        const school = await firebaseQuery.getDocData(["schools", alumniConvoUser.schoolId]) as SchoolData;
                        console.log("SCHOOL ACCESS", school, school.alumniConversations);
                        return school.alumniConversations ? constraints : false;
    
                    } else {
                        const institute = await firebaseQuery.getDocData(["institutes", alumniConvoUser.oId]) as InstituteData;
                        return institute.alumniConversations ? constraints : false;
                    }
                }
                return false;
            }
            const constraints = await getQueryAccess();
            console.log("CONSTRAINTS", constraints);
            if (!constraints) return;
    
            school && constraints.push(["schoolId", "==", school]);
            queryConstraints && constraints.unshift(...queryConstraints);
    
            return constraints;
        }
        console.log("Creating query");
        createQuery().then((constraints) => {
            setQuery([{
                path: ["alumni"],
                where: constraints
            }])
        })
    }, [user, queryConstraints, school])

    useEffect(() => {
        console.log("Alumni data", query, tableData);
    }, [tableData]);

    return {tableData, page, loading, setFilters, setView, pageUp, pageDown, sorts}
}


const algoliaPlacementSearch = async (data: QueryObject[], user: UserData, query?: string, sort?: [string, {value: string, direction: "asc"|"desc"}], page?: number, filters?: FilterObject, limit?: number, cohort?: string, inProgress?: boolean) => {
    const algoliaClient = algoliasearch(process.env.NODE_ENV === "development" ? "A0ZB50I7VS" : "A0ZB50I7VS", user.algoliaKey);
    const placementsIndex = algoliaClient.initIndex(sort ? Object.values(sort[1]).join("_") : "placements");
    // const usersIndex = algoliaClient.initIndex("users");

        // let userSearchString = `userType:Students AND status:active AND oId:${user.oId} AND product:${user.product}`
        // if (cohort) {
        //     userSearchString = userSearchString + ` AND cohort:${cohort}`;
        // }

        // if (user.product === "institutes" && user.userType === "Staff") {
        //     const searchStudentHits = await usersIndex.search<UserData>(query, {
        //         filters: userSearchString,
        //         hitsPerPage: limit,
        //         page: page
        //     });

        //     if (searchStudentHits) {
        //         console.log("FOUND", searchStudentHits.hits.length, "students");

        //         await Promise.all(searchStudentHits.hits.map(async (hit) => {
        //             console.log("STUDENT", hit.objectID);
        //             const constraints = [...(cohort ? [where("cohort", "==", cohort)] : [])]
        //             if (inProgress !== undefined) {
        //                 constraints.push(inProgress ? where("inProgress", "==", true) : where("completed", "==", true));
        //             }
        //             const fPlacements = await getPlacementsWhere({w: constraints, uid: hit.objectID, oId: hit.oId}) as {[key: string]: StudentPlacementData};
                            
        //             console.log("PLACEMENTS", fPlacements)
        //             Object.entries(fPlacements).forEach(([k, v]) => {
        //                 placementsFound[k] = {...v, student: hit};
        //             })
        //         }))
        //     }
        // }

        if (data.length === 0) return {};

        const constraints = data[0].where;

        if (!constraints?.length) return {};

        let placementSearchString = constraints.map(([k, e, v]) => {
            if (e === "==") return `${k}:${quoteAlgoliaFilterIfNeeded(v)}`; // Equality check
            if (e === "!=") return `${k}:-${quoteAlgoliaFilterIfNeeded(v)}`; // Not equal check
            if (e === "<") return `${k}:<${quoteAlgoliaFilterIfNeeded(v)}`; // Less than check
            if (e === "<=") return `${k}:<=${quoteAlgoliaFilterIfNeeded(v)}`; // Less than or equal check
            if (e === ">") return `${k}:>${quoteAlgoliaFilterIfNeeded(v)}`; // Greater than check
            if (e === ">=") return `${k}:>=${quoteAlgoliaFilterIfNeeded(v)}`; // Greater than or equal check
            if (e === "array-contains") return `${k}:${quoteAlgoliaFilterIfNeeded(v)}`; // Array contains check (string format)
            if (e === "array-contains-any") return `${k}:"${(v as string[]).join('","')}"`; // Array contains any of the values (string format)
            if (e === "in") return `(${(v as string[]).map((value) => `${k}:${quoteAlgoliaFilterIfNeeded(value)}`).join(' OR ')})`; // In check
            if (e === "not-in") return (v as string[]).map((value) => `${k}:-${quoteAlgoliaFilterIfNeeded(value)}`).join(' AND '); // In check
            return;
        }).join(" AND ");
        
        filters && Object.entries(filters).filter(([, filter]) => filter.value).map(([id, filter]) => {
            placementSearchString = placementSearchString + ` AND ${id}:${quoteAlgoliaFilterIfNeeded(filter.value)}`;
        });

        const options = {
            filters: placementSearchString,
            hitsPerPage: limit,
            page: page ? page - 1 : undefined,
        }

        const searchPlacementHits = await placementsIndex.search<StudentPlacementData>(query || "", options);

        console.log(searchPlacementHits.hits);
        
        const i = (searchPlacementHits ? (await Promise.all(searchPlacementHits.hits.map(async (hit) => {
                return [hit.objectID, hit];
            }))) : []).filter((e) => e !== undefined) as [string, StudentPlacementData][];

        return Object.fromEntries(i)
}


export function useVeryOldInstitutePlacementList({user, cohort, queryConstraint, ql=DEFAULTQUERYLIMIT}:InstitutePlacementParams) {
    const [loadMoreIcon, setLoadMoreIcon] = useState(true);
    const [query, setQuery] = useState("");
    const [initialQueryLimit, setInitialQueryLimit] = useState(ql);
    const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
    const [oId, setOId] = useState<string>();

    const firebaseQuery = new FirebaseQuery();

    const [placements, setPlacements] = useState<{[key:string]:StudentPlacementData & {student: UserData}}>();

    const [startPlacementAfter, setStartPlacementAfter] = useState<["placement"|"student"|undefined, string|undefined, QueryDocumentSnapshot<DocumentData>|undefined, number]>(); // uid, pId
    const [loadedStudents, setLoadedStudents] = useState<string[]>([]);

    useEffect(() => {
        if (user.product !== "institutes" || user.userType !== "Staff") {
            setOId(undefined);
        }

        setOId(user.oId);
    }, [user]);

    const reset = () => {
        setPlacements(undefined);
        setLoadedStudents([]);
        setStartPlacementAfter(["placement", undefined, undefined, 0]);
    };

    const changeQueryConstraints = (e: QueryConstraint[]) => {
        setQueryConstraints([...(queryConstraint || []), ...e]);
    };

    useEffect(() => {
        setPlacements(undefined);
        setLoadedStudents([]);
        setStartPlacementAfter(["placement", undefined, undefined, 0]);
        loadMorePlacements(["placement", undefined, undefined, 0], []);
    }, [query, queryConstraints]);

    const loadMorePlacements = async (
        fStartPlacementAfter:["placement"|"student"|undefined, string|undefined, QueryDocumentSnapshot<DocumentData>|undefined, number]|undefined=startPlacementAfter,
        fLoadedStudents:string[]=loadedStudents
    ) => {
        // Only false when at end of results
        // console.log("Load placements");
        if (user.viewCohorts === "none" || !oId) {
            setLoadMoreIcon(false);
            return;
        }
        setLoadMoreIcon(true);

        // If can view all, query placements directly. Otherwise query students.

        let fPlacements:{[key:string]: StudentPlacementData&{student:UserData}} = {};

        if (((user.userGroup === "admin" || (cohort && user.viewCohorts === "some" && user?.visibleCohorts?.includes(cohort as string)) || (user.viewCohorts === "all" && user.viewStudents === "all") || query) && (fStartPlacementAfter && fStartPlacementAfter[0] === "placement"))) {
            const queryConstraintOrdered = Boolean(queryConstraints && queryConstraints.find((v) => v.type === "orderBy"));

            const constraints:QueryConstraint[] = fStartPlacementAfter?.length === 4 && fStartPlacementAfter[1] ?
                [limit(placements ? DEFAULTQUERYLIMIT : initialQueryLimit),
                    ...(queryConstraintOrdered ? [] : [orderBy(documentId())]),
                    startAfter(fStartPlacementAfter[2])] :
                [limit(placements ? DEFAULTQUERYLIMIT : initialQueryLimit), ...(queryConstraintOrdered ? [] : [orderBy(documentId())])];

            queryConstraints && constraints.unshift(...queryConstraints);
            query !== "" && constraints.unshift(where("name", "==", query));
            cohort && constraints.unshift(where("cohort", "==", cohort));


            const placementsQuery = await getPlacementsWhere({w: constraints, oId: oId, raw: true}) as QuerySnapshot;

            console.log("placementsQuery", placementsQuery);
            
            const placementsWithStudentData:([string, StudentPlacementData&{student:UserData}]|false)[] = placementsQuery.empty ? [] : await Promise.all(placementsQuery.docs.map(async (placement) => {
                const pData = placement.data() as StudentPlacementData;
                const student = pData.uid === user.id ? user : (await getUserById(pData.uid).catch(() => false)) as UserData;

                if (user.viewStudents === "some") {
                    if (!(user.studentFilter && user.studentFilterValues)) return false;

                    if (!user.studentFilterValues.includes(student.details[user.studentFilter])) {
                        return false;
                    }
                }
                return [placement.id, {...pData, student: student, id: placement.id}];
            }));
            console.log("placementsWithStudentData", placementsWithStudentData);

            const filtered = placementsWithStudentData.filter((el) => el) as [string, StudentPlacementData & {student: UserData}][];

            fPlacements = {...fPlacements, ...Object.fromEntries(filtered)};


            if (Object.keys(fPlacements).length === (placements ? DEFAULTQUERYLIMIT : initialQueryLimit)) {
                console.log("More to come, setting");
                const lastPlacement = placementsQuery.docs[placementsQuery.docs.length-1];
                if (lastPlacement) {
                    setStartPlacementAfter(() => (["placement", lastPlacement.data().uid, lastPlacement, 0]));
                }
                setPlacements((p) => ({...p, ...fPlacements}));
                return;
            }
            if (!query) {
                console.log("no query, finishing")
                setPlacements((p) => ({...p, ...fPlacements}));
                setLoadMoreIcon(false);
                return;
            }
        }

        if (!(user.studentFilterValues && user.studentFilter && user.viewStudents === "some")) {
            console.log("fenaibn");
            return;
        };

        const getStudentPlacements = async (ffStartPlacementAfter=fStartPlacementAfter, ffPlacements=fPlacements) : Promise<{sLoadedStudents:string[],
            sPlacements: { [k: string]: StudentPlacementData&{student: UserData}},
            sStartPlacementAfter: ["placement"|"student"|undefined, string | undefined, QueryDocumentSnapshot<DocumentData> | undefined, number]|undefined}> => {
            if (ffStartPlacementAfter && ffStartPlacementAfter[0] === "placement") {
                ffStartPlacementAfter = [undefined, undefined, undefined, 0];
            }
            // console.log("ff", ffStartPlacementAfter);
            const constraints:QueryConstraint[] = ffStartPlacementAfter && ffStartPlacementAfter[1] ?
                [limit(1), orderBy(documentId()), startAfter(ffStartPlacementAfter[1])] :
                [limit(1), orderBy(documentId())];

            constraints.push(
                where("oId", "==", oId),
                where("userType", "==", "Students")
            );
            
            if (ffStartPlacementAfter && ffStartPlacementAfter[3]) {
                const currentFilter = user?.studentFilterValues?.[ffStartPlacementAfter[3]];

                if (currentFilter && user.studentFilter) {
                    constraints.push(where(user.studentFilter, "==", currentFilter));
                }
            }

            // console.log("CONSTERAINTS: ", constraints);

            const student:[string, UserData] = Object.entries((await firebaseQuery.getDocsWhere("users", constraints)) as {[key:string]: UserData})[0];
            // console.log("student", student);

            // If there is no students but more filters
            if (!student && ffStartPlacementAfter && ffStartPlacementAfter[3] + 1 < (user?.studentFilterValues || []).length) {
                // console.log("No more students. Calling recursion");
                return await getStudentPlacements(["student", undefined, undefined, ffStartPlacementAfter[3]+1], ffPlacements);
            }

            if (!student) {
                // console.log("No more students or queries. Returning.");
                setLoadMoreIcon(false);
                return {sLoadedStudents: fLoadedStudents, sPlacements: ffPlacements, sStartPlacementAfter: ffStartPlacementAfter};
            }

            if (fLoadedStudents.includes(student[0]) && ffStartPlacementAfter) {
                return await getStudentPlacements(["student", student[0], undefined, ffStartPlacementAfter[3]], ffPlacements);
            }


            fLoadedStudents.push(student[0]);

            const studentConstraints:QueryConstraint[] = ffStartPlacementAfter && ffStartPlacementAfter[2] ?
                [
                    limit(placements ? DEFAULTQUERYLIMIT : initialQueryLimit),
                    orderBy(documentId()), where("uid", "==", student[0]),
                    startAfter(ffStartPlacementAfter[2])] :
                [limit(placements ? DEFAULTQUERYLIMIT : initialQueryLimit), orderBy(documentId()), where("uid", "==", student[0])];
            // console.log("QCCCC", queryConstraints);
            queryConstraints && studentConstraints.unshift(...queryConstraints);

            const studentPlacements = await getPlacementsWhere({w: studentConstraints, oId: oId, raw: true}) as QuerySnapshot;
            // console.log('SP', studentPlacements)

            const placementsWithStudentData:[string, StudentPlacementData&{student:UserData}][] = (studentPlacements.docs.map((placement) =>
                [placement.id, {...placement.data() as StudentPlacementData, student: student[1], id: placement.id}]
            ));

            ffPlacements = {...ffPlacements, ...Object.fromEntries(placementsWithStudentData)};

            if (Object.keys(ffPlacements).length < (placements ? DEFAULTQUERYLIMIT : initialQueryLimit)) {
                // console.log("Below query length, calling recursion")
                return await getStudentPlacements(["student", student[0], undefined, ffStartPlacementAfter ? ffStartPlacementAfter[3] : 0], ffPlacements);
            }

            const lastItem = studentPlacements.docs[studentPlacements.docs.length-1];
            return {sLoadedStudents: fLoadedStudents, sPlacements: ffPlacements,
                sStartPlacementAfter: ["student", ffStartPlacementAfter ? ffStartPlacementAfter[1] : undefined, lastItem, ffStartPlacementAfter ? ffStartPlacementAfter[3] : 0]};


            // Get placements from user after current user.
            // If number of placements matches query, return new placements and setStartPlacement as no update and current user.

            // If number of placements below query, recursively call function with setStartPlacement as new and no placement one
        };
        // console.log("Call student placements");
        const {sPlacements, sStartPlacementAfter, sLoadedStudents} = await getStudentPlacements();

        setLoadedStudents((s) => s ? s.concat(sLoadedStudents.filter((item:string) => s.indexOf(item) < 0)) : sLoadedStudents);
        setPlacements((p) => ({...p, ...fPlacements, ...sPlacements}));
        setStartPlacementAfter(sStartPlacementAfter);
        return;

        /**
             * if(!loadingIcon) return
             *
             * If user can view all, get placements matching title, limit 5. If 5, return and set as last placement.
             * On scroll bottom, call searchPlacements again, but this time there will be a startAfter.
             * Query on startAfter, limit 5. If there are 5, return.
             *
             * If less than 5, continue.
             *
             * If no query and view all, set loading icon to false, end of results.
             *
             * There may be a query, or the user can view only staff roles.
             * At this point, query users based on the staff roles and forename and surname with query.
             * Limit students results to 5.
             * For each student, get 5 placements and add to total placements.
             * When adding placements, make sure to remove any duplicates from the existing ones.
             * At end of each student, check if >= 5 placements.
             * If true, set role query at currentPos, student startAt as current student and placement startAfter as prev placement. Return
             *
             * Otherwise, it will keep looping.
             *
             * When scroll bottom this time, only get placements if studentPos is null. If it is a value, we must be
             * querying based on student. All individual placements are in the list.
             * Lots of returning going on so if gets to end of function, setLoadingIcon to false, end of results. When scroll bottom, loadingIcon is false, so return
             *
             *
             * How to restart search? on query or onLoad, reset all queryCursors and placements and call load.
             * This can be one useEffect listening to query. Will reset on start (but no need to) and whenever query changes.

        // Search users, limit to 5. Go through each until end, then extend limit by 5 each time.
        // For each user, query 5 placements at a time. When at end, increment user position by 1. Continue to append.
        // Implement this all into the filters, so one search tool.

        // When search changes, reset placements.
        // console.log("s", students);
        // Search students.
        For each student, get placements. Limit and note student id. Start after on scroll. Should be easy.
        */
    };

    return ({...{placements, loadMoreIcon, loadMorePlacements, setQuery, setInitialQueryLimit, reset, changeQueryConstraints}});
}

export function useFilterTablePaginator({data}:{data:{[key:string]:{[key:string]: unknown}}|QueryObject[]}) {
    const [tableData, setTableData] = useState<{[key:string]:{[key:string]: unknown}}>(Array.isArray(data) ? Object.fromEntries(Object.entries(data).slice(0, 10)) : {});
    const [page, setPage] = useState([1, 0]);
    const [filters, setFilters] = useState<{[key:string]: unknown}>();
    const [queryAnchor, setQueryAnchor] = useState<{startKey: string, endKey: string, startQueryPos: number, endQueryPos: number}>({startKey: "", endKey: "", startQueryPos: 0, endQueryPos: 0});
    const [prevEntryIds, setPrevEntryIds] = useState<{[key:string]:number}>({});
    const [dataListenerUnsubscribe, setDataListenerUnsubscribe] = useState<Unsubscribe|undefined>();


    const getDataFromQuery = async (
        itemList: {[key: string]: any} = {}, currentQueryAnchor=queryAnchor, cursorDirection?:"increase"|"decrease"|undefined, prevEntries=prevEntryIds, loadMoreFromQuery=false):Promise<any> => {
        if (!Array.isArray(data)) {
            setTableData(Object.fromEntries(Object.entries(data).slice((page[0] - 1)*10, page[0]*10)));
            return;
        }
        let cursorPos:number;
        if (page[0] > page[1]) {
            cursorPos = currentQueryAnchor.endQueryPos;
        } else {
            cursorPos = currentQueryAnchor.startQueryPos;
        }

        const querySchema:QueryObject = data[cursorPos];

        const createQuery = (queryData:QueryObject) => {
            const constraints:any[] = [];
            queryData.where && queryData.where.forEach((w) => {
                constraints.push(where(...w));
            });

            filters && Object.entries(filters).forEach(([key, value]) => {
                constraints.push(where(key, "==", value));
            });

            constraints.push(orderBy(queryData.orderBy ? queryData.orderBy === "documentId" ? documentId() : queryData.orderBy : documentId()));

            if (page[0] > page[1] && !cursorDirection) { // Going up
                currentQueryAnchor.endKey && constraints.push(startAfter(currentQueryAnchor.endKey));
                constraints.push(limit(10));
                if (!loadMoreFromQuery) {
                    currentQueryAnchor = {...currentQueryAnchor, startQueryPos: currentQueryAnchor.endQueryPos};
                }
            } else if (page[0] < page[1] && !cursorDirection) { // Going down
                if (!loadMoreFromQuery) {
                    currentQueryAnchor = {...currentQueryAnchor, endQueryPos: currentQueryAnchor.startQueryPos};
                }
                constraints.push(limitToLast(10));
                if (currentQueryAnchor.startKey) {
                    currentQueryAnchor.startKey && constraints.push(endBefore(currentQueryAnchor.startKey));
                } else {
                    currentQueryAnchor.startKey && constraints.push(endAt(currentQueryAnchor.startKey));
                }
            } else {
                if (cursorDirection === "decrease") {
                    constraints.push(limitToLast(10));
                } else {
                    constraints.push(limit(10));
                }
            }
            return constraints;
        };

        const constraints = createQuery(querySchema);
        // console.log(queryId, "constraints", constraints)

        const q = query(collection(db, ...(querySchema.path as [any])), ...(constraints));
        const queryResults:{[key:string]: unknown} = {};

        const queryData = await getDocs(q);

        // console.log("queryData.size", queryData.size)


        const reverseIfBack = (docs:QueryDocumentSnapshot<DocumentData>[]) => {
            if (page[0] < page[1]) {
                return docs.reverse();
            }
            return docs;
        };

        let index = 0;
        reverseIfBack(queryData.docs).forEach((doc: QueryDocumentSnapshot) => {
            if (Object.keys(queryResults).length + Object.keys(itemList).length === 10) {
                return;
            }

            let position = Object.keys(itemList).length+(page[0]-1)*10+index+1;
            if (page[0] < page[1]) {
                position = (page[0])*10-index-Object.keys(itemList).length;
            }

            // console.log(index, "doc.id", doc.id, position, "E", prevEntries[doc.id])

            if (itemList[doc.id] || (prevEntries[doc.id] && prevEntries[doc.id] !== position)) {
                console.log("Removing ", doc.id, ": E=", prevEntries[doc.id], ", G=", position);
                return;
            }

            const item = doc.data();
            item.id = doc.id;
            queryResults[doc.id] = item;
            index = index+1;

            if (prevEntries[doc.id]) return;
            prevEntries[doc.id] = position;
        });

        if (cursorDirection === "decrease" || page[0] < page[1]) {
            itemList = {...Object.fromEntries(Object.entries(queryResults).reverse()), ...itemList};
        } else {
            itemList = {...itemList, ...queryResults};
        }

        setPrevEntryIds(prevEntries);

        if (queryData.size < 10 && Object.keys(itemList).length < 10) {
            if (page[0] > page[1] && cursorPos+1 < data.length) {
                console.log("Increase query index");
                return getDataFromQuery(itemList, {...currentQueryAnchor, endQueryPos: currentQueryAnchor.endQueryPos+1}, "increase", prevEntries);
            } else if (page[0] < page[1] && cursorPos > 0) {
                console.log("Decrease query index");
                return getDataFromQuery(itemList, {...currentQueryAnchor, startQueryPos: currentQueryAnchor.startQueryPos-1}, "decrease", prevEntries);
            }
        }

        if (Object.keys(itemList).length < 10 && queryData.size === 10) {
            console.log("Shorter than ten");
            // if(loadMoreFromQuery){return}
            return getDataFromQuery(itemList, {...currentQueryAnchor, startKey: Object.keys(itemList)[0], endKey: Object.keys(itemList).slice(-1)[0]}, undefined, prevEntries, true);
        }

        if (queryData.size === 0 &&
            Object.keys(itemList).length === 0 &&
            currentQueryAnchor.endQueryPos+1 === data.length &&
            page[0] > 1) {
            setTableData({});
            setQueryAnchor((a) => ({...a, startKey: ""}));
            return;
        }

        const listenForUpdates = () => {
            if (!Object.keys(itemList).length) return;
            console.log("Fetching filter table updates")
            const itemListUpdateQuery = query(collection(db, ...(querySchema.path as [any])), where(documentId(), "in", Object.keys(itemList)));
            const itemUpdateSnapshot = onSnapshot(itemListUpdateQuery, (querySnapshot) => {
                querySnapshot.docs.forEach((doc) => {
                    setTableData((data) => {
                        const newData = {...data, [doc.id]: {...doc.data(), id: doc.id}};
                        return newData;
                    });
                });
            });
            setDataListenerUnsubscribe(() => itemUpdateSnapshot);
        };
        listenForUpdates();

        setQueryAnchor({...currentQueryAnchor, startKey: Object.keys(itemList)[0], endKey: Object.keys(itemList).slice(-1)[0]});
        setTableData(itemList);
    };

    useEffect(() => {
        if (!filters) return;
        setPage([1, 0]);
        setTableData({});
        setQueryAnchor({startKey: "", endKey: "", startQueryPos: 0, endQueryPos: 0});
        setPrevEntryIds({});
        dataListenerUnsubscribe && dataListenerUnsubscribe();
    }, [filters]);

    // Fetch new data when queries or page change
    useEffect(() => {
        console.log("SETTING TABLE DATA")
        getDataFromQuery();
        dataListenerUnsubscribe && dataListenerUnsubscribe();
    }, [page]);

    return ({...{tableData, setPage, setFilters, page}});
}

export function useProviderContactPaginator({data, institute, user, event, view, eventId, filters}:{data:QueryObject[], institute: InstituteData, event?: Partial<ExternalEvent>, eventId?: string, user: UserData, view: "list"|"table", filters?: FilterObject}) {
    const [query, setQuery] = useState<QueryObject[]>();
    const firebaseQuery = new FirebaseQuery();

    const getAdditionalData = async (k: string, v: ProviderContactData|ExternalEventAttendee):Promise<[string, any]|undefined> => {
        if (eventId) {
            const providerData = await firebaseQuery.getDocData(["providerContacts", (v as ExternalEventAttendee).providerContactId]).catch(() => undefined) as ProviderContactData|undefined;
            if (!providerData) return;
            const attendeeData = v as ExternalEventAttendee;
            return [k, {
                contact: `${providerData.contactForename} ${providerData.contactSurname}`,
                email: providerData.providerEmail,
                employer: providerData.name,
                capacity: attendeeData.capacity,
                remainingSpaces: (attendeeData.capacity || 0) - (attendeeData.students || [])?.length - Object.values(attendeeData.schools || {}).reduce((partialSum, a) => partialSum + a, 0)
            }]
        }
        if ((v as ProviderContactData).savedBy?.[user.oId]?.activities?.includes("workExperience")) {
            const placementsCount = await firebaseQuery.getCount("placementListings", [where(`savedBy.${user.oId}.exists`, "==", true), where("providerContactId", "==", k)]);
            return [k, {...v, placements: placementsCount}];
        }
        return [k, v];
    }

    const {tableData, pageUp, pageDown, setFilters, page, setView, loading} = useDataViewerPaginator({view, filters, queryLimit: 10, data: query, additionalEntryProcessing: getAdditionalData})


    useEffect(() => {
        console.log("Event ID OID", eventId, event?.oId);

        if (eventId && event?.oId) {
            console.log("Setting Query");
            setQuery([{
                path: ["externalEventAttendees"],
                where: [
                    ["oId", "==", event.oId],
                    ["eventId", "==", eventId],
                    ["status", "==", "providerConfirmed"],
                ]
            }]);
            return;
        }


        if (user.viewSchools === "none" && user.userType === "Staff" && user.userGroup !== "admin") {
            setQuery(undefined);
            return;
        }

        const statuses = ["providerReviewed", "uploaded", "approved"];

        // if (user.viewSchools === "some") {
        //     if (!user.visibleSchools || user.visibleSchools.length === 0) {
        //         setQuery(undefined);
        //         return;
        //     }
        //     const constraints:QueryObjectConstraint = [
        //             [`savedBy.${user.oId}.exists`, "==", true],
        //             [`savedBy.${user.oId}.MATschools`, "array-contains-any", user.visibleSchools]
        //     ];
        //     setQuery(statuses.map((status) => ({
        //         path: ["providerContacts"],
        //         where: [...constraints, [`savedBy.${user.oId}.status`, "==", status]],
        //     }) as QueryObject))
        //     return;
        // }

        const constraints:QueryObjectConstraint[] = [
            [`savedBy.${user.oId}.exists`, "==", true],
        ]

        // if (event && event.activityId) {
        //     constraints.push([`savedBy.${user.oId}.activities`, "array-contains", event.activityId]);
        // }

        if (user.userType === "Students") {
            constraints.push([`savedBy.${user.oId}.cohorts.${user.cohort}.listed`, "==", true]);
        }

        const queries = statuses.map((status) => ({
            path: ["providerContacts"],
            where: [...constraints, [`savedBy.${user.oId}.status`, "==", status]],
        }) as QueryObject)

        const links = Object.entries(institute.trustLinks || {});
        if (links.length > 0 && user.userType === "Staff") {
            queries.push(...links.filter(([, link]) => link.status === "approved").map(([linkOId]) => ({
                path: ["providerContacts"],
                where: [[`savedBy.${linkOId}.exists`, "==", true],
                    [`savedBy.${linkOId}.status`, "==", "approved"],
                    ((event && event.activityId) ? [`savedBy.${linkOId}.activities`, "array-contains", event.activityId] : undefined)]
                .filter((i) => i)
            } as QueryObject)))
        }

        setQuery(queries);
    }, [user]);

    return ({...{tableData, pageUp, pageDown, setFilters, page, setView, loading}});
}

type UserPaginatorParams = {
    user: UserData,
    cohort?: string,
    data: QueryObject[],
    search?: string,
    userType: "Staff"|"Students",
    sort?: string
}



const algoliaUsersSearch = async (data: QueryObject[], user: UserData, query?: string, sort?: [string, {value: string, direction: "asc"|"desc"}], page?: number, filters?: FilterObject, limit?: number, cohort?: string, userType?: string) => {
    const algoliaClient = algoliasearch("A0ZB50I7VS", user.algoliaKey);
    const userIndex = algoliaClient.initIndex(sort ? `users_${Object.values(sort[1]).join("_")}` : "users");
        

    const items = (await Promise.all(data.map(async (queryObj) => {
        if (!queryObj.where) return;
        let userSearchString = queryObj.where.map(([k, e, v]) => {
            if (e === "==") return `${k}:${quoteAlgoliaFilterIfNeeded(v)}`; // Equality check
            if (e === "!=") return `${k}:-${quoteAlgoliaFilterIfNeeded(v)}`; // Not equal check
            if (e === "<") return `${k}:<${quoteAlgoliaFilterIfNeeded(v)}`; // Less than check
            if (e === "<=") return `${k}:<=${quoteAlgoliaFilterIfNeeded(v)}`; // Less than or equal check
            if (e === ">") return `${k}:>${quoteAlgoliaFilterIfNeeded(v)}`; // Greater than check
            if (e === ">=") return `${k}:>=${quoteAlgoliaFilterIfNeeded(v)}`; // Greater than or equal check
            if (e === "array-contains") return `${k}:${quoteAlgoliaFilterIfNeeded(v)}`; // Array contains check (string format)
            if (e === "array-contains-any") return `${k}:"${(v as string[]).join('","')}"`; // Array contains any of the values (string format)
            if (e === "in") return `(${(v as string[]).map((value) => `${k}:${quoteAlgoliaFilterIfNeeded(value)}`).join(' OR ')})`; // In check
            if (e === "not-in") return (v as string[]).map((value) => `${k}:-${quoteAlgoliaFilterIfNeeded(value)}`).join(' AND '); // In check
            return;
        }).join(" AND ");

        filters && Object.entries(filters).filter(([, filter]) => filter.value).map(([id, filter]) => {
            userSearchString = userSearchString + ` AND ${id}:${quoteAlgoliaFilterIfNeeded(filter.value)}`;
        });

        const options:{[key: string]: any} = {
            filters: userSearchString,
        }

        if (data.length === 1) {
            options.hitsPerPage = limit;
            options.page = page ? page - 1 : undefined;
        }

        console.log("Search", options);

        const searchUsersHits = await userIndex.search<UserData>(query || "", options);

        console.log(searchUsersHits.hits);
        
        const i = (searchUsersHits ? (await Promise.all(searchUsersHits.hits.map(async (hit) => {
                return [hit.objectID, hit];
            }))) : []).filter((e) => e !== undefined) as [string, UserData][];

        return i;
    }))).filter((i) => i) as [string, UserData][][];

    return Object.fromEntries(items.flat());
}


type NewUserPaginatorParams = {
    user: UserData,
    cohort?: string,
    oId?: string,
    queryConstraints?: QueryObjectConstraint[],
    ql?: number,
    view: "list"|"table",
    filters?: FilterObject,
    institute: InstituteData,
    userType: "Staff"|"Students",
    initialSort?: string
    trustSchoolsIDs?: string[]
}


export function useNewCohortUserPaginator({user, oId, trustSchoolsIDs, institute, initialSort, filters, view, cohort, queryConstraints, ql=DEFAULTQUERYLIMIT, userType}:NewUserPaginatorParams) {
    const [query, setQuery] = useState<QueryObject[]>();

    const firebaseQuery = new FirebaseQuery();

    const sorts:Sorts = {
        [`${userType} Forename - Asc`]: {
            value: "details.forename",
            direction: "asc",
        },
        [`${userType} Forename - Desc`]: {
            value: "details.forename",
            direction: "desc",
        },
        [`${userType} Surname - Asc`]: {
            value: "details.surname",
            direction: "asc",
        },
        [`${userType} Surname - Desc`]: {
            value: "details.surname",
            direction: "desc",
        },
        [`${userType} Email - Asc`]: {
            value: "email",
            direction: "asc",
        },
        [`${userType} Email - Desc`]: {
            value: "email",
            direction: "desc",
        }
    }
    
    useEffect(() => {

        const getAccess = async () => {
            // Sets the query of for the DataViewerPaginator
            if(user.product !== "institutes" || user.userType !== "Staff") {
                setQuery(undefined);
                return;
            }

            // Set the query based on defined groups to make it easier.
            if (institute.package === "careersHub") {
                const qConstraints:QueryObjectConstraint[] = [["oId", "==", user.oId], ["userType", "==", userType]];
                queryConstraints && qConstraints.unshift(...queryConstraints);

                setQuery([{
                    path: ["users"],
                    where: qConstraints,
                }]);
                return;
            }

            if (institute.package === "institutes-one") {
                const constraints:QueryObjectConstraint[] = [["oId", "==", user.oId], ["userType", "==", userType]];
                cohort && constraints.push(["cohort", "==", cohort]);
                queryConstraints && constraints.unshift(...queryConstraints);


                if (user.userGroup === "admin" || (user.viewCohorts === "all" && user.viewStudents === "all")) {
                    setQuery([{
                        path: ["users"],
                        where: constraints,
                    }]);
                    return;
                };
                
                if (!user.viewCohorts || user.viewCohorts === "none" || !user.viewStudents || user.viewStudents === "none" || (user.viewCohorts === "some" && !user.visibleCohorts?.length) || (user.viewStudents === "some" && !user.studentFilterValues?.length)) return setQuery(undefined);
                
                if (user.viewStudents === "some") {
                    constraints.push([`details.${user.studentFilter}`, "in", user.studentFilterValues as string[]]);
                }

                if (cohort) {
                    const canViewCohort = user.viewCohorts === "all" || (user.viewCohorts === "some" && user.visibleCohorts?.includes("cohort"));
                    setQuery(canViewCohort ? [{
                        path: ["users"],
                        where: constraints,
                    }] : undefined);
                    return;
                } else {
                    // No cohort.
                    if (user.viewCohorts === "all") {
                        setQuery([{
                            path: ["users"],
                            where: constraints,
                        }]);
                        return;
                    };
                    if (user.viewCohorts === "some" && user.visibleCohorts) {
                        constraints.push(["cohort", "in", user.visibleCohorts]);
                        setQuery([{
                            path: ["users"],
                            where: constraints,
                        }]);
                        return;
                    }
                    return setQuery(undefined);
                }
            }

            if (institute.package === "institutes-two") {
                const buildQuery = (additionalFilters?: QueryObjectConstraint[]) => {
                    // If no oId or cohort, make it for any.

                    let constraints:QueryObjectConstraint[] = [];

                    if (trustSchoolsIDs && !(cohort || oId)) {
                        setQuery([user.oId, ...trustSchoolsIDs].map((orgId) => ({
                            path: ["users"],
                            where: [...(queryConstraints || []), ...(additionalFilters || []), ["oId", "==", orgId], ["userType", "==", userType]],
                        })))
                        return;
                    }

                    cohort && constraints.push(["cohort", "==", cohort]);
                    oId && constraints.push(["oId", "==", oId]);
                    userType && constraints.push(["userType", "==", userType]);
                    queryConstraints && constraints.unshift(...queryConstraints);
                    additionalFilters && constraints.unshift(...additionalFilters);
                    setQuery([{
                        path: ["users"],
                        where: constraints,
                    }]);
                }
                if (user.userGroup === "admin" || (user.viewSchools === "all" && user.viewCohorts === "all" && user.viewStudents === "all")) return buildQuery();

                if (!user.viewCohorts || !user.viewSchools || user.viewSchools === "none" || user.viewCohorts === "none") return setQuery(undefined);
                if (!user.viewStudents || user.viewStudents === "none" || (user.viewStudents === "some" && !user.studentFilterValues?.length) || (user.viewCohorts === "some" && !user.visibleCohorts?.length) || (user.viewSchools === "some" && !user.visibleSchools?.length)) return setQuery(undefined);
                
                if (user.viewStudents === "some") {
                    buildQuery([[`details.${user.studentFilter}`, "in", user.studentFilterValues as string[]]]);
                }
                
                if (cohort) {
                    if (user.viewCohorts === "some" && !user.visibleCohorts?.includes(cohort)) return setQuery(undefined);
                        
                    if (user.viewSchools === "some") {
                        const cohortData = await firebaseQuery.getDocData(["cohorts", cohort]) as CohortData;
                        if (!cohortData.oId || !user.visibleSchools?.includes(cohortData.oId)) return setQuery(undefined);
                    }
                } else {
                    if (user.viewCohorts === "some" && user.visibleCohorts) {
                        buildQuery([["cohort", "in", user.visibleCohorts]]);
                    } else if (user.viewSchools === "some" && user.visibleSchools) {
                            const cohorts = await firebaseQuery.getDocsWhere("cohorts", [where("oId", "in", user.visibleSchools)]);
                            buildQuery([["cohort", "in", Object.keys(cohorts || {})]]);
                    }
                }
            }

            setQuery(undefined);
        }
        getAccess();
    }, [user, queryConstraints, cohort]);


    console.log("Query", query);

    const {tableData, pageUp, pageDown, setFilters, page, setView, loading, updateSearch, updateSort, search, sort} = useDataViewerPaginator({view, filters, initialSort, sorts, queryLimit: ql, data: query, onSearch: async (s, sort, page, filters, limit) => await algoliaUsersSearch(query || [], user, s, sort, page, filters, limit, cohort, userType)})


    return {tableData, page, loading, updateSearch, setFilters, setView, pageUp, pageDown, sorts, updateSort, sort, search}
}



export function useCohortUserPaginator({user, cohort, data, search, userType, sort}:UserPaginatorParams) {
    const [tableData, setTableData] = useState<{[key:string]:{[key:string]: unknown}}>({});
    const [queryAnchor, setQueryAnchor] = useState<{startDoc?: QueryDocumentSnapshot, endDoc?: QueryDocumentSnapshot, startQueryPos: number, endQueryPos: number}>({startDoc: undefined, endDoc: undefined, startQueryPos: 0, endQueryPos: 0});
    const [filters, setFilters] = useState<{[key:string]: unknown}>();
    const [sortResultsBy, setSortResultsBy] = useState<string|undefined>(sort);
    const [prevEntryIds, setPrevEntryIds] = useState<{[key:string]:number}>({});
    const [page, setPage] = useState([1, 0]);
    const [dataListenerUnsubscribe, setDataListenerUnsubscribe] = useState<Unsubscribe|undefined>();
    const [queries, setQueries] = useState<QueryConstraint[][]>();
    const [prevSearch, setPrevSearch] = useState<string>();

    const algoliaClient = algoliasearch(process.env.NODE_ENV === "development" ? "A0ZB50I7VS" : "A0ZB50I7VS", user.algoliaKey);
    const usersIndex = algoliaClient.initIndex("users");

    const sortOptions = {
        ["Forename - Asc"]: ["details.forename", "asc"],
        ["Forename - Desc"]: ["details.forename", "desc"],
        ["Surname - Asc"]: ["details.surname", "desc"],
        ["Surname - Desc"]: ["details.surname", "asc"],
        ["Email - Asc"]: ["email", "asc"],
        ["Email - Desc"]: ["email", "desc"],
    }
    
    useEffect(() => {
        if (user.userType !== "Staff") {
            console.log("Not a staff member", user)
            setQueries(undefined);
            return;
        }
        if (
            (!user.viewCohorts && user.userGroup !== "admin") ||
            (user.viewCohorts === "none" && user.userGroup !== "admin") ||
            (user.viewCohorts === "some" && user.userGroup !== "admin" && cohort !== "all" && !user.visibleCohorts?.includes(cohort || ""))) {
            setQueries(undefined);
            return;
        }
        setQueries(() => {
            const finalConstraints:QueryConstraint[][] = [];
            console.log("data", data, data.length)
            for (var i = 0; i < data.length; i++) {
                const item = data[i]
                console.log("item", item)
                const constraints:QueryConstraint[] = [];
                item.where && item.where.forEach((w) => {
                    constraints.push(where(...w));
                });

                constraints.push(where("oId", "==", user.oId));
                cohort && cohort !== "all" && constraints.push(where("cohort", "==", cohort));

                if (user.userGroup === "admin" || user.viewStudents === "all") {
                    finalConstraints.push(constraints);
                    continue
                }
                if (!user.studentFilter || !user.studentFilterValues) continue;
                user.studentFilterValues.forEach((filterValue) => {
                    user.studentFilter && finalConstraints.push([...constraints, where("details."+user.studentFilter, "==", filterValue)])
                })
            };
            console.log("final", finalConstraints)
            return finalConstraints;
        });
    }, []);


    const getDataFromQuery = async (
        itemList: {[key: string]: QueryDocumentSnapshot<DocumentData>} = {}, currentQueryAnchor=queryAnchor, cursorDirection?:"increase"|"decrease"|undefined, prevEntries=prevEntryIds, loadMoreFromQuery=false):Promise<any> => {
        
        if (!queries?.length) {
            setTableData({});
            return;
        }
        console.log("q", queries)
        let cursorPos:number;
        if (page[0] > page[1]) {
            cursorPos = currentQueryAnchor.endQueryPos;
        } else {
            cursorPos = currentQueryAnchor.startQueryPos;
        }

        const querySchema = queries[cursorPos];
        console.log("schema", querySchema)
        const createQuery = (mConstraints:QueryConstraint[]) => {

            console.log("mConstraints", mConstraints);
            const fConstraints = [...mConstraints];
            
            let addOrderBy = true;
            filters && Object.entries(filters).forEach(([key, value]) => {
                if (Array.isArray(value)) {
                    value.forEach((v) => {
                        if (typeof v === "object") {
                            if (v instanceof QueryOrderByConstraint) {
                                addOrderBy = false;
                            }
                            fConstraints.push(v as QueryFieldFilterConstraint|QueryOrderByConstraint);
                        } else {
                            fConstraints.push(where(key, "==", v));
                        }
                    })
                } else if (typeof value === "object") {
                    if (value instanceof QueryOrderByConstraint) {
                        addOrderBy = false;
                    }
                    fConstraints.push(value as QueryFieldFilterConstraint|QueryOrderByConstraint);
                } else {
                    fConstraints.push(where(key, "==", value));
                }
            });
            console.log("Add order by", addOrderBy);
            if (addOrderBy) {
                if (sortResultsBy) {
                    fConstraints.push(orderBy(sortOptions[sortResultsBy][0], sortOptions[sortResultsBy][1]));
                } else {
                    fConstraints.push(orderBy(documentId()));

                }
            }


            if (page[0] > page[1] && !cursorDirection) { // Going up
                currentQueryAnchor.endDoc && fConstraints.push(startAfter(currentQueryAnchor.endDoc));
                fConstraints.push(limit(10));
                if (!loadMoreFromQuery) {
                    currentQueryAnchor = {...currentQueryAnchor, startQueryPos: currentQueryAnchor.endQueryPos};
                }
            } else if (page[0] < page[1] && !cursorDirection) { // Going down
                if (!loadMoreFromQuery) {
                    currentQueryAnchor = {...currentQueryAnchor, endQueryPos: currentQueryAnchor.startQueryPos};
                }
                fConstraints.push(limitToLast(10));
                if (currentQueryAnchor.startDoc) {
                    currentQueryAnchor.startDoc && fConstraints.push(endBefore(currentQueryAnchor.startDoc));
                } else {
                    currentQueryAnchor.startDoc && fConstraints.push(endAt(currentQueryAnchor.startDoc));
                }
            } else {
                if (cursorDirection === "decrease") {
                    fConstraints.push(limitToLast(10));
                } else {
                    fConstraints.push(limit(10));
                }
            }
            return fConstraints;
        };

        const constraints = createQuery(querySchema);
        // console.log(queryId, "constraints", constraints)

        const q = query(collection(db, "users"), ...(constraints));
        const queryResults:{[key:string]: QueryDocumentSnapshot<DocumentData>} = {};

        const queryData = await getDocs(q);

        // console.log("queryData.size", queryData.size)


        const reverseIfBack = (docs:QueryDocumentSnapshot<DocumentData>[]) => {
            if (page[0] < page[1]) {
                return docs.reverse();
            }
            return docs;
        };

        let index = 0;
        reverseIfBack(queryData.docs).forEach((doc: QueryDocumentSnapshot) => {
            if (Object.keys(queryResults).length + Object.keys(itemList).length === 10) {
                return;
            }

            let position = Object.keys(itemList).length+(page[0]-1)*10+index+1;
            if (page[0] < page[1]) {
                position = (page[0])*10-index-Object.keys(itemList).length;
            }

            // console.log(index, "doc.id", doc.id, position, "E", prevEntries[doc.id])

            if (itemList[doc.id] || (prevEntries[doc.id] && prevEntries[doc.id] !== position)) {
                console.log("Removing ", doc.id, ": E=", prevEntries[doc.id], ", G=", position);
                return;
            }

            queryResults[doc.id] = doc;
            index = index+1;

            if (prevEntries[doc.id]) return;
            prevEntries[doc.id] = position;
        });

        if (cursorDirection === "decrease" || page[0] < page[1]) {
            itemList = {...Object.fromEntries(Object.entries(queryResults).reverse()), ...itemList};
        } else {
            itemList = {...itemList, ...queryResults};
        }

        setPrevEntryIds(prevEntries);

        if (queryData.size < 10 && Object.keys(itemList).length < 10) {
            if (page[0] > page[1] && cursorPos+1 < queries.length) {
                return getDataFromQuery(itemList, {...currentQueryAnchor, endQueryPos: currentQueryAnchor.endQueryPos+1}, "increase", prevEntries);
            } else if (page[0] < page[1] && cursorPos > 0) {
                return getDataFromQuery(itemList, {...currentQueryAnchor, startQueryPos: currentQueryAnchor.startQueryPos-1}, "decrease", prevEntries);
            }
        }

        if (Object.keys(itemList).length < 10 && queryData.size === 10) {
            return getDataFromQuery(itemList, {...currentQueryAnchor, startDoc: Object.values(itemList)[0], endDoc: Object.values(itemList).slice(-1)[0]}, undefined, prevEntries, true);
        }

        if (queryData.size === 0 &&
            Object.keys(itemList).length === 0 &&
            currentQueryAnchor.endQueryPos+1 === queries.length &&
            page[0] > 1) {
            setTableData({});
            setQueryAnchor((a) => ({...a, startKey: ""}));
            return;
        }

        const listenForUpdates = () => {
            if (!Object.keys(itemList).length) return;
            console.log("Fetching cohort user data")

            const itemListUpdateQuery = query(collection(db, "users"), where(documentId(), "in", Object.keys(itemList)));
            const itemUpdateSnapshot = onSnapshot(itemListUpdateQuery, (querySnapshot) => {
                querySnapshot.docs.forEach((doc) => {
                    setTableData((data) => {
                        const newData = {...data, [doc.id]: {...doc.data(), id: doc.id}};
                        return newData;
                    });
                });
            });
            setDataListenerUnsubscribe(() => itemUpdateSnapshot);
        };
        listenForUpdates();

        setQueryAnchor({...currentQueryAnchor, startDoc: Object.values(itemList)[0], endDoc: Object.values(itemList).slice(-1)[0]});
        setTableData(Object.fromEntries(Object.entries(itemList).map(([k, v]) => [k, {id: k, ...v.data()}])));
    };

    const searchUsers = async () => {
            if (!search) return;

            let userSearchString = `userType:${userType} AND oId:${user.oId} AND product:${user.product}`
            if (cohort && cohort !== "all") {
                userSearchString = userSearchString + ` AND cohort:${cohort}`;
            }
            Object.entries(filters || {}).forEach(([field, value]) => {
                userSearchString = userSearchString + ` AND ${field}:${value}`;
            })

            console.log("Going", page[0] > page[1] ? "up" : "down");
            console.log("Start at", page[0] > page[1] ? page[1] : page[1] - 1)

            const searchStudentHits = await usersIndex.search<UserData>(search, {
                filters: userSearchString,
                length: 10,
                offset: 10 * (page[0] > page[1] ? page[1] : (page[0] - 1))
            });

            const results = Object.fromEntries(await Promise.all(searchStudentHits.hits.map(async (hit) => {
                    return [hit.objectID, hit as UserData]
                })))

            setPrevSearch(search);
            setTableData(results);
    }

    useEffect(() => {
        if (!filters && !prevSearch && !search) return;

        if (search && prevSearch) {
            console.log("New search")
            setPrevSearch(search);
            return;
        }
        console.log("Setting page");
        setPage([1, 0]);
        console.log("Set page");

        setTableData({});
        setQueryAnchor({startQueryPos: 0, endQueryPos: 0});
        setPrevEntryIds({});
        dataListenerUnsubscribe && dataListenerUnsubscribe();
    }, [filters, search, sortResultsBy]);

    // Fetch new data when queries or page change
    useEffect(() => {
        if (search) {
            console.log("PAGE", page);
            searchUsers();
            return;
        }
        getDataFromQuery();
        dataListenerUnsubscribe && dataListenerUnsubscribe();
    }, [page, queries, prevSearch]);


    const setSort = (s: string) => setSortResultsBy(s);

    return ({...{tableData, setPage, page, setFilters, setSort, sortOptions: Object.keys(sortOptions), sortBy: sortResultsBy}});
}


export function useAdmissionsPaginator({data}:{data:{[key:string]:{[key:string]: unknown}}}) {
    const [tableData, setTableData] = useState<{[key:string]:{[key:string]: unknown}}>({});
    const [page, setPage] = useState([1, 0]);
    const [filters, setFilters] = useState<{[key:string]: unknown}>();

    useEffect(() => {
        // Get slice of results and get data from those.
        setTableData(Object.fromEntries(Object.entries(data).slice((page[0] - 10)*10, page[0]*10)));
    }, [page]);

    useEffect(() => {
        // Filter then get first 10 results.
    }, [filters]);

    return ({...{tableData, setPage, setFilters}});
}

type LazyLoadQueryParams = {
    path: string|string[],
    constraints?: QueryConstraint|QueryConstraint[],
    number: number,
    endResultsText?: string,
    noResultsText?: string,
    onItemFetched?: (item: unknown) => Promise<{[key: string]: unknown}>
}

export function useLazyLoadQueryList({path, constraints, number, onItemFetched}:LazyLoadQueryParams) {
    const [items, setItems] = useState<{[key:string]:{[key:string]:unknown}}>({});
    const [loadMoreIcon, setLoadMoreIcon] = useState<boolean>(false);

    const [lastItem, setLastItem] = useState<QueryDocumentSnapshot<DocumentData>>();
    const firebaseQuery = new FirebaseQuery();

    const reset = () => {
        setItems({});
        setLastItem(undefined);
    };

    const loadMore = async () => {
        setLoadMoreIcon(true);
        let formattedConstraints:QueryConstraint[] = [];

        if (constraints) {
            formattedConstraints = (Array.isArray(constraints) ? [...constraints, limit(number)] : [constraints, limit(number)]);
        }
        if (lastItem) {
            formattedConstraints.push(startAfter(lastItem));
        }
        const documents = await firebaseQuery.getDocsWhere(path, formattedConstraints, true) as QuerySnapshot<DocumentData>;

        console.log("docs", documents);

        if (documents?.empty) {
            setLoadMoreIcon(false);
            return;
        }
        setLastItem(documents?.docs[documents?.docs?.length-1]);

        const processedItems = Object.fromEntries(await Promise.all(Object.values(documents?.docs || {}).map(async (doc) => {
            const itemObj = {...doc.data(), id: doc.id, docPath: doc.ref};
            return [doc.id, onItemFetched ? await onItemFetched(itemObj) : itemObj];
        })))

        setItems((i) => ({...i, ...processedItems}));
        setLoadMoreIcon(false);
    };
    useEffect(() => {
        loadMore();
    }, []);

    return ({...{items, loadMore, loadMoreIcon, reset}});
}


// type PublicPlacementListingLoaderParams = {
//     providerId?: string,
//     number: number,
// }

// export function usePublicPlacementListingLoader({providerId, number=5}:PublicPlacementListingLoaderParams) {
//     const [items, setItems] = useState<{[key:string]:{[key:string]:unknown}}>({});
//     const [loadMoreIcon, setLoadMoreIcon] = useState<boolean>(false);

//     const [lastItem, setLastItem] = useState<QueryDocumentSnapshot<DocumentData>>();
//     const firebaseQuery = new FirebaseQuery();

//     const reset = () => {
//         setItems({});
//         setLastItem(undefined);
//     };

//     const loadMore = async () => {
//         setLoadMoreIcon(true);
//         let formattedConstraints:QueryConstraint[] = [
//             where("status", "==", "listed"),
//             limit(number)
//         ];

//         if (providerId) {
//             formattedConstraints.push(where("providerId", "==", providerId));
//         }
//         if (lastItem) {
//             formattedConstraints.push(startAfter(lastItem));
//         }
//         const documents = await firebaseQuery.getDocsWhere("placementListings", formattedConstraints, true) as QuerySnapshot<DocumentData>;

//         console.log("docs", documents.docs);
//         setLastItem(documents.docs[documents.docs.length-1]);

//         const processedItems = Object.fromEntries(await Promise.all(Object.values(documents.docs).map(async (doc) => {
//             let itemObj = {...doc.data(), id: doc.id} as PlacementListing;

//             if (itemObj.addressId) {
//                 const address = await firebaseQuery.getDocData(["addresses", itemObj.addressId]) as Address;
//                 delete address.id;
//                 itemObj = {...address, ...itemObj};
//             }
//             if (itemObj.applicantWorkflowId) {
//                 const applicantWorkflow = (await firebaseQuery.getDocData(["applicantWorkflows", itemObj.applicantWorkflowId]) as ApplicantWorkflow).workflow.filter((i) => i.id === 1)[0];
//                 const applicantFiles = applicantWorkflow.files ? Object.fromEntries(await Promise.all(applicantWorkflow.files?.map(async (fileId) => {
//                     const file = await firebaseQuery.getDocData(["files", fileId]);
//                     file.url = await getDownloadURL(ref(storage, `providers/${itemObj.providerId}/${file.fileName}`));

//                     return [fileId, file];
//                 }))) : [];
//                 const applicantForms = applicantWorkflow.forms ? Object.fromEntries(await Promise.all(applicantWorkflow.forms?.map(async (formId) => {
//                     return [formId, await firebaseQuery.getDocData(["forms", formId])];
//                 }))) : [];

//                 applicantWorkflow.viewableFiles = applicantFiles;
//                 applicantWorkflow.formDetails = applicantForms;
//                 itemObj = {...itemObj, applicantWorkflow: [applicantWorkflow]};
//             }
//             return [doc.id, itemObj];
//         })))

//         setItems((i) => ({...i, ...processedItems}));
//         setLoadMoreIcon(false);
//     };
//     useEffect(() => {
//         loadMore();
//     }, []);

//     return ({...{items, loadMore, loadMoreIcon, reset}});
// }

// export type ApplicationHookParams = {
//     successText: {
//         submitted: {
//             title: string;
//             desc: string;
//         };
//         draftSaved: {
//             title: string;
//             desc: string;
//         };
//         stageComplete: {
//             title: string;
//             desc: string;
//         };
//         outcome: {
//             title: string;
//             desc: string;
//         };
//     };
//     setSuccessPopup: import("react").Dispatch<import("react").SetStateAction<"submitted" | "draftSaved" | "stageComplete" | "outcome" | undefined>>;
//     openSuccessPopup: (type: "submitted" | "draftSaved" | "stageComplete" | "outcome") => void;
//     setFApplication: import("react").Dispatch<import("react").SetStateAction<Partial<Application>>>;
//     fApplication: Partial<Application>;
//     draftSaved: boolean;
//     profileUrl: string | undefined;
//     successPopup: "submitted" | "draftSaved" | "stageComplete" | "outcome" | undefined;
//     fApplicationId: string | undefined;
//     fListing: PlacementListing | false | undefined;
//     student: UserData | undefined;
//     fProvider: {
//         details?: ProviderData;
//         profile?: string;
//         id?: string;
//     } | undefined;
//     onFApply: (draft?: boolean) => Promise<void>;
//     setFormComplete: (formId: string, e: {
//         [key: string]: unknown;
//     }) => void;
//     viewFile: (file: string, onOpen: (url: string) => void) => void
//     addFile: (files: string[], fileId: number) => void;
//     uploadedFiles?: {
//         [key: string]: FileItem;
//     };
//     getCurrentStage: (stage: number) => Promise<{
//         stage: ApplicantStage;
//         completedSections: {
//             submitted?: string | undefined;
//             filesViewed?: string[] | undefined;
//             formsCompleted?: {
//                 [key: string]: unknown;
//             } | undefined;
//             filesUploaded?: {
//                 [key: number]: string[];
//             } | undefined;
//         };
//     }>;
//     currentStageComplete?: boolean;
//     progressStage: (type: number | "accept" | "reject", e?: {
//         feedback?: string;
//     }) => Promise<void>;
// };

// export function useCreateApplicationRenderer({user, listingId, listing, provider, application, applicationId, orgContext}:
//     {user:UserData, listingId:string, applicationId?: string, listing?: PlacementListing, application?: Partial<Application>,
//         orgContext?: {details: ProviderData, addresses: {[key: string]: OrganisationAddress}, applicantWorkflows: {[key: string]: ApplicantWorkflow}},
//         provider?: {details?: ProviderData, profile?: string, id?: string}}) {

//         const firebaseQuery = new FirebaseQuery();

//         let applicationWithoutAdditionalData = {...(application || {})} as any;
//         delete applicationWithoutAdditionalData.listing;
//         delete applicationWithoutAdditionalData.address;
//         delete applicationWithoutAdditionalData.provider;
    
//         const [fApplication, setFApplication] = useState<Partial<Application>>(application ? applicationWithoutAdditionalData : {
//             uid: user.userType === "Students" ? user.id : undefined,
//             listingId: listingId,
//             addressId: listing?.addressId,
//             stage: 1,
//             reqUserType: "Students",
//             status: "draft"});
//         const [fApplicationId, setFApplicationId] = useState<string|undefined>(applicationId);
//         const [draftSaved, setDraftSaved] = useState(false);
//         const [fProvider, setFProvider] = useState<{details?: ProviderData, profile?: string, id?: string}|undefined>(provider);
//         const [fListing, setFListing] = useState<PlacementListing|false|undefined>(Object.keys(listing || {}).length > 5 ? listing : undefined);
//         const [student, setStudent] = useState<UserData|undefined>(user.userType === "Students" ? user : undefined);
//         const [profileUrl, setProfileUrl] = useState<string>();
//         const [successPopup, setSuccessPopup] = useState<"submitted"|"draftSaved"|"stageComplete"|"outcome">();
//         const [uploadedFiles, setUploadedFiles] = useState<{[key: string]: FileItem}>();
//         const [currentStageComplete, setCurrentStageComplete] = useState<boolean>();

//         useEffect(() => {
//             const getListing = async () => {
//                 console.log("Checking ID")
//                 if (!listingId) return;
//                 console.log("Getting listing")
//                 console.log("LISTING PARAM", listing, Object.keys(listing || {}).length > 5);

//                 const listingData = (Object.keys(listing || {}).length > 5) ? listing : (await firebaseQuery.getDocData(["placementListings", listingId]).catch(() => false) as PlacementListing|false);
                
//                 console.log("LISTINGDATA", listingData, Object.keys(listing || {}).length > 5 ? {a: "string"} : "AAA");
                
                
                
//                 const address = listingData ? (user.product === "providers" && orgContext) ? orgContext.addresses[listingData.addressId || ""] : await firebaseQuery.getDocData(["addresses", listingData.addressId as string]) as Address : undefined;
//                 const workflow = listingData ? (user.product === "providers" && orgContext) ? orgContext.applicantWorkflows[listingData.applicantWorkflowId || ""] as ApplicantWorkflow : await firebaseQuery.getDocData(["applicantWorkflows", listingData.applicantWorkflowId as string]) as ApplicantWorkflow : undefined;
            
//                 if (workflow && listingData) {
//                     workflow.workflow = await Promise.all(workflow.workflow.map(async (s) => {
//                         const applicantFiles = s.files ? Object.fromEntries(await Promise.all(s.files?.map(async (fileId: string) => {
//                             const file = await firebaseQuery.getDocData(["files", fileId]);
//                             file.url = await getDownloadURL(ref(storage, `providers/${listingData?.providerId}/${file.fileName}`));
    
//                             return [fileId, file];
//                         }))) : [];
//                         const applicantForms = s.forms ? Object.fromEntries(await Promise.all(s.forms?.map(async (formId: string) => {
//                             return [formId, await firebaseQuery.getDocData(["forms", formId])];
//                         }))) : [];
    
//                         return {...s, viewableFiles: applicantFiles, formDetails: applicantForms};
//                     }));
//                     delete workflow.id;
//                 }
//                 if (address) {
//                     delete address.id;
//                 }

//                 console.log("Setting listing")
//                 setFListing((listingData && workflow) ? {...listingData, ...address, applicantWorkflow: workflow.workflow} as PlacementListing : false);


//                 if ((fProvider?.id === application?.providerId) && user.product === "providers") {
//                     setFProvider({details: orgContext?.details, id: user.oId});
//                 } else if (listingData && listingData?.providerId) {
//                     console.log("Getting provider from DB");
//                     const provider = await firebaseQuery.getDocData(["providers", listingData.providerId]) as ProviderData;
//                     console.log("Provider", provider);
//                     setFProvider({details: provider, id: listingData.providerId});
//                 }
//             }

//             getListing();
//         }, [listingId, listing]);
    
//         useEffect(() => {
//             if (student?.id !== application?.uid) {
//                 if (user.userType === "Students") {
//                     setStudent(user);
//                 } else if (user.product === "providers" && application?.uid) {
//                     firebaseQuery.getDocData(["users", application.uid]).then((s) => setStudent(s as UserData));
//                 }
//             }
//         }, []);
    
//         useEffect(() => {
//             setFListing(Object.keys(listing || {}).length > 5 ? listing : undefined);
//         }, [listing]);
    
//         useEffect(() => {
//             if (provider?.profile) return;
//             if (!provider?.id) return;
    
//             getDownloadURL(ref(storage, `providers/${provider?.id}/profilePic.png`)).then(setProfileUrl).catch(() => null);
//         }, [provider]);
    
    
//         useEffect(() => {
//             console.log("APPLICATIONID", applicationId);
//             if (!applicationId) {
//                 if (!listingId) return;
//                 firebaseQuery.getDocsWhere("applications", [where("status", "not-in", ["approved", "declined"]), where("uid", "==", user.id), where("listingId", "==", listingId)]).then((existingApplication) => {
//                     console.log("EXISTING", existingApplication);
//                     // get uploaded files
//                     if (existingApplication && Object.keys(existingApplication).length) {
//                         setFApplication(Object.values(existingApplication)[0]);
//                         setFApplicationId(Object.keys(existingApplication)[0]);
//                     }
//                 });
//                 return;
//             }
//             if (applicationId) {
//                 setFApplicationId(applicationId);
//                 if (application) {
//                     let applicationWithoutAdditionalData = {...(application || {})} as any;
//                     delete applicationWithoutAdditionalData.listing;
//                     delete applicationWithoutAdditionalData.address;
//                     delete applicationWithoutAdditionalData.provider;
//                     setFApplication(applicationWithoutAdditionalData);
//                 } else {
//                     firebaseQuery.getDocData(["applications", applicationId]).then(setFApplication);
//                 }
//             }
//         }, [application, applicationId]);

//         const getCurrentStage = async (stage: number): Promise<{
//             stage: ApplicantStage; completedSections: {
//                 submitted?: string | undefined;
//                 filesViewed?: string[] | undefined;
//                 formsCompleted?: {
//                     [key: string]: unknown;
//                 } | undefined;
//                 filesUploaded?: {
//                     [key: number]: string[];
//                 } | undefined;
//             };
//         }> => {
//             console.log("fLSITING CURRENT STAGE", fListing);
//             if (!fListing) throw new Error("Listing deleted");
//             if (!fListing?.applicantWorkflowId) throw new Error("No workflow stage");

//             const mApplicantWorkflow = (await firebaseQuery.getDocData(["applicantWorkflows", fListing.applicantWorkflowId]) as ApplicantWorkflow);

//             const stageObj = mApplicantWorkflow?.workflow?.find((s) => s.id === stage);

//             if (!stageObj) throw new Error("Can't find stage.");

//             const applicantFiles = stageObj.files ? Object.fromEntries(await Promise.all(stageObj.files?.map(async (fileId) => {
//                 const file = await firebaseQuery.getDocData(["files", fileId]);
//                 file.url = await getDownloadURL(ref(storage, `providers/${mApplicantWorkflow?.oId}/${file.fileName}`));

//                 return [fileId, file];
//             }))) : [];
//             const applicantForms = stageObj.forms ? Object.fromEntries(await Promise.all(stageObj.forms?.map(async (formId) => {
//                 return [formId, await firebaseQuery.getDocData(["forms", formId])];
//             }))) : [];

//             stageObj.viewableFiles = applicantFiles;
//             stageObj.formDetails = applicantForms;
            
//             return {stage: stageObj, completedSections: fApplication?.completedSections?.[stage] || {}};
//         };
    
//         useEffect(() => {
//             const getUploadedFiles = async () => {
//                 if (!fApplication.completedSections) {
//                     setUploadedFiles({});
//                     return
//                 };
                
//                 const fileIds = Object.values(fApplication.completedSections)
//                     .flatMap(stageData =>
//                     Object.values(stageData.filesUploaded || {}).flatMap(fileIds => fileIds)
//                     );
                
//                 const fileDataPromises = fileIds.map(async (fileId) => {
//                     const fileData = await firebaseQuery.getDocData(["files", fileId]) as FileItem;
//                     fileData.url = await getDownloadURL(ref(storage, `userFiles/${fileData.fileName}`));
                
//                     return [fileId, fileData] as [string, FileItem]
//                 });
                
//                 const fileDataArray = await Promise.all(fileDataPromises);
//                 const fileDataObj = Object.fromEntries(fileDataArray)
//                 setUploadedFiles(fileDataObj)
//             }

//             const addApplication = async () => {
//                 console.log("ADDING APPLICATION");
//                 if (!fListing || !fListing?.id || !fProvider?.id || !student?.id) return;

//                 const applicationData = {
//                     uid: student.id,
//                     listingId: fListing?.id,
//                     addressId: fListing.addressId,
//                     applicantWorkflowId: fListing?.applicantWorkflowId,
//                     providerId: fProvider.id,
//                     stage: 1,
//                     status: "draft",
//                     created: (new Date()).toISOString(),
//                     ...fApplication,
//                 } as Partial<Application>;
//                 const mApplicationId = (await firebaseQuery.add(["applications"], applicationData)).id;
//                 console.log("APPLICATION ADDED");
//                 setFApplicationId(mApplicationId);
//                 setFApplication(applicationData);
//                 setDraftSaved(true);
//                 return;
//             };
//             getUploadedFiles();

//             console.log("Checking IDs");

//             if (!fListing || !fListing?.id || !fProvider?.id || !student?.id) return;
//             if (user.product === "providers") return;

//             console.log("Checking dates and sections");
//             if (!fApplication.completedSections && !fApplication.startDate && !fApplication.endDate) return;

//             console.log("Application updated");
//             if (!fApplicationId && !applicationId) {
//                 // save new
//                 console.log("Add application")
//                 addApplication();
//                 return;
//             }
//             // update
//             firebaseQuery.update(["applications", (fApplicationId || applicationId) as string], fApplication);
//             setDraftSaved(true);
//         }, [fApplication]);
    
//         const openSuccessPopup = (type: "submitted"|"draftSaved"|"stageComplete"|"outcome") => {
//             setSuccessPopup(type);
//             setTimeout(() => {
//                 // onClose();
//             }, 1500);
//         };

//         useEffect(() => {

//             const areStagesCompleted = async ():Promise<boolean|undefined> => {
//                 if (!fListing) return;
//                 if (fApplication.stage === undefined) return undefined;
//                 const currentStage = await getCurrentStage(fApplication.stage);

//                 console.log("Checking current stage is complete");
                
//                 for (const fileViewed of currentStage.stage?.files || []) {
//                     console.log("Checking file viewed", fileViewed, "in", currentStage?.completedSections?.filesViewed);
//                     if (!currentStage?.completedSections?.filesViewed?.includes(fileViewed)) {        
//                         console.log("File not viewed");
//                         return false;
//                     } else {
//                         console.log("File viewed");
//                     }
//                 }
//                 for (const formCompleted of currentStage?.stage?.forms || []) {
//                     console.log("Checking form completed", formCompleted, "in", currentStage?.completedSections?.formsCompleted);

//                     if (!Object.keys(currentStage?.completedSections?.formsCompleted || {}).includes(formCompleted)) {
//                         console.log("Form not completed");
//                         return false;
//                     } else {
//                         console.log("Form completed")
//                     }
//                 }
//                 for (let i = 0; i++; i < (currentStage?.stage?.requiredFiles || []).length) {
//                     if (!Object.keys(currentStage?.completedSections?.filesUploaded || {}).includes(i.toString())) {
//                         console.log("Form not uploaded");
//                         return false;
//                     } else {
//                     console.log("Form uploaded");
//                     }
//                 }
                
//                 return true;
//             };

//             areStagesCompleted().then(setCurrentStageComplete);
//         }, [fApplication, fListing])

//         const viewFile = (file: string, onOpen: (url: string) => void) => {
//             if (fApplication.reqUserType !== user.userType) return;
//             if (fApplication.stage === undefined) throw new Error("Missing applciation stage.")
//             if (!fListing) throw new Error("No associated listing.");

//             const viewableFiles = fListing?.applicantWorkflow?.find((stage) => stage.id === fApplication.stage)?.viewableFiles;

//             setFApplication((a) => {
//                 const oldA = {...a};
//                 const viewedFiles = a.completedSections?.[1]?.filesViewed || [];
//                 if (viewedFiles?.includes(file)) return a;
    
//                 viewableFiles?.[file].url && onOpen(viewableFiles?.[file].url);
//                 viewedFiles?.push(file);
    
//                 const newA = editNestedObject(["completedSections", 1, "filesViewed"], oldA, viewedFiles) as Partial<Application>;
//                 return newA;
//             });
//         };

//         const addFile = (files: string[], fileId: number) => {
//             if (fApplication.reqUserType !== user.userType) throw new Error(`Incorrect user type. Expected ${fApplication.reqUserType}, got: ${user.userType}`);
//             if (fApplication.stage === undefined) throw new Error("Missing applciation stage.")

//             if (!files.length) throw new Error("No files to upload");
//             setFApplication((a) => editNestedObject(["completedSections", fApplication.stage as number, "filesUploaded", fileId], a, files) as Application);
//         };

//         const setFormComplete = (formId: string, e: {[key: string]: unknown}) => {
//             if (fApplication.reqUserType !== user.userType) throw new Error(`Incorrect user type. Expected ${fApplication.reqUserType}, got: ${user.userType}`);
//             if (fApplication.stage === undefined) throw new Error("Missing applciation stage.")
//             setFApplication((a) => editNestedObject(["completedSections", fApplication.stage as number, "formsCompleted", formId], a, e) as Application);
//         };
    
//         const successText = {
//             submitted: {
//                 title: "Application submitted",
//                 desc: "We've sent your application to the placement provider. You will hear what the next steps are soon.",
//             },
//             draftSaved: {
//                 title: "Draft saved",
//                 desc: "Your draft has been saved. Go to the 'Applications' section from your home screen to edit.",
//             },
//             stageComplete: {
//                 title: "Stage complete",
//                 desc: "We have sent this to the next stage in the application process. We will update you with any progress.",
//             },
//             outcome: {
//                 title: "Outcome submitted",
//                 desc: "We have sent the student an email with the outcome of their application.",
//             },
//         };

//         const onFApply = async (draft?: boolean) => {
//             if (draft) {
//                 openSuccessPopup("draftSaved")
//                 return;
//             }

//             if (!fApplicationId) return;
    
//             // Check all items have been filled in.
//             if (!fApplication.startDate || !fApplication.endDate) throw new Error("Please select dates for your placement.");
    
//             await executeCallable("applications-submit", {applicationId: fApplicationId});
            
//             const newApplication = await firebaseQuery.getDocData(["applications", fApplicationId]) as Application;
//             setFApplication(newApplication);
//             openSuccessPopup("submitted")
//         };
    
//         const progressStage = async (type: number|"accept"|"reject", e?: {feedback?: string}) => {
//             // Check all stages completed.
//             if (!fApplicationId) return;
//             if (!currentStageComplete) throw new Error("Complete all forms before submitting.");
//             if (fApplication.reqUserType !== user.userType) throw new Error(`Incorrect user type. Expected ${fApplication.reqUserType}, got: ${user.userType}`);
//             if (fApplication.stage === undefined) throw new Error("Missing applciation stage.")

//             await executeCallable("applications-changeStage", {applicationId: fApplicationId, type: type, feedback: e?.feedback});
//             const newApplication = await firebaseQuery.getDocData(["applications", fApplicationId]) as Application;
//             setFApplication(newApplication);
//         };
    

//     return {successText, onFApply, progressStage, currentStageComplete, uploadedFiles, getCurrentStage, setFormComplete, viewFile, addFile, setSuccessPopup, openSuccessPopup, setFApplication, fApplication, draftSaved, profileUrl, successPopup, fApplicationId, fListing, student, fProvider}
// }

export function useProposePlacementRenderer({user, orgContext, placement}:
    {user:UserData, orgContext?: {details: InstituteData, userGroups: {[key:string]: UserGroupData}, placements: {[key: string]: StudentPlacementData}, forms: {[key:string]: unknown}, cohorts: {[key:string]: CohortData}},
    placement?:StudentPlacementData}) {
    const [businessSectionComplete, setBusinessSectionComplete] = useState(false);
    const [addressSectionComplete, setAddressSectionComplete] = useState(false);
    const [placementSectionComplete, setPlacementSectionComplete] = useState(false);
    const [formData, setFormData] = useState<StudentPlacementData>();
    const [disabled, setDisabled] = useState(false);
    const [student, setStudent] = useState<UserData|undefined>((user && (user?.userType === "Students")) ? user : undefined);
    const [cohort, setCohort] = useState<CohortData>();
    const [complete, setComplete] = useState(false);
    const [stage, setStage] = useState<"provider"|"address"|"dates"|"review">("dates");

    const sections:Array<"dates"|"provider"|"address"|"review"> = ["dates", "provider", "address", "review"];

    const firebaseQuery = new FirebaseQuery();

    useEffect(() => {
        if (user.userType === "Students") {
            setStudent(user);
        }
        if (placement) {
            console.log("p", placement);

            setFormData(placement);
            if (placement.placementId) {
                setStage("dates");
            } else {
                setStage("review");
            }
        }
    }, []);


    useEffect(() => {
        if (!student) {
            return;
        }

        if (user.userType === "Staff" && orgContext) {
            setCohort(orgContext.cohorts[student?.cohort as string]);
            return;
        }
        setCohort(user.cohortData as CohortData);
    }, [student]);

    useEffect(() => {
        if (!cohort) {
            return;
        }

        if (user.userType === "Students" && cohort.startSubmission && cohort.endSubmission) {
            const today = new Date();
            const submissionStart = new Date(cohort.startSubmission);
            const submissionEnd = new Date(cohort.endSubmission);
            setDisabled(today <= submissionStart || today >= submissionEnd);
        }
    }, [cohort]);

    const submitSection = (type:"dates"|"provider"|"address", data:{[key:string]:unknown}) => {
        setFormData((f) => {
            return {
              ...f,
              ...data
            } as StudentPlacementData;
          });
        if (placement?.placementId && type === "dates") {
            setStage("review");
            return;
        }
        setStage(sections[sections.indexOf(type)+1]);
    };

    const resetFormData = () => {
        setFormData(undefined);
    };

    const proposePlacement = async (draft = false) => {
        // const getPlacementStatus = (startDate: Date, endDate: Date) => {
        //     const today = new Date()
        //      if (startDate <= today && endDate >= today) return { active: !draft ? true : false, inProgress: true, completed: false};
        //      else if (endDate <= today) return { completed: true, inProgress: false, active: false};
        //      return { completed: false, inProgress: true, active: false };
        // }
        
        if (!formData || !student) {
            throw new Error("Cannot find placement details.");
        }
    
        try {
            console.log("formData", formData);
            if (formData.id && formData.uid && draft === formData.draft) await firebaseQuery.update(["placements", formData.id], formData);
            const response = await addPlacement(formData, student.id, draft)
            setComplete(true);
            return response
           
        } catch (error) {
            console.log("Error:", error);
            throw error;
        }
/*
        console.log("STUDENTID", student.id);

        return await addPlacement(formData, student.id, draft).catch((e) => {
            console.log("error");
            console.log(e);
            throw e;
        }).then((e)  => {
            setComplete(true);
            return e;
        });
*/
    };

    const deletePlacement = async (id:string) => {
        return await firebaseQuery.delete(["placements", id]);
    };

    return ({...{student, disabled, stage, formData, complete, cohort,
        businessSectionComplete, addressSectionComplete, placementSectionComplete,
        setBusinessSectionComplete, setAddressSectionComplete, setPlacementSectionComplete,
        proposePlacement, deletePlacement, setStudent, submitSection, setFormData, resetFormData, setStage, setComplete}});
}

export const useRefDimensions = () => {
    const [dimensions, setDimensions] = useState({width: 1, height: 2});

    const refCallback = useCallback((node: HTMLDivElement) => {
        if (!node) return;
        const resizeObserver = new ResizeObserver(() => {
            const boundingRect = node.getBoundingClientRect();
            const {width, height} = boundingRect;
            setDimensions({width: Math.round(width), height: Math.round(height)});
        });
        resizeObserver.observe(node);
    }, []);

    return {dimensions, refCallback};
};

type CreateCohortRendererParams = {
    oId: string,
    product: Products,
    initialData ?: CohortData
}

export type CreateCohortStages = "info"|"name"|"placementType"|"database"|"review";
const cohortStages = ["info", "name", "placementType", "review", "created"];

const defaultCohortData:CohortData = {
    name: "",
    stage: "info",
    startSubmission: "",
    endSubmission: "",
    startPlacements: "",
    endPlacements: "",
    placementType: "defined",
    workflow: [],
    oId: "",
    product: "providers",
    feedback: {
        students: "defaultStudentFeedackForm",
        provider: "defaultEmployerFeedackForm",
        parent: "defaultParentFeedackForm",
    }
};

export function useCreateCohortRenderer({oId, product, initialData=defaultCohortData}:CreateCohortRendererParams) {
    const [cohortData, setCohortData] = useState<CohortData>(initialData);
    const [cohortId, setCohortId] = useState<string|undefined>(initialData.id);
    const firebaseQuery = new FirebaseQuery();

    const submitSection = (e?: {[key:string]: unknown}) => {
        console.log("Next stage", cohortData);

        if (cohortData?.stage === "students") {
            console.log("Next stage");
            setCohortData((p) => ({...p, stage: cohortStages[cohortStages.indexOf(cohortData.stage)+1] as CreateCohortStages}));
        } else {
            setCohortData((p) => ({...p, ...e, stage: cohortStages[cohortStages.indexOf(cohortData.stage)+1] as CreateCohortStages}));
        }
    };

    useEffect(() => {
        if (!Object.keys(cohortData).length || cohortData.stage === "name" || objectsEqual(initialData, cohortData)) return;

        if (cohortId) {
            console.log("update", cohortId, cohortData);
            firebaseQuery.update(["cohorts", cohortId], cohortData);
            return;
        }
        firebaseQuery.add(["cohorts"], {...cohortData, product: product, oId: oId}).then((doc) => {
            setCohortId(doc.id);
        });
    }, [cohortData]);

    const submitCohort = () => {
        if (cohortId) {
            firebaseQuery.update(["cohorts", cohortId], {stage: "created"});
            return;
        }
    };

    const deleteCohort = async () => {
        if (cohortId) {
            await firebaseQuery.delete(["cohorts", cohortId]);
        }
    };

    const back = () => {
        setCohortData((p) => ({...p, stage: cohortStages[cohortStages.indexOf(cohortData?.stage)-1] as CreateCohortStages}));
    };

    return ({...{submitCohort, submitSection, back, setCohortData, deleteCohort, cohortData, cohortId}});
}

type UserUploadParams = {
    userType: "Staff"|"Students"|"eventStudents",
    user: UserData,
    onComplete?: (e: string) => void,
    cohortId?: string,
    userGroupId?: string, 
    shareNameWithReferralLeaderboardConsent?: string,
    product: Products,
    oId: string,
    event?: Partial<ExternalEvent>,
    eventId?: string
}

export function useUserUploadHandler({product, oId, event, eventId, userType, user, onComplete, userGroupId, cohortId}: UserUploadParams) {
    const [emptyCellsWarning, setEmptyCellsWarning] = useState(false);
    const [alert, setAlert] = useState<{severity: "warning"|"error"|"success"|"info", msg: string}>();

    const {execute} = useExecuteCallableJob({user: user});

    const requiredFields = ["forename", "surname", "email"];

    const checkData = (userData: {email?: string, parentEmail?: string, year?: number, [key:string]: unknown}[]) => {
        setAlert(undefined);

        userData = userData.filter((u) => Object.entries(u).some(([, v]) => v));

        if (!Object.entries(userData)) {
            return [];
        }

        const emailDuplicateLookup = userData.reduce((acc, e, index) => {
            if (!e.email) return acc;
            acc[e.email] = [...(acc[e.email] || []), index];
            return acc;
        }, {} as {[email:string]: number[]});

        const emailDuplicates = Object.entries(emailDuplicateLookup).filter(([_, ids]) => ids.length > 1).map(([email]) => email)

        if (userData.filter((u) => u.email).length < userData.length) {
            setAlert({msg: "Your data contains missing email addresses.", severity: "error"});
            return false;
        }


        console.log("emailDuplicates", emailDuplicates);

        if (emailDuplicates.length) {
            setAlert({msg: `Your data contains multiple rows with the same email address. Please remove these and reupload.\n\n Duplicate emails: ${emailDuplicates.join(", ")}`, severity: "error"});
            return false;
        }

        let emptyOptionalCell = false;
        let emptyRequiredCell = false;

        for (const user of userData ) {
            if (!checkEmailValidity(user)) {
                return false;
            }

            if (Object.keys(user).includes("")) {
                setAlert({msg: "Data cannot be uploaded in unnamed columns.", severity: "error"});
                return false;
            }

            for (const field in user) {
                if (!user[field]) {
                    if (requiredFields.includes(field)) {
                        setAlert({msg: "All users must contain "+requiredFields.join(", "), severity: "error"});
                        emptyRequiredCell = true;
                    }
                    emptyOptionalCell = true;
                }
            }
            // Add more test cases
        }

        if (emptyRequiredCell) {
            return false;
        }

        if (emptyOptionalCell && !emptyCellsWarning) {
            setAlert({msg: "You have empty cell(s). Continue to submit or upload an amended file.", severity: "warning"});
            setEmptyCellsWarning(true);
            return false;
        } else {
            setEmptyCellsWarning(false);
        }
        return userData;
    };

    const checkEmailValidity = (user: {[key:string]: unknown}) => {
        if (!validateEmail(user.email as string)) {
            setAlert({msg: `Error in email formatting: ${user.email}. Amend errors and reupload.`, severity: "error"});
            return false;
        }
        if (user.parentEmail && !validateEmail(user.parentEmail as string)) {
            setAlert({msg: `Error in parent email formatting: ${user.parentEmail}. Amend errors and reupload.`, severity: "error"});
            return false;
        }
        return true;
    };

    const uploadUsers = async (users: {email?: string, parentEmail?: string, year: number, [key:string]: unknown}[]) => {
        let fUsers:{email?: string, parentEmail?: string, [key:string]: unknown}[] = [];

        const cleanUpload = checkData(users);
        if (!cleanUpload) {
            return;
        }
        fUsers = cleanUpload;

        if (userType === "Students" && !cohortId) {
            setAlert({msg: "No cohort associated with user uploads. Contact admin@careerthread.co.uk to address this error", severity: "error"});
        }


        setAlert(undefined);
        if (fUsers.length) {
            console.log("fUsers", fUsers);
            const jobId = await execute(userType === "eventStudents" ? "events-addStudents" : "userManagement-addUsers", {product: product, oId: oId, event: event, eventId: eventId, users: fUsers, userType: userType, userGroupId: userGroupId, cohortId: cohortId});
            onComplete && onComplete(jobId.jobId);
        }
        console.log("Complete", onComplete);
        console.log("Finished");
    };

    const onChange = () => {
        setAlert(undefined);
        setEmptyCellsWarning(false);
    };

    return ({...{uploadUsers, alert, onChange}});
}

type WorkflowEditorParams = {
    user: UserData,
    initialData?: WorkflowStage[]|ApplicantStage[]
    onSubmit: (e:{workflow: WorkflowStage[], includedFiles: string[], includedForms: string[]}) => void,
    cohortId?: string,
    product?: Products,
    oId?: string,
}

export function useWorkflowEditor({user, initialData, onSubmit, cohortId}:WorkflowEditorParams) {
    const firebaseQuery = new FirebaseQuery();

    const {filePopupActive, files, includedFiles, includedForms, uploadFile, fAddNode, fOnDelete, arrows, onChange, error, onMoveEnd, addEdgePoint, fWorkflowNodes, newEdgePoint, setTutorialActive, setFilePopupActive,
        fSubmitWorkflow, openPopup, setSnackbar, snackbar, setMousePosFunc, mousePos, tutorialActive, onDeleteArrow, containerRef, setError, setArrows, setFWorkflowNodes} = useGenericWorkflowEditor({...{user, initialData}, defaultData: defaultInstituteWorkflow, onSubmit: onSubmit});


    const addNode = ({userType, nextStage, prevStage}:{userType?: "Students" | "Staff" | "Provider" | "Parent", nextStage?: number, prevStage?: number} = {}) => {
        return fAddNode({userType: userType}, `${userType} review`, nextStage, prevStage);
    };

    const onDelete = useCallback(async (id?: number) => {
        if (!id) {
            return;
        }


        return fOnDelete(id, onDeleteValidator)
    }, []);

    const onDeleteValidator = async (id: number): Promise<{res: boolean, reason?: string}> => {
        const placementsOnStage = await firebaseQuery.getCount("placements", [where("oId", "==", user.oId), where("cohort", "==", cohortId), where("status", "==", id)]);
        if (placementsOnStage > 0) {
            return {res: false, reason: "Cannot delete a stage with current active placements."}
        }
        if ([1, 6, 8, 11].includes(id)) {
            return {res: false, reason: "Cannot delete core workflow component."}
        }
        return {res: true};
    }


    const submitWorkflow = async (workflow?: WorkflowStage[]|ApplicantStage[], callOnComplete=true) => {

        const validatorTypeConversion = (newWorkflow: GenericWorkflowStage[], oldWorkflow?: GenericWorkflowStage[]) => {
            return validateWorkflow(newWorkflow as WorkflowStage[], oldWorkflow as WorkflowStage[])
        }

        return fSubmitWorkflow(validatorTypeConversion, workflow, callOnComplete)
    };

    const validateWorkflow = async (newWorkflow: WorkflowStage[], oldWorkflow?: WorkflowStage[]) => {
        const getStageById = (id: number, checkInitialData ?: boolean) => {
            let node:WorkflowStage|undefined;
            if (checkInitialData) {
                if (!oldWorkflow?.length) {
                    throw new Error("Check initial data but no oldWorkflow");
                }
                node = (oldWorkflow).find((x) => x.id === id);
            } else {
                node = (newWorkflow).find((x) => x.id === id);
            }
            if (!node) {
                throw new Error("Error in configuration. Cannot find stage: "+id);
            }
            return node;
        };

        console.log("workflows, new, old", newWorkflow, oldWorkflow);

        // Steps: 

        // Check if any deleted stages that have placements assigned to them
        const missingNodes = oldWorkflow && oldWorkflow.length ? oldWorkflow.reduce((acc, oldNode) => {
            const existsInNewWorkflow = Boolean(newWorkflow.find((newNode) => newNode.id === oldNode.id))
            if (!existsInNewWorkflow) {
                acc.push(oldNode.id);
            }
            return acc;
        }, [] as number[]) : [];

        await Promise.all(missingNodes.map(async (nodeId) => {
            const placementsOnStage = await firebaseQuery.getCount("placements", [where("oId", "==", user.oId), where("cohort", "==", cohortId), where("status", "==", nodeId)]);
            if (placementsOnStage > 0) {
                throw new Error("Cannot delete a stage with current active placements.")
            }
        }));

        // After verifying, we have a list of stages that 

        // Create paths with new workflow
            // Check cyclic
            // Check early termination
            // Check missed items

        // Create paths from old workflow
            // Check cyclic
            // Check early termination
            // Check missed items

        const paths:(WorkflowStage[])[] = [];
        const createPaths = (stage: WorkflowStage, path:WorkflowStage[], checkingInitialData?: boolean) => {
            if (!stage) return;
            
            const currentPath = [...(path || [])];

            // Check if infinite cycles
            if (currentPath.find((s) => s.id === stage.id)) {
                // ID must be already in it, so a loop has been formed.

                // Get just the elements in that cycle, such as [1, 2, 3, 4, 1]
                const cyclicPath = currentPath.slice(currentPath.findIndex((s) => s.id === stage.id));

                // If we have a cycle [1, 2, 3, 4, 1], check if any of them have a way out. 
                const escapableCycle = cyclicPath.some((cycleStage, idx) => {
                    let cyclicStageButtons = cycleStage?.buttons || [];

                    // Keep only the arrows that don't go to the next one already documented, or the ID being checked.
                    cyclicStageButtons = cyclicStageButtons.filter((button) => ![cyclicPath[idx+1], stage.id].includes(button.id));
                    return cyclicStageButtons.length > 0;
                });

                if (!escapableCycle) {
                    throw new Error(`Inescapable cycle detected at ${stage.name}. Amend and resubmit.`)
                }
                return;
            }

            currentPath.push(stage);
            if (!stage?.buttons?.length) {
                console.log("Final path", currentPath, checkingInitialData);
                // Check if in order and ends in 11
                if (stage.id !== 11) {
                    throw new Error("All paths must end with the 'Workflow end' stage. Amend and reupload.");
                }

                //Check if a valid path.
                const validEssentialRoute = [1, "Provider", 6, 8, 11];
                const currentEssentialRoute = currentPath.reduce((acc, node) => {
                    if ([1, 6, 8, 11].includes(node.id)) {
                        acc.push(node.id);
                        return acc;
                    }
                    if (node?.userType !== "Provider") return acc;

                    if (!acc.includes(6) && !acc.includes("Provider")) {
                        acc.push("Provider");
                    }
                    return acc;
                }, [] as Array<any>);

                const valid = arraysEqual(currentEssentialRoute, validEssentialRoute);

                if (!valid) {
                    console.log("Error in", currentEssentialRoute);
                    throw new Error(checkingInitialData ?
                        "Cannot submit. Changing workflow will cause current placements to skip core workflow components." :
                        "Missing core workflow components. Ensure all placements have a provider review, start, end and workflow end in that order.")
                }

                paths.push(currentPath);
                return;
            }

            if (checkingInitialData) {
                stage.buttons.forEach((button) => {
                    createPaths(getStageById(button.id, true), currentPath, checkingInitialData);
                });
            }
            const newWorkflowStage = newWorkflow.find((newStage) => newStage.id === stage.id);
            if (!newWorkflowStage) {
                if (checkingInitialData) {
                    return;
                }
                throw new Error("Cannot find node: "+stage.id);
            }
            newWorkflowStage.buttons?.forEach((button) => {
                console.log("stage", stage, checkingInitialData);
                createPaths(getStageById(button.id), currentPath);
            });
        };

        createPaths(getStageById(1), []);

        if (oldWorkflow && oldWorkflow.length) {
            createPaths(getStageById(1, true), [], true);
        }
        return paths.every((x) => x);
    };

    const workflowNodes = fWorkflowNodes as WorkflowStage[];
    const setWorkflowNodes = setFWorkflowNodes as React.Dispatch<React.SetStateAction<WorkflowStage[]|ApplicantStage[]>>

    return ({...{filePopupActive, files, uploadFile, addNode, onDelete, arrows, onChange, error, includedFiles, onMoveEnd, addEdgePoint, newEdgePoint, setTutorialActive, setFilePopupActive,
        includedForms, submitWorkflow, openPopup, snackbar, setSnackbar, setMousePosFunc, mousePos, tutorialActive,
        onDeleteArrow, workflowNodes, containerRef, setError, setArrows, setWorkflowNodes}});
}

type ApplicantWorkflowEditorParams = {
    user: UserData,
    initialData?: ApplicantStage[]
    onSubmit: (e:{workflow: ApplicantStage[], includedFiles: string[], includedForms: string[]}) => void,
    workflowId?: string,
    product?: Products,
    oId?: string,
}

export function useApplicantWorkflowEditor({user, initialData, onSubmit, workflowId}:ApplicantWorkflowEditorParams) {
    const firebaseQuery = new FirebaseQuery();

    const {filePopupActive, includedFiles, includedForms, uploadFile, fAddNode, fOnDelete, arrows, onChange, error, onMoveEnd, addEdgePoint, fWorkflowNodes, newEdgePoint, setTutorialActive, setFilePopupActive,
        fSubmitWorkflow, openPopup, setSnackbar, snackbar, setMousePosFunc, mousePos, tutorialActive, onDeleteArrow, containerRef, setError, setArrows, setFWorkflowNodes} = useGenericWorkflowEditor({...{user, initialData}, defaultData: workflowId ? [] : defaultApplicantWorkflow, onSubmit: onSubmit});

    useEffect(() => {
        if (!workflowId) return;

        firebaseQuery.getDocData(["applicantWorkflows", workflowId]).then((e) => setFWorkflowNodes(e.workflow))
    }, [workflowId]);

    const addNode = ({userType, nextStage, prevStage}:{userType?: "Students" | "Staff", nextStage?: number, prevStage?: number} = {}) => {
        return fAddNode({userType: userType, buttons: [{name: "Reject", id: 12, required: true}]}, "", nextStage, prevStage);
    };

    const onDelete = useCallback(async (id?: number) => {
        if (!id) {
            return;
        }


        return fOnDelete(id, onDeleteValidator)
    }, []);

    const onDeleteValidator = async (id: number): Promise<{res: boolean, reason?: string}> => {
        const placementsOnStage = await firebaseQuery.getCount("placement", [where("providerId", "==", user.oId), where("applicantWorkflowId", "==", workflowId), where("applicantStage", "==", id)]);
        if (placementsOnStage > 0) {
            return {res: false, reason: "Cannot delete a stage with current applicants."}
        }
        if ([1, 11, 12].includes(id)) {
            return {res: false, reason: "Cannot delete core workflow component."}
        }
        return {res: true};
    }


    const submitWorkflow = async (workflow?: ApplicantStage[], callOnComplete=true) => {

        const validatorTypeConversion = (newWorkflow: GenericWorkflowStage[], oldWorkflow?: GenericWorkflowStage[]) => {
            return validateWorkflow(newWorkflow as ApplicantStage[], oldWorkflow as ApplicantStage[])
        }

        return fSubmitWorkflow(validatorTypeConversion, workflow, callOnComplete)
    };


    // Change validator
    const validateWorkflow = async (newWorkflow: ApplicantStage[], oldWorkflow?: ApplicantStage[]) => {
        const getStageById = (id: number, checkInitialData ?: boolean) => {
            let node:ApplicantStage|undefined;
            if (checkInitialData) {
                if (!oldWorkflow?.length) {
                    throw new Error("Check initial data but no oldWorkflow");
                }
                node = (oldWorkflow).find((x) => x.id === id);
            } else {
                node = (newWorkflow).find((x) => x.id === id);
            }
            if (!node) {
                throw new Error("Error in configuration. Cannot find stage: "+id);
            }
            return node;
        };

        console.log("workflows, new, old", newWorkflow, oldWorkflow);

        // Steps: 

        // Check if any deleted stages that have placements assigned to them
        const missingNodes = oldWorkflow && oldWorkflow.length ? oldWorkflow.reduce((acc, oldNode) => {
            const existsInNewWorkflow = Boolean(newWorkflow.find((newNode) => newNode.id === oldNode.id))
            if (!existsInNewWorkflow) {
                acc.push(oldNode.id);
            }
            return acc;
        }, [] as number[]) : [];

        await Promise.all(missingNodes.map(async (nodeId) => {
            const placementsOnStage = await firebaseQuery.getCount("placement", [where("providerId", "==", user.oId), where("applicantWorkflow", "==", workflowId), where("applicantStage", "==", nodeId)]);
            if (placementsOnStage > 0) {
                throw new Error("Cannot delete a stage with current active placements.")
            }
        }));

        // After verifying, we have a list of stages that 

        // Create paths with new workflow
            // Check cyclic
            // Check early termination
            // Check missed items

        // Create paths from old workflow
            // Check cyclic
            // Check early termination
            // Check missed items

        const paths:(ApplicantStage[])[] = [];
        const createPaths = (stage: ApplicantStage, path:ApplicantStage[], checkingInitialData?: boolean) => {
            if (!stage) return;
            
            const currentPath = [...(path || [])];

            // Check if infinite cycles
            if (currentPath.find((s) => s.id === stage.id)) {
                // ID must be already in it, so a loop has been formed.

                // Get just the elements in that cycle, such as [1, 2, 3, 4, 1]
                const cyclicPath = currentPath.slice(currentPath.findIndex((s) => s.id === stage.id));

                // If we have a cycle [1, 2, 3, 4, 1], check if any of them have a way out. 
                const escapableCycle = cyclicPath.some((cycleStage, idx) => {
                    let cyclicStageButtons = cycleStage?.buttons || [];

                    // Keep only the arrows that don't go to the next one already documented, or the ID being checked.
                    cyclicStageButtons = cyclicStageButtons.filter((button) => ![cyclicPath[idx+1], stage.id].includes(button.id));
                    return cyclicStageButtons.length > 0;
                });

                if (!escapableCycle) {
                    throw new Error(`Inescapable cycle detected at ${stage.name}. Amend and resubmit.`)
                }
                return;
            }

            currentPath.push(stage);
            if (!stage?.buttons?.length) {
                console.log("Final path", currentPath, checkingInitialData);
                // Check if in order and ends in 11
                if (![11, 12].includes(stage.id)) {
                    throw new Error("All paths must end with the 'Success' or 'Reject' stage. Amend and reupload.");
                }

                paths.push(currentPath);
                return;
            }
            if (checkingInitialData) {
                stage.buttons.forEach((button) => {
                    createPaths(getStageById(button.id, true), currentPath, checkingInitialData);
                });
            }
            const newWorkflowStage = newWorkflow.find((newStage) => newStage.id === stage.id);
            if (!newWorkflowStage) {
                if (checkingInitialData) {
                    return;
                }
                throw new Error("Cannot find node: "+stage.id);
            }
            newWorkflowStage.buttons?.forEach((button) => {
                createPaths(getStageById(button.id), currentPath);
            });
        };

        createPaths(getStageById(1), []);

        if (oldWorkflow && oldWorkflow.length) {
            createPaths(getStageById(1, true), [], true);
        }
        return paths.every((x) => x);
    };

    const workflowNodes = fWorkflowNodes as ApplicantStage[];
    const setWorkflowNodes = setFWorkflowNodes as React.Dispatch<React.SetStateAction<ApplicantStage[]>>

    return ({...{filePopupActive, uploadFile, addNode, onDelete, arrows, onChange, error, includedFiles, onMoveEnd, addEdgePoint, newEdgePoint, setTutorialActive, setFilePopupActive,
        includedForms, submitWorkflow, openPopup, snackbar, setSnackbar, setMousePosFunc, mousePos, tutorialActive,
        onDeleteArrow, workflowNodes, containerRef, setError, setArrows, setWorkflowNodes}});
}

type GenericWorkflowEditorParams = {
    user: UserData,
    initialData?: GenericWorkflowStage[],
    defaultData: GenericWorkflowStage[], 
    onSubmit: (e: {
        workflow: GenericWorkflowStage[];
        includedFiles: string[];
        includedForms: string[];
    }) => void
}

type GenericWorkflowStage = {
    id: number,
    checkpoint?: boolean,
    permanent?: boolean,
    description?: string,
    name: string,
    buttons?: {id: number, name: string|false, description?: string, required: boolean}[],
    pos?: { x: number, y: number },
    forms?: string[],
    files?: string[],
    [key: string]: unknown
};

function useGenericWorkflowEditor({user, initialData, defaultData, onSubmit}: GenericWorkflowEditorParams) {
    const [filePopupActive, setFilePopupActive] = useState<boolean>(false);
    const [files, setFiles] = useState<{name: string, url: string}[]>([]);
    const [fWorkflowNodes, setFWorkflowNodes] = useState<GenericWorkflowStage[]>([]); // Workflow data
    const [arrows, setArrows] = useState<ArrowObject[]>([]);
    const [includedForms, setIncludedForms] = useState<string[]>([]);
    const [includedFiles, setIncludedFiles] = useState<string[]>([]);
    const [error, setError] = useState<string>();
    const [snackbar, setSnackbar] = useState<string>();
    const [newEdgePoint, setNewEdgePoint] = useState<number>();
    const [mousePos, setMousePos] = useState<{x: number, y: number}>();
    const [tutorialActive, setTutorialActive] = useState(false);

    const containerRef = useRef<HTMLFormElement>(null);

    let ticking = false;

    useEffect(() => {
        let newData = initialData;
        if (initialData === undefined || initialData.length === 0) {
            newData = defaultData;
            setTimeout(() => {
                setTutorialActive(false);
            }, 1000);
        }

        getFiles(`${user.product}/${user.oId}/files`).then(setFiles);
        setFWorkflowNodes(newData || []);
    }, [initialData]);

    useEffect(() => {
        // New edge points
        if (!newEdgePoint) {
            return;
        }

        const removeListener = (e: MouseEvent) => {
            setNewEdgePoint(undefined);
            e.preventDefault();
        };
        window.addEventListener("contextmenu", removeListener);

        return () => {
            setMousePos(undefined);
            window.removeEventListener("contextmenu", removeListener);
        };
    }, [newEdgePoint]);

    const setMousePosFunc = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
        if (!newEdgePoint) {
            return;
        }

        if (!ticking) {
            requestAnimationFrame(() => {
                ticking = false;

                setMousePos({x: e.pageX, y: e.pageY});
            });
        }
        ticking = true;
    };

    const uploadFile = async (files: any[]) => {
        const res = await uploadFiles(files, `${user.product}/${user.oId}/files/`);
        if (res===1) {
            // change this
            setFilePopupActive(false);
            getFiles(`${user.product}/${user.oId}/files`).then(setFiles);
        }
    };

    const fAddNode = (additionalData?: {buttons?: {required: boolean, name: string, id: number}[], [key: string]: unknown}, name?: string, nextStage?: number, prevStage?: number) => {
        let nodeId = getUniqueId(fWorkflowNodes);
        // Add user type and name for institute placement workflow!
        setFWorkflowNodes((p) => {
            let newWorkflowNodes = ([...p, {id: nodeId, pos: {x: 10, y: 10}, ...additionalData, name: name || `Stage ${nodeId}`, buttons: nextStage ? [...(additionalData?.buttons || []), {required: true, name: "Accept", id: nextStage}] : []}]);

            if (prevStage) {
                const prevStageIndex = p.findIndex((node) => node.id === prevStage);
                newWorkflowNodes[prevStageIndex] = {...newWorkflowNodes[prevStageIndex], buttons: [{required: true, name: "Accept", id: nodeId}]}
            }

            return newWorkflowNodes;
        
        });
        return nodeId;
    };

    const fOnDelete = useCallback(async (id?: number, validation?: (id: number) => Promise<{res: boolean, reason?: string}>) => {
        if (!id) {
            return;
        }

        if (validation) {
            // Add placementsOnStage to validation
            const validationResult = await validation(id);
            if (!validationResult.res) {
                setError(validationResult.reason);
                return;
            }
        }

        setFWorkflowNodes((prev) => {
            let oldMutable = [...prev];

            // Buttons going from deleted nodes to new node
            const deletedNodeButtons = oldMutable.find((node) => node.id === id)?.buttons;

            oldMutable = oldMutable.filter((e) => e.id.toString() !== id.toString());

            const arrowsToDeletedNode = oldMutable.map((e, i) => {
                const node = {...e};
                // Remove any arrows to this node
                const nodeFeedsToDeleted = Boolean(node.buttons?.find((destination) => destination.id === id))
                if (nodeFeedsToDeleted) {
                    console.log("Delete line from node", node.id);
                }

                if (nodeFeedsToDeleted) {
                    const newNodeButtons = node.buttons ? node.buttons.filter((button) => id.toString() !== button.id.toString()) : [];
                    node.buttons = newNodeButtons;
                    console.log("New node buttons", newNodeButtons, node.buttons);
                }
                oldMutable[i] = node;
                return nodeFeedsToDeleted ? node : undefined;
            }).filter((i) => i) as {
                id: number;
                name: string | false;
                required: boolean;
            }[];

            console.log("arrows to and destinations", arrowsToDeletedNode, deletedNodeButtons);

            if (deletedNodeButtons?.length && arrowsToDeletedNode.length) {
                const newNodeDestination = deletedNodeButtons[0].id;
                const newNodeOrigin = arrowsToDeletedNode[0].id;
                const indexOfOrigin = oldMutable.findIndex((node) => node.id === newNodeOrigin);

                oldMutable[indexOfOrigin] = {...oldMutable[indexOfOrigin], buttons: [...(oldMutable[indexOfOrigin].buttons || []), {id: newNodeDestination, required: true, name: "Submit"}]}
            }

            return oldMutable;
        });
    }, []);

    useEffect(() => {
        error && setError(undefined);
        if (!fWorkflowNodes) {
            return;
        }
        setArrows(fWorkflowNodes.reduce((acc, node) => {
            if (node.buttons) {
                node.buttons.forEach((buttonData) => {
                    acc.push({
                        start: node.id,
                        end: buttonData.id,
                        name: buttonData.name,
                        required: Boolean(buttonData.required),
                    });
                });
            }
            return acc;
        }, [] as ArrowObject[]));

        const mIncludedFiles = fWorkflowNodes.reduce((acc, node) => {
            if (node.files) {
                acc.push(...node.files.map((f) => {
                    try {
                        return JSON.parse(f).name;
                    } catch {
                        return f;
                    }
                }));
            }
            return arrayUniqueValues(acc) as string[];
        }, [] as string[]);

        const mIncludedForms = fWorkflowNodes.reduce((acc, node) => {
            if (node.forms) {
                acc.push(...node.forms);
            }
            return arrayUniqueValues(acc) as string[];
        }, [] as string[])

        setIncludedFiles(mIncludedFiles);
        setIncludedForms(mIncludedForms);
    }, [fWorkflowNodes]);


    const onChange = useCallback((path: (string|number)[], value: unknown) => {
        setFWorkflowNodes((prev) => editNestedObject(path, prev, value, true) as GenericWorkflowStage[]);
    }, []);

    const fSubmitWorkflow = async (validation: (newWorkflow: GenericWorkflowStage[], oldWorkflow?: GenericWorkflowStage[]) => Promise<boolean>, workflow?: GenericWorkflowStage[], callOnComplete=true) => {
        setError(undefined);

        const fWorkflow = workflow || fWorkflowNodes;

        const mIncludedFiles = fWorkflow.reduce((acc, node) => {
            if (node.files) {
                acc.push(...node.files.map((f) => {
                    try {
                        return JSON.parse(f).name;
                    } catch {
                        return f;
                    }
                }));
            }
            return arrayUniqueValues(acc) as string[];
        }, [] as string[]);

        const mIncludedForms = fWorkflow.reduce((acc, node) => {
            if (node.forms) {
                acc.push(...node.forms);
            }
            return arrayUniqueValues(acc) as string[];
        }, [] as string[])

        setIncludedFiles(mIncludedFiles);
        setIncludedForms(mIncludedForms);

        if (initialData === fWorkflow) {
            return callOnComplete && onSubmit({workflow: fWorkflow, includedFiles: mIncludedFiles, includedForms: mIncludedForms});
        }

        const validatedWorkflow = await validation(fWorkflow, initialData).catch((e: Error) => {
            setError(e.message);
            throw e;
        })


        if (!validatedWorkflow) {
            return;
        }

        return callOnComplete && onSubmit({workflow: fWorkflow, includedFiles: includedFiles, includedForms: includedForms});
    };


    const deleteEdge = useCallback((originId: number|string, nodeId: number|string) => {
        setFWorkflowNodes((prev) => editNestedObject([originId, "buttons", nodeId], prev, undefined, true) as GenericWorkflowStage[]);
    }, []);

    const onDeleteArrow = useCallback((data: ArrowObject) => {
        deleteEdge(data.start, data.end);
    }, [deleteEdge]);


    const onMoveEnd = useCallback((id: number, currentRef?: HTMLDivElement) => {
        if (!containerRef.current || !currentRef) return;
        const clientRect = currentRef.getBoundingClientRect();
        const {left, top} = containerRef.current.getBoundingClientRect();
        const x = containerRef.current.scrollLeft + clientRect.x-left;
        const y = containerRef.current.scrollTop + clientRect.y-top-26;
        // Make sure x and y are not negative. Contrains canvas by top and left
        const pos = {x: x >= 0 ? x : 0, y: y >= 0 ? y : 0};
        setFWorkflowNodes((p) => (editNestedObject([id, "pos"], p, pos) as GenericWorkflowStage[]));
    }, []);

    const addEdgePoint = useCallback((id: number, existingEdgePoint: number) => {
        if (!existingEdgePoint) {
            if (id === 11) {
                setSnackbar("Cannot connect stages after workflow end.");
                return;
            }

            const buttons = fWorkflowNodes.find((node) => node.id === id)?.buttons;
            if ([1, 11].includes(id) && buttons && buttons.length > 0) {
                setSnackbar("Stage can only have one destination.");
                return;
            }
            setNewEdgePoint(id);
            return;
        }

        // Prevent making arrow to own stage
        if (existingEdgePoint === id) {
            setNewEdgePoint(undefined);
            return;
        }

        const originPos = fWorkflowNodes.findIndex((x) => x.id.toString() === existingEdgePoint.toString());

        const buttons: {id: number, name: string|false, required: boolean}[] = [...(fWorkflowNodes[originPos].buttons || [])];

        if (!buttons.some((x) => x.id.toString() === id.toString())) {
            buttons.push({
                id: id,
                required: false,
                name: [1, 6, 8].includes(existingEdgePoint) ? false : "",
            });
        }

        setFWorkflowNodes((p) => editNestedObject([existingEdgePoint, "buttons"], p, buttons, true) as WorkflowStage[]);
        setNewEdgePoint(undefined);
    }, [fWorkflowNodes]);

    const openPopup = useCallback(() => {
        setFilePopupActive(true);
    }, []);

    return ({...{filePopupActive, files, uploadFile, fAddNode, fOnDelete, arrows, onChange, error, includedFiles, onMoveEnd, addEdgePoint, newEdgePoint, setTutorialActive, setFilePopupActive,
        includedForms, fSubmitWorkflow, openPopup, snackbar, setSnackbar, setMousePosFunc, mousePos, tutorialActive,
        onDeleteArrow, fWorkflowNodes, containerRef, setError, setArrows, setFWorkflowNodes}});
}

type InstituteProviderContactUploadParams = {
    user: UserData,
    addToEvent?: boolean
    onComplete?: () => void
}

export type InstituteProviderContactUpload = {
    business: string;
    forename?: string;
    surname?: string;
    email: string;
    phone?: string;
    addressOne?: string;
    addressTwo?: string;
    city?: string;
    postcode?: string;
    country?: string;
};

export function useInstituteProviderContactsHandler({user, addToEvent}: InstituteProviderContactUploadParams) {
    const [emptyCellsWarning, setEmptyCellsWarning] = useState(false);
    const [alert, setAlert] = useState<{severity: "warning"|"error"|"success"|"info", msg: string}>();

    const requiredFields = ["business", "email"];
    const {execute} = useExecuteCallableJob({user: user});

    const checkData = (providerContacts: InstituteProviderContactUpload[]) => {
        setAlert(undefined);

        providerContacts = providerContacts.filter((u) => Object.entries(u).some(([, v]) => v));

        if (!Object.entries(providerContacts)) {
            return [];
        }
        console.log("P", providerContacts);

        if (providerContacts.filter((u) => u.email).length < providerContacts.length) {
            setAlert({msg: "Your data contains missing email addresses.", severity: "error"});
            return false;
        }

        let emptyOptionalCell = false;
        let emptyRequiredCell = false;

        for (const providerContact of providerContacts ) {
            if (!checkEmailValidity(providerContact)) {
                return false;
            }

            if (Object.keys(providerContact).includes("")) {
                setAlert({msg: "Data cannot be uploaded in unnamed columns.", severity: "error"});
                return false;
            }

            for (const field in providerContact) {
                if (!providerContact[field]) {
                    if (requiredFields.includes(field)) {
                        setAlert({msg: "All users must contain "+requiredFields.join(", "), severity: "error"});
                        emptyRequiredCell = true;
                    }
                    emptyOptionalCell = true;
                }
            }
        }

        if (emptyRequiredCell) {
            return false;
        }

        if (emptyOptionalCell && !emptyCellsWarning) {
            setAlert({msg: "You have empty cell(s). Continue to submit or upload an amended file.", severity: "warning"});
            setEmptyCellsWarning(true);
            return false;
        } else {
            setEmptyCellsWarning(false);
        }
        return providerContacts;
    };

    const checkEmailValidity = (providerContact: InstituteProviderContactUpload) => {
        if (!validateEmail(providerContact.email as string)) {
            setAlert({msg: `Error in email formatting: ${providerContact.email}. Amend errors and reupload.`, severity: "error"});
            return false;
        }
        return true;
    };

    const uploadProviderContacts = async (providers: InstituteProviderContactUpload[], schoolId?: string) => {
        let fProviders:InstituteProviderContactUpload[] = [];
        console.log("PP", providers);

        const cleanUpload = checkData(providers);
        if (!cleanUpload) {
            return false;
        }
        fProviders = cleanUpload;

        setAlert(undefined);
        if (fProviders.length) {
            if (addToEvent) {
                const addedContacts = await executeCallable("providerContacts-add", {providerContacts: fProviders, instituteId: user.product === "admin" ? "admin" : user.oId, schoolId: schoolId})
                return addedContacts.data as string[];
            }
            execute("providerContacts-add", {providerContacts: fProviders, instituteId: user.product === "admin" ? "admin" : user.oId, schoolId: schoolId});
        }
        return true;
    };

    const onChange = () => {
        setAlert(undefined);
        setEmptyCellsWarning(false);
    };

    return ({...{uploadProviderContacts, alert, onChange}});
}


export async function getIndividualPlacementForTaskList({type, placement}:{placement: StudentPlacementData, type: "insurance"|"dbsCheck"|"riskAssessment"}) {
    const newPlacementData:StudentPlacementData&{
        eliData?: string,
        riskAssessmentURL?: string,
        dbsCheckURL?: string,
    } = {...placement};

    if (type === "riskAssessment") {
        if (placement.riskAssessmentType === "file") {
            console.log("Risk assessment");
            const storageRef = ref(storage, `riskAssessments/${placement.placementId || placement.id}.pdf`);
            const file = await getDownloadURL(storageRef).catch(async () => await getDownloadURL(ref(storage, `riskAssessments/${placement.id}.pdf`)).catch(() => undefined));
            newPlacementData.riskAssessmentURL = file;
        }
    }
    if (type === "insurance") {
        const storageRef = ref(storage, `insurance/${placement.providerContactId}.pdf`);
        const file = await getDownloadURL(storageRef).catch(() => placement.insuranceSkippedReason);
        newPlacementData.eliData = file;
    }
    if (type === "dbsCheck") {
        if (placement.dbsCheckType === "file") {
            const storageRef = ref(storage, `dbsChecks/${placement.placementId || placement.id}.pdf`);
            const file = await getDownloadURL(storageRef);
            newPlacementData.dbsCheckURL = file;
        }
    }

    return newPlacementData;
}

export function useGetIndividualPlacementForPlacementPage({user, placementId, organisation}:{user: UserData, placementId: string, organisation?: {details: InstituteData|ProviderData, cohorts: {[key: string]: CohortData}}}) {
    const [placement, setPlacement] = useState<StudentPlacementData>();
    const [institute, setInstitute] = useState<InstituteData|undefined>(user.product === "institutes" ? organisation?.details as InstituteData : undefined);
    const [workflow, setWorkflow] = useState<WorkflowStage[]>();
    const [cohort, setCohort] = useState<CohortData>();
    const [student, setStudent] = useState<UserData>();
    const [wStage, setWStage] = useState<WorkflowStage&{files: {[fileId: string]: {name: string, url: string}}}>();
    const [snackbar, setSnackbar] = useState<{ open: boolean; message?: string, color?: string, button?: {title: string, onClick: () => any}}>({open: false});
    const [disableEmail, setDisableEmail] = useState({parent: false, provider: false});
    const [rejectELIPopup, setRejectELIPopup] = useState(false);
    const [eliPopupOpen, setEliPopupOpen] = useState(false);
    const [eliData, setELIData] = useState<string>();

    const [rejectExternalDocPopup, setRejectExternalDocPopup] = useState<"riskAssessment"|"dbsCheck"|false>(false);
    const [externalDocPopupOpen, setExternalDocPopupOpen] = useState<"riskAssessment"|"dbsCheck"|false>(false);
    const [riskAssessmentURL, setRiskAssessmentURL] = useState("");
    const [dbsCheckURL, setDbsCheckURL] = useState("");
    const [uploadProviderDocPopup, setUploadProviderDocPopup] = useState<"insurance"|"riskAssessment"|"DbsCheck"|undefined>();

    const [skipStagePopup, setSkipStagePopup] = useState(false);
    const [viewExternalLinkPopup, setViewExternalLinkPopup] = useState(false);
    const [externalLinkCopied, setExternalLinkCopied] = useState(false);
    const [uploadInsurance, setUploadInsurance] = useState(false);
    const [uploadRA, setUploadRA] = useState(false);
    const [uploadDBS, setUploadDBS] = useState(false);
    const [onboardingPopup, setOnboardingPopup] = useState(false);
    const [dismissOnboardingPopup, setDismissOnboardingPopup] = useState(false);
    const [addOnboardingDocsPopup, setAddOnboardingDocsPopup] = useState(false);
    const [shareStudentRequestPopup, setShareStudentRequestPopup] = useState(false);

    const [editable, setEditable] = useState(false);

    const [withdrawFromPlacementPopup, setWithdrawFromPlacementPopup] = useState(false);


    const firebaseQuery = new FirebaseQuery();

    useEffect(() => {
        if (!placementId) return;
        getPlacementbyId(placementId, setPlacement);
    }, [placementId]);



    useEffect(() => {
        console.log("p", placement);
        if (!placement) {
            return;
        }

        setEditable(user.userType === "Staff" || !((placement.providerCompleted && placement.providerCompleted.includes("details")) || placement.completed));


        if (placement.oId) {
            if (user.product === "institutes") {
                setInstitute(organisation?.details as InstituteData);
            } else if (user.product === "providers") {
                firebaseQuery.getDocData(["institutes", placement.oId]).then((i) => setInstitute(i as InstituteData));
            }
        }

        if (placement.cohort) {
            if (user.product === "institutes" && user.oId === placement.oId) {
                setCohort(organisation?.cohorts[placement.cohort]);
                setWorkflow(organisation?.cohorts[placement.cohort]?.workflow);
            } else {
                firebaseQuery.getDocData(["cohorts", placement.cohort]).then((w) => {
                    setWorkflow(w.workflow);
                    setCohort(w as CohortData);
                });
            }
        } else {
            setWorkflow(defaultStudentWorkflow);
        }


        if (user.userType === "Students") {
            setStudent(user);
        } else {
            if (placement.uid) {
                getUserById(placement.uid, undefined, false).then(setStudent);
            } else {
                setStudent({
                    details: {
                        forename: placement.studentForename || "",
                        surname: placement.studentSurname || "",
                    },
                    email: placement.studentEmail || ""
                } as UserData)
            }
        }

    }, [placement]);



    useEffect(() => {
        if (!workflow || !placement) return;

        const getAdditionalStageData = async ():Promise<WorkflowStage&{files: {[fileId: string]: {name: string, url: string}}}> => {

            const currentWorkflowStage = {...workflow.find((obj) => obj.id === placement.status) as WorkflowStage&{files: {[fileId: string]: {name: string, url: string}}}};
            // console.log("currentWorkflowStage", currentWorkflowStage)
            currentWorkflowStage.id = placement.status;
    
            // Get form data for current stage
            if (currentWorkflowStage.forms) {
                getFormsFromId(["forms"], currentWorkflowStage.forms).then((details) => {
                    currentWorkflowStage.formDetails = details as [{name:string}];
                });
            }
            if (currentWorkflowStage.files) {
                currentWorkflowStage.files = Object.fromEntries(await Promise.all(currentWorkflowStage.files.map(async (file) => {
                    const fileItem = await firebaseQuery.getDocData(["files", file]) as FileItem;
                    const url = await getDownloadURL(ref(storage, `institutes/${fileItem.oId}/${fileItem.fileName}`))
                    return [file, {name: fileItem.name, url: url}];
                })));
            }
            return currentWorkflowStage;
        }

        getAdditionalStageData().then(setWStage);
    }, [placement, workflow]);


    const editStage = async (nextStageId:number) => {
        if (!placementId || !wStage) return;
        await editPlacementStage(placementId, wStage.id, nextStageId);
        setSnackbar({open: true, message: "Stage updated."});
    };


    const sendEmail = (type:"provider"|"parent", forceSend?: boolean) => {
        if (!placement || !student) return undefined;

        const sendRequest = async () => {
            await executeCallable("placement-sendExternalEmail", {pId: placementId, userType: type});
            setSnackbar({open: true, message: "Email sent."});
            return;
        };

        if ((type === "provider" && !placement.providerEmail) || (type === "parent" && !student.details.parentEmail)) {
            setSnackbar({open: true, message: `Please add a ${type} email.`});
            return;
        }

        const insuranceRequired = Boolean(workflow?.find((i) => i.eli)) ? placement.insurance : true;
        const raRequired = Boolean(workflow?.find((i) => i.riskAssessment)) ? placement.riskAssessment : true;
        const dbsRequired = Boolean(workflow?.find((i) => i.dbsCheck)) ? placement.dbsCheck : true;

        const completedAllDocs = insuranceRequired && raRequired && dbsRequired;

        if (!forceSend && (type === "provider" && wStage?.userType !== "Provider" && completedAllDocs) || (type === "parent" && wStage?.userType !== "Parent")) {
            setSnackbar({open: true, message: "We don't currently need any information from this user.", color: "primary", button: {
                title: "Send Anyway",
                onClick: () => sendEmail(type, true)
            }});
            return;
        }

        if (!placement[`${type}Emailed`]) {
            setDisableEmail((x) => ({...x, [type]: true}));
            sendRequest().then(() => setDisableEmail((x) => ({...x, [type.toLowerCase()]: false})));
            return;
        }

        const previousEmailTime = new Date(placement[`${type}Emailed`].seconds * 1000);
        const today = new Date();
        const timeSinceEmail = getDateDiff(previousEmailTime, today);

        if (timeSinceEmail === 0) {
            setSnackbar({open: true, message: "Emails can only be sent after 1 day."});
            return;
        }
        setDisableEmail((x) => ({...x, [type.toLowerCase()]: true}));
        sendRequest().then(() => setDisableEmail((x) => ({...x, [type.toLowerCase()]: false})));
    };


    const onboardingStatus:"Add onboarding documents"|"Onboarding sent"|"Onboarding docs completed"|"Onboarding docs approved"|"Complete onboarding" =
        placement?.onboarding ? placement.onboarding.completed?.submitted ? placement.onboarding.completed.accepted ? "Onboarding docs approved" : "Onboarding docs completed" : (user.userType === "Staff" ? "Onboarding sent" : "Complete onboarding") : "Add onboarding documents";

    const signOffPlacements = getAccess(user, "signOffPlacements");

    let canEdit = false;

    if ((wStage?.userType === "Staff" && user.userType === "Staff" && user.product === "institutes") || (user.product === "providers" && wStage?.userType === "Provider") || user.userType === "Students" && wStage?.userType === "Students") {
        console.log("ALMOST CAN EDIT");
        if (user.userType === "Staff" && !signOffPlacements) {
            canEdit = false;
        } else {
            canEdit = true;
        }
    }


    const setFeedbackComplete = async (e: {instituteName: string}) => {
        if (!placement) throw new Error("No placement loaded.");
        await executeCallable("placement-submitExternalForm", {uid: placement.uid, pId: placementId, key: "students", formData: e, feedback: "students"});
    };

    const onFlagClick = async (e:FlagCodes, onClose?: boolean) => {
        if (!placement) return;
        if (e === "completeOnboarding" || e === "reviewOnboarding") {
            setOnboardingPopup(true);
        }
        if (e === "studentNotAccepted") {
            setShareStudentRequestPopup(true);
        }
        if (e === "providerEmailFailed") {
            firebaseQuery.update(["placements", placement.id], {flags: arrayRemove("providerEmailFailed")})
        }
        if (e === "parentEmailFailed") {
            firebaseQuery.update(["placements", placement.id], {flags: arrayRemove("parentEmailFailed")})
        }
        if (e === "noInsurance") {
            if (!eliData) {
                const storageRef = ref(storage, `insurance/${placement.providerContactId}.pdf`);
                const file = await getDownloadURL(storageRef).catch(() => placement.insuranceSkippedReason);
                setELIData(file);
            }
            setEliPopupOpen(true);
        }
        if (e === "noRiskAssessment") {
            if (!riskAssessmentURL && placement.riskAssessmentType === "file") {
                console.log("Risk assessment");
                const storageRef = ref(storage, `riskAssessments/${placement.placementId || placement.id}.pdf`);
                const file = await getDownloadURL(storageRef).catch(async () => await getDownloadURL(ref(storage, `riskAssessments/${placement.id}.pdf`)));
                setRiskAssessmentURL(file);
            }
            setExternalDocPopupOpen("riskAssessment");
        }
        if (e === "noDbsCheck") {
            if (!dbsCheckURL && placement.dbsCheckType === "file") {
                const storageRef = ref(storage, `dbsChecks/${placement.placementId || placement.id}.pdf`);
                const file = await getDownloadURL(storageRef);
                setDbsCheckURL(file);
            }
            setExternalDocPopupOpen("dbsCheck");
        }
        if (e === "addOnboarding") {
            if (onClose) {
                setDismissOnboardingPopup(true);
            } else {
                setAddOnboardingDocsPopup(true);
            }
        }
    };

    const approveELI = async () => {
        if (!placement) return;
        await executeCallable("insurance-approve", {oId: user.oId, providerContactId: placement.providerContactId});
        setEliPopupOpen(false);
    };

    const rejectELI = async ({reason}:{reason: string}) => {
        if (!placement || !institute) return;

        console.log("Reject", {reason: reason, placement: placement, instituteName: institute.name});
        await executeCallable("insurance-reject", {reason: reason, placementId: placementId, instituteName: institute.name});
        setRejectELIPopup(false);
        setEliPopupOpen(false);
    };

    const approveProviderDoc = async (type: "riskAssessment"|"dbsCheck") => {
        await executeCallable(`${type}-approve`, {oId: user.oId, placementId: placementId});
        setExternalDocPopupOpen(false);
    };

    const rejectProviderDoc = async ({reason}:{reason: string}, type: "riskAssessment"|"dbsCheck") => {
        if (!placement || !institute) return;

        await executeCallable(`${type}-reject`, {reason: reason, placementId: placementId, instituteName: institute.name, staffEmail: user.email});
        setRejectExternalDocPopup(false);
        setExternalDocPopupOpen(false);
    };

    const manuallyConfigureProvider = async () => {
        if (!placement) return;
        if (!placement.providerId) {
            const res = await executeCallable("providerContacts-uploadProviderDetails", {
                placement: {
                    id: placementId,
                    data: Object.fromEntries(Object.entries(placement).filter(([k]) => k !== "contactId")),
                    stage: wStage,
                },
                skipSearch: true}).catch((e) => {
                throw e;
            });
            console.log("RETURN", res.data);
            setPlacement((p) => ({...p, ...res.data as any}));
            if (uploadProviderDocPopup === "insurance") {
                setUploadProviderDocPopup(undefined);
                setUploadInsurance(true);
            } else if (uploadProviderDocPopup === "riskAssessment") {
                setUploadProviderDocPopup(undefined);
                setUploadRA(true);
            } else if (uploadProviderDocPopup === "DbsCheck") {
                setUploadProviderDocPopup(undefined);
                setUploadDBS(true);
            }
        }
    };

    const withdrawFromPlacement = async () => {
        if (user.userType !== "Students") throw new Error("Must be a student to withdraw.");
        await executeCallable("placement-withdraw", {placementId: placementId});
    }

    return {placement, wStage, student, workflow, editable, withdrawFromPlacementPopup, addOnboardingDocsPopup, setFeedbackComplete, setAddOnboardingDocsPopup, dismissOnboardingPopup, setDismissOnboardingPopup, setWithdrawFromPlacementPopup, withdrawFromPlacement, onFlagClick, setUploadInsurance, setUploadProviderDocPopup, setUploadRA, setUploadDBS, onboardingStatus, setSkipStagePopup, onboardingPopup, setViewExternalLinkPopup, setOnboardingPopup, setRejectELIPopup, eliData, riskAssessmentURL, dbsCheckURL, setExternalLinkCopied, skipStagePopup, snackbar, setSnackbar, cohort, disableEmail, rejectELIPopup, eliPopupOpen, rejectExternalDocPopup, externalDocPopupOpen, viewExternalLinkPopup, externalLinkCopied, uploadInsurance, uploadRA, uploadDBS, editStage, sendEmail, canEdit, approveELI, setEliPopupOpen, uploadProviderDocPopup, rejectELI, setRejectExternalDocPopup, setExternalDocPopupOpen, approveProviderDoc, rejectProviderDoc, manuallyConfigureProvider, institute, shareStudentRequestPopup, setShareStudentRequestPopup}
}

export function useOnboardingPopup({onboarding, providerId, placementId, user, onClose}:{onboarding: (
    OnboardingDocs&{
        completed: {
            submitted: boolean,
            submittedDate?: string,
            accepted?: boolean,
            filesViewed?: string[],
            formsCompleted?: {[key: string]: unknown},
            filesUploaded?: {[key: number]: string[]},
        }}
), placementId: string, user: UserData, providerId: string, onClose: () => void}) {

    const [fileUploadPopup, setFileUploadPopup] = useState<false|number>(false);
    const [form, setForm] = useState<{id: string, name: string, [key:string]: unknown}>();
    const [rejectPopup, setRejectPopup] = useState(false);
    const [mOnboarding, setMOnboarding] = useState(onboarding);
    const [completedSections, setCompletedSections] = useState<{
        submitted: boolean,
        submittedDate?: string,
        filesViewed?: string[] | undefined;
        formsCompleted?: {
            [key: string]: unknown;
        } | undefined;
        filesUploaded?: {
            [key: number]: string[];
        } | undefined;
    }>();

    const [uploadedFiles, setUploadedFiles] = useState<{[key: string]: FileItem}>({});
    const [viewableFiles, setViewableFiles] = useState<{[key: string]: FileItem}>();
    const [formDetails, setFormDetails] = useState<{
        [key: string]: {
            name: string;
            id: string;
            description?: string;
            product: Products;
            oId: string;
            form: CustomFormSchema;
        };
    }>({});

    const firebaseQuery = new FirebaseQuery();


    const addFile = (files: string[]) => {
        if (!files.length || fileUploadPopup === false) return;
        setMOnboarding((a) => editNestedObject(["completed", "filesUploaded", fileUploadPopup], a, files) as any);
        setFileUploadPopup(false);
    };

    const viewFile = (file: string, onOpen: (url: string) => void) => {
        setMOnboarding((a) => {
            const oldA = {...a};

            const viewedFiles = a.completed ? a.completed?.filesViewed || [] : [];
            if (viewedFiles?.includes(file)) return a;

            viewableFiles?.[file].url && onOpen(viewableFiles?.[file].url);
            viewedFiles?.push(file);

            const newA = editNestedObject(["completed", "filesViewed"], oldA, viewedFiles) as any;
            return newA;
        });
    };

    const setFormComplete = (e: {[key: string]: unknown}, formId?: string) => {
        const mFormId = formId || form?.id;
        if (!mFormId) return;
        setMOnboarding((a) => editNestedObject(["completed", "formsCompleted", mFormId], a, e) as any);
        setFileUploadPopup(false);
    };

    useEffect(() => {
        if (!onboarding) return;

        const getOnboardingData = async () => {
            const onboardingNew = {...onboarding};

            const onboardingFiles = onboarding.files ? Object.fromEntries(await Promise.all(onboarding.files?.map(async (fileId) => {
                const file = await firebaseQuery.getDocData(["files", fileId]);
                file.url = await getDownloadURL(ref(storage, `providers/${providerId}/${file.fileName}`));

                return [fileId, file];
            }))) : [];
            const onboardingForms = onboarding.forms ? Object.fromEntries(await Promise.all(onboarding.forms?.map(async (formId) => {
                return [formId, await firebaseQuery.getDocData(["forms", formId])];
            }))) : [];

            setViewableFiles(onboardingFiles);
            setFormDetails(onboardingForms);
            return onboardingNew;
        };
        getOnboardingData().then(setMOnboarding);
    }, [onboarding]);

    useEffect(() => {
        console.log("onboarding change", mOnboarding, !objectsEqual(mOnboarding, onboarding));
        if (!objectsEqual(mOnboarding, onboarding) && placementId) {
            firebaseQuery.update(["placements", placementId], {onboarding: mOnboarding} as Partial<StudentPlacementData>);
        }
    }, [mOnboarding]);



    const stagesCompleted = ():boolean => {
        console.log("CompletedSections", completedSections);
        for (const fileViewed of mOnboarding?.files || []) {
            if (!completedSections?.filesViewed?.includes(fileViewed)) {
                console.log("Checking file", fileViewed);

                console.log("File not completed");
                return false;
            }
        }
        for (const formCompleted of mOnboarding?.forms || []) {
            if (!Object.keys(completedSections?.formsCompleted || {}).includes(formCompleted)) {
                console.log("Form not completed");
                return false;
            }
        }

        for (var i; i++; i < (mOnboarding?.requiredFiles?.length || 1)) {
            if (!Object.keys(completedSections?.filesUploaded || {}).includes((i))) {
                return false;
            }
        }
        return true;
    };

    const rejectOnboarding = async ({reason}:{reason: string}) => {
        if (user.product !== "providers") return;

        return await executeCallable("placement-rejectOnboarding", {reason: reason, placementId: placementId}).then(()=> {
            setRejectPopup(false);
            onClose();
        }).catch(() => {
            throw new Error("Error");
        });
    };

    const acceptOnboarding = async () => {
        if (user.product !== "providers" || !placementId) return;

        await firebaseQuery.update(["placements", placementId], {"onboarding.completed.accepted": true});
    };


    const submit = async () => {
        // Check all stages completed.
        if (!placementId) return;
        if (!stagesCompleted()) throw new Error("Complete all forms before submitting.");
        await firebaseQuery.update(["placements", placementId], {["onboarding.completed.accepted"]: false, ["onboarding.completed.submitted"]: true, ["onboarding.completed.submittedDate"]: convertDate(new Date(), "dbstring") as string});
        //executeCallable("sendOnboardingSubmittedEmail", {});
    };

    useEffect(() => {

        const getUploadedFiles = async () => {
            if (!mOnboarding.completed) {
                setUploadedFiles({});
                return
            };
            
            const fileIds = Object.values(mOnboarding.completed.filesUploaded || {})
                .flatMap(fileIds => fileIds);
            
            const fileDataPromises = fileIds.map(async (fileId) => {
                const fileData = await firebaseQuery.getDocData(["files", fileId]) as FileItem;
                fileData.url = await getDownloadURL(ref(storage, `userFiles/${fileData.fileName}`));
            
                return [fileId, fileData] as [string, FileItem]
            });
            
            const fileDataArray = await Promise.all(fileDataPromises);
            const fileDataObj = Object.fromEntries(fileDataArray)
            setUploadedFiles(fileDataObj)
        }

        const addCompletedSectionURLs = async () => {
            const completedSectionsWithURLs = {...mOnboarding?.completed};

            completedSectionsWithURLs.filesUploaded = Object.fromEntries(await Promise.all(Object.entries(completedSectionsWithURLs.filesUploaded || {}).map(async ([fileId, items]) => {
                const filesWithObjects = await Promise.all(items.map(async (itemId) => {
                    const file = await firebaseQuery.getDocData(["files", itemId]);
                    const fileUrl = await getDownloadURL(ref(storage, `userFiles/${file.fileName}`));
                    console.log("|FILEURL", fileUrl);
                    return fileUrl;
                }));
                return [fileId, filesWithObjects];
            })));

            setCompletedSections(completedSectionsWithURLs);
        };
        getUploadedFiles();
        addCompletedSectionURLs();
    }, [mOnboarding]);

    return {addFile, viewFile, uploadedFiles, setFormComplete, setRejectPopup, stagesCompleted, setForm, mOnboarding, form, submit, acceptOnboarding, rejectOnboarding, rejectPopup, completedSections, fileUploadPopup, setFileUploadPopup, viewableFiles, formDetails};
}





export function useLoadAddresses(user: UserData, limitItems?: number, queryConstraint?: QueryConstraint[], request?: boolean) {
    const [addresses, setAddresses] = useState<{[key: string]: OrganisationAddress&{listings: number}}>({});
    const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
    const [loading, setLoading] = useState<boolean>(false);

    const firebaseQuery = new FirebaseQuery();

    const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);

    const changeQueryConstraints = (e: QueryConstraint[]) => {
        setQueryConstraints([...(queryConstraint || []), ...e]);
    };
    
    const loadAddresses = () => {
        const constraints:QueryConstraint[] = [where("oId", "==", user.oId), where("product", "==", user.product), orderBy(documentId())]

        if (limitItems) {
            constraints.push(limit(limitItems));
        }

        if (user.viewAddresses === "all" || user.userGroup === "admin" || request) {
            if (lastDoc?.id) {
                constraints.push(startAfter(lastDoc?.id));
            }
        } else if (user.viewAddresses === "request") {
            if (!user.visibleAddresses?.length) return;

            constraints.push(where(documentId(), "in", user.visibleAddresses))
            if (lastDoc?.id) {
                constraints.push(startAfter(lastDoc?.id));
            }
        } else {
            setLoading(false);
            return;  // viewAddresses === "none", no need to load anything
        }
        queryConstraints && constraints.unshift(...queryConstraints);

        return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
            const deletedAddresses = snapshot.docChanges().map((change) => {
                if (change.type === "removed") {
                    return change.doc.id;
                }
                return;
            })

            setAddresses((prev) => Object.fromEntries(Object.entries(prev).filter(([k]) => !deletedAddresses.includes(k))))

            if (!snapshot.empty) {


                const newAddresses:[string, OrganisationAddress][] = snapshot.docs.map(doc => ([doc.id, {id: doc.id, ...doc.data() as OrganisationAddress}]));

                const withListings:{[key: string]: OrganisationAddress&{listings: number}} = Object.fromEntries(await Promise.all(newAddresses.map(async ([k, address]) => {
                    const listings = await firebaseQuery.getCount("placementListings", [where("providerId", "==", user.oId), where("addressId", "==", k)]);
                    return [k, {...address, listings: listings}];
                })));
                
                setAddresses(prev => ({...prev, ...withListings}));
                setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
            }
            setLoading(false);
        }), "addresses", constraints, undefined, true);
    };

    useEffect(() => {
        const unsubscribe = loadAddresses();

        return () => {
            if (unsubscribe) {
                unsubscribe();  // Unsubscribe from the snapshot listener when the component unmounts
            }
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [queryConstraints]);

    const onScrollBottom = () => {
        if (!limitItems) return;
        if (!loading) {
            setLoading(true);
            loadAddresses();
        }
    };

    return { addresses, onScrollBottom, loading, changeQueryConstraints };
}




export function useLoadListings(user: UserData, queryConstraint?: QueryConstraint[], request?: boolean) {
    const [listings, setListings] = useState<[string, PlacementListing&{applicants?: number, scheduled?: number, active?: number}][]>([]);
    const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
    const [loading, setLoading] = useState<boolean>(false);

    const firebaseQuery = new FirebaseQuery();
    
    const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);

    const changeQueryConstraints = (e: QueryConstraint[]) => {
        setQueryConstraints([...(queryConstraint || []), ...e]);
    };

    const loadListings = () => {
        const constraints:QueryConstraint[] = [where("providerId", "==", user.oId), limit(10), orderBy(documentId())]
        
        if (user.viewPlacementListings === "all" || user.userGroup === "admin" || request) {
            if (lastDoc?.id) {
                constraints.push(startAfter(lastDoc));
            }
        } else {
            if (!user.visibleListings?.length) return;
            constraints.push(where(documentId(), 'in', user.visibleListings));
            if (lastDoc?.id) {
                constraints.push(startAfter(lastDoc));
            }
        }

        if (user.viewAddresses !== "all" && user.viewPlacementListings === "all" && user.userGroup !== "admin") {
            if (!user.visibleAddresses?.length) return;

            constraints.push(where('addressId', 'in', user.visibleAddresses));
        }
        queryConstraints && constraints.unshift(...queryConstraints);

        return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
            const deletedListings = snapshot.docChanges().map((change) => {
                if (change.type === "removed") {
                    return change.doc.id;
                }
                return;
            })

            setListings((prev) => prev.filter(([k]) => !deletedListings.includes(k)))

            if (!snapshot.empty) {
                const newListings:[string, PlacementListing][] = snapshot.docs.map(doc => ([doc.id, {...doc.data() as PlacementListing, id: doc.id}]));

                const listingsWithAdditionalData:[string, PlacementListing&{applicants?: number, scheduled?: number, active?: number}][] = await Promise.all(newListings.map(async ([id, listing]) => {
                    const listingWithAdditionalData = {...listing} as PlacementListing&{applicants?: number, scheduled?: number, active?: number};
    
                    if (listingWithAdditionalData.applicants !== undefined) return [id, listingWithAdditionalData];
    
                    listingWithAdditionalData.applicants = await firebaseQuery.getCount("applications", [where("providerId", "==", user.oId), where("placementId", "==", id), where("status", "==", "submitted")]);
                    listingWithAdditionalData.scheduled = await firebaseQuery.getCount("placements", [where("providerId", "==", user.oId), where("placementId", "==", id), where("inProgress", "==", true), where("startDate", ">", convertDate(new Date(), "dbstring"))]);
                    listingWithAdditionalData.active = await firebaseQuery.getCount("placements", [where("providerId", "==", user.oId), where("placementId", "==", id), where("active", "==", true)]);
    
                    return [id, listingWithAdditionalData];
                }));
                setListings(prev => (Object.entries({...Object.fromEntries(prev), ...Object.fromEntries(listingsWithAdditionalData)})));
                setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
            }
            setLoading(false);
        }), "placementListings", constraints, undefined, true);
    };

    useEffect(() => {

        loadListings();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [queryConstraints]);

    const onScrollBottom = () => {
        if (!loading) {
            setLoading(true);
            loadListings();
        }
    };

    return { listings, onScrollBottom, loading, changeQueryConstraints };
}



export function useLoadProviderPlacements(user: UserData, queryConstraint?: QueryConstraint[], placementId?: string) {
    const [placements, setPlacements] = useState<[string, StudentPlacementData&{applicants?: number, scheduled?: number, active?: number}][]>([]);
    const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
    const [loading, setLoading] = useState<boolean>(false);

    const firebaseQuery = new FirebaseQuery();
    
    const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);

    if (user.product !== "providers") throw new Error("Only providers can use this hook.");

    const changeQueryConstraints = (e: QueryConstraint[]) => {
        setQueryConstraints([...(queryConstraint || []), ...e]);
    };

    const loadListings = () => {
        const constraints:QueryConstraint[] = [where("providerId", "==", user.oId), limit(10), orderBy(documentId())]

        if (placementId) {
            constraints.push(where("placementId", "==", placementId));
        }
        
        if (user.viewPlacementListings === "all" || user.userGroup === "admin") {
            if (lastDoc?.id) {
                constraints.push(startAfter(lastDoc));
            }
        } else {
            if (!user.visibleListings?.length) return;
            constraints.push(where("placementId", 'in', user.visibleListings));
            if (lastDoc?.id) {
                constraints.push(startAfter(lastDoc));
            }
        }

        if (user.viewAddresses !== "all" && user.viewPlacementListings === "all" && user.userGroup !== "admin") {
            if (!user.visibleAddresses?.length) return;

            constraints.push(where('addressId', 'in', user.visibleAddresses));
        }
        queryConstraints && constraints.unshift(...queryConstraints);

        return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
            const deletedListings = snapshot.docChanges().map((change) => {
                if (change.type === "removed") {
                    return change.doc.id;
                }
                return;
            })

            setPlacements((prev) => prev.filter(([k]) => !deletedListings.includes(k)))

            if (!snapshot.empty) {
                const newPlacements:[string, StudentPlacementData][] = snapshot.docs.map(doc => ([doc.id, {...doc.data() as StudentPlacementData, id: doc.id}]));

                const withAdditionalData = await Promise.all(newPlacements.map(async ([id, placement]) => {
                    const student = placement.uid ? await firebaseQuery.getDocData(["users", placement.uid || ""]).catch(() => false) as UserData|false :
                    {
                        details: {
                            forename: placement.studentForename,
                            surname: placement.studentSurname,
                        },
                        email: placement.studentEmail
                    };
                    const listing = await firebaseQuery.getDocData(["placementListings", placement.placementId || ""]).catch(() => false) as PlacementListing;
    
                    return [id, {...placement, student: student, listing: listing}];
                }));
                setPlacements(prev => (Object.entries({...Object.fromEntries(prev), ...Object.fromEntries(withAdditionalData)})));
                setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
            }
            setLoading(false);
        }), "placements", constraints, undefined, true);
    };

    useEffect(() => {
        loadListings();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [queryConstraints]);

    const onScrollBottom = () => {
        if (!loading) {
            setLoading(true);
            loadListings();
        }
    };

    return { placements: Object.fromEntries(placements), onScrollBottom, loading, changeQueryConstraints };
}



export function useLoadApplications({user, applicationType, listingId, queryConstraint}:{user: UserData, applicationType?: "all"|"actionRequired"|"awaitingStudent"|"closed", listingId?: string, queryConstraint?: QueryConstraint[]}) {
    const [applications, setApplications] = useState<[string, Application][]>([]);
    const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
    const [loading, setLoading] = useState<boolean>(false);
    const [type, setType] = useState<"actionRequired"|"awaitingStudent"|"closed"|"all">(applicationType || "all");

    const firebaseQuery = new FirebaseQuery();

    const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);

    const changeQueryConstraints = (e: QueryConstraint[]) => {
        setQueryConstraints([...(queryConstraint || []), ...e]);
    };

    const loadApplications = () => {
        const constraints:QueryConstraint[] = [where("providerId", "==", user.oId), limit(10), orderBy(documentId())]
        
 
        if (lastDoc?.id) {
            constraints.push(startAfter(lastDoc));
        }
        
        switch (type) {
            case "actionRequired":
                constraints.push(where("status", "==", "submitted"), where("reqUserType", "==", "Staff"));
                break;
            case "awaitingStudent":
                constraints.push(where("status", "==", "submitted"), where("reqUserType", "==", "Students"));
                break;
            case "closed":
                constraints.push(where("status", "in", ["approved", "declined"]));
                break;
            default:
                constraints.push(where("status", "==", "submitted"));
            }
            if (listingId) {
                constraints.push(where("listingId", "==", listingId));
            }
        
        console.log("Constraints before user group check", constraints);
        if (user.viewAddresses !== "all" && user.userGroup !== "admin") {
            if (user.viewPlacementListings === "all") {
                if (!user.visibleAddresses?.length) return;
                constraints.push(where('addressId', 'in', user.visibleAddresses));
            } else {
                if (!user.visibleListings?.length) return;
                constraints.push(where('placementId', 'in', user.visibleListings));
            }
        }

        queryConstraints && constraints.unshift(...queryConstraints);
        console.log("Constraints after user group check", constraints);

        return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
            const deletedApplications = snapshot.docChanges().map((change) => {
                if (change.type === "removed") {
                    return change.doc.id;
                }
                return;
            })

            console.log("applicantCount", snapshot.size);

            setApplications((prev) => prev.filter(([k]) => !deletedApplications.includes(k)))

            if (!snapshot.empty) {
                const newApplications:[string, Application][] = snapshot.docs.map(doc => ([doc.id, {id: doc.id, ...doc.data() as Application}])).filter(([, v]) => (v as Application).status !== "draft") as [string, Application][];

                setApplications(prev => ([...prev, ...newApplications]));
                setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
            } else {
                setApplications([]);
                setLastDoc(null);
            }
            setLoading(false);
        }), "applications", constraints, undefined, true);
    };

    useEffect(() => {
        loadApplications();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [type, queryConstraints]);

    const onScrollBottom = () => {
        if (!loading) {
            setLoading(true);
            loadApplications();
        }
    };

    return { applications, type, setType, onScrollBottom, loading, changeQueryConstraints };
}



export type FilterObject = {
    [key: string]: {
        label: string,
        value?: unknown,
        values?: {[key:string|number]: string|{label: string, test: QueryFieldFilterConstraint}},
        type: "dropdown"|"string"|"number"
    }
}

export type DataViewerPaginater = {
    view: "list"|"table";
    queryLimit?: number;
    formatItems?: (key: string, item: any) => Promise<{ key: string, item: any }> | { key: string, item: any };
    snapshot?: boolean;
    filters?: FilterObject;
    onSearch?: boolean | ((search?: string, sort?: [string, {value: string, direction: "asc"|"desc"}], page?: number, filters?:FilterObject, limit?: number) => Promise<{ [key: string]: any }>);
    data?: {[key:string]:{[key:string]: unknown}}|QueryObject[];
    additionalEntryProcessing?: (k: string, v: any) => Promise<[string, any]|undefined> | [string, any]|undefined,
    sorts?: Sorts,
    initialSort?: string,
    initialSearch?: string
}

export function useDataViewerPaginator({view: initialView, sorts, queryLimit=10, additionalEntryProcessing, formatItems, snapshot, filters: initialFilters, initialSort, initialSearch, onSearch, data}:DataViewerPaginater) {
    const [tableData, setTableData] = useState<{[key:string]:{[key:string]: unknown}}|undefined>(data ? Array.isArray(data) ? {} : Object.fromEntries(Object.entries(data).slice(0, queryLimit)) : undefined);
    const [page, setPage] = useState([1, 0]);
    const [view, setView] = useState(initialView);
    const [filters, setFilters] = useState<FilterObject|undefined>(initialFilters);
    const [queryAnchor, setQueryAnchor] = useState<{startKey: string, endKey: string, startQueryPos: number, endQueryPos: number}>({startKey: "", endKey: "", startQueryPos: 0, endQueryPos: 0});
    const [prevEntryIds, setPrevEntryIds] = useState<{[key:string]:number}>({});
    const [dataListenerUnsubscribe, setDataListenerUnsubscribe] = useState<{[key: string] : Unsubscribe}>();
    const [loading, setLoading] = useState<boolean|"loaded">(true);
    const [searchString, setSearchString] = useState<string>();
    const [sort, setSort] = useState<[string, {value: string, direction: "asc"|"desc"}]|undefined>((initialSort && sorts && sorts[initialSort]) ? [initialSort, sorts[initialSort]] : undefined);
    const [fData, setFData] = useState(data);

    const processedData = async (k: string, v: any):Promise<[string, any] | undefined> => additionalEntryProcessing ? await additionalEntryProcessing(k, v) : [k, v];

    const setTableDataFromDefinedData = async () => {
        if (!fData || Array.isArray(fData)) return;

        const dataWithAdditionalProcessingPossibleNulls: [string, any][] = (await Promise.all(Object.entries(fData).map(async ([k, v]) => (await processedData(k, v)) as [string, any])))
        const dataWithAdditionalProcessing = dataWithAdditionalProcessingPossibleNulls.filter(([k, v]) => v);

        const searchedData: [string, any][] = searchString ? dataWithAdditionalProcessing.filter(([, v]) => {
            const values = Object.values(v).join(", ");
            console.log("VALUESTRING", v);
            return values.includes(searchString);
        }) : dataWithAdditionalProcessing;

        const filteredData:[string, any][] = filters && Object.keys(filters).length > 0 ? searchedData.filter(([, dataValue]) => Object.entries(filters).every(([filterKey, filterValue]) => {
            const value = dataValue[filterKey];
            if ((typeof value === "number") && value === parseInt(filterValue.value as string)) return true;
            if ((typeof value === "boolean") && value === (filterValue.value === "true")) return true;
            if ((typeof value === "boolean") && value === (filterValue.value === "false")) return true;

            if ((typeof value === "string" || Array.isArray(value)) && value.includes(filterValue.value as string)) return true;
            return false;
        })) : searchedData;
        if (view === "table") {
            if (!queryLimit) throw new Error("Tables must have a limit defined.");
            const newData:[string, any][] = filteredData.slice((page[0] - 1)*queryLimit, page[0]*queryLimit);
            setTableData(Object.fromEntries(newData));
            if (Object.keys(Object.fromEntries(newData)).pop() === Object.keys(fData).pop()) {
                setLoading("loaded");
            } else {
                setLoading(false);
            }
            return;
        }
        if (view === "list") {
            setTableData(Object.fromEntries(filteredData));
            setLoading("loaded");
        }
    };

    const getDataFromQuery = async (
        itemList: {[key: string]: any} = {}, currentQueryAnchor=queryAnchor, cursorDirection?:"increase"|"decrease"|undefined, prevEntries=prevEntryIds, loadMoreFromQuery=false):Promise<any> => {
        
        // if (!filters) return;
        setLoading(true);
        if (!Array.isArray(fData)) {
            setTableDataFromDefinedData();
            return;
        }
        if (!queryLimit) throw new Error("Firestore queries must have a limit defined.");

        if (onSearch && (searchString || sort)) {
            if (typeof onSearch === "boolean") throw new Error("When using Firestore queries, an onSearch function should be passed to retrieve data externally. Additional processing is however completed in this hook.");
            const searchData = await onSearch(searchString, sort, page[0], filters, queryLimit);
            console.log("Search data", searchData)
            const dataWithAdditionalProcessing = ((await Promise.all(Object.entries(searchData).map(async ([k, v]) => await processedData(k, v)))).filter((a) => a?.[1])) as [string, any][];
            console.log("dataWithAdditionalProcessing", dataWithAdditionalProcessing)

            setTableData((old) => {
                console.log("Setting table data from search")
                if (view === "table") {
                    console.log("Setting table data as table")

                    return {...Object.fromEntries(dataWithAdditionalProcessing)};
                }
                console.log("Setting table data as list")
                return {...old, ...Object.fromEntries(dataWithAdditionalProcessing)};
            });

            if (dataWithAdditionalProcessing.length < queryLimit) {
                setLoading("loaded");
            } else {
                setLoading(false);
            }
            return;
        }

        let cursorPos:number;
        if (page[0] > page[1]) {
            cursorPos = currentQueryAnchor.endQueryPos;
        } else {
            cursorPos = currentQueryAnchor.startQueryPos;
        }

        const querySchema:QueryObject = fData[cursorPos];

        const createQuery = (queryData:QueryObject) => {
            const constraints:any[] = [];
            queryData.where && queryData.where.forEach((w) => {
                constraints.push(where(...w));
            });

            console.log("FILTERS IN DATA PAGINATOR", filters);

            filters && Object.entries(filters).filter(([, value]) => value.value).forEach(([key, value]) => {
                if (value.type === "dropdown" && value.values && typeof value.values[value.value as string] !== "string") {
                    const chosenValue = value.values[value.value as string] as {
                        label: string;
                        test: QueryFieldFilterConstraint|QueryObjectConstraint;
                    };
                    constraints.push(Array.isArray(chosenValue.test) ? where(chosenValue.test[0], chosenValue.test[1], chosenValue.test[2]) : chosenValue.test);
                } else {
                    const filterValue = (value.type === "number" || value.type === "dropdown") ? parseInt(value.value as string) || value.value : value.value;
                    constraints.push(where(key, "==", filterValue));
                }
            });

            constraints.push(orderBy(queryData.orderBy ? queryData.orderBy === "documentId" ? documentId() : queryData.orderBy : documentId()));
            console.log("PAGE", page);
            console.log("QUERYANC", currentQueryAnchor);
            if (page[0] > page[1] && !cursorDirection) { // Going up
                currentQueryAnchor.endKey && constraints.push(startAfter(currentQueryAnchor.endKey));
                constraints.push(limit(queryLimit));
                if (!loadMoreFromQuery) {
                    currentQueryAnchor = {...currentQueryAnchor, startQueryPos: currentQueryAnchor.endQueryPos};
                }
            } else if (page[0] < page[1] && !cursorDirection) { // Going down
                if (!loadMoreFromQuery) {
                    currentQueryAnchor = {...currentQueryAnchor, endQueryPos: currentQueryAnchor.startQueryPos};
                }
                constraints.push(limitToLast(queryLimit));
                if (currentQueryAnchor.startKey) {
                    currentQueryAnchor.startKey && constraints.push(endBefore(currentQueryAnchor.startKey));
                } else {
                    currentQueryAnchor.startKey && constraints.push(endAt(currentQueryAnchor.startKey));
                }
            } else {
                if (cursorDirection === "decrease") {
                    constraints.push(limitToLast(queryLimit));
                } else {
                    constraints.push(limit(queryLimit));
                }
            }
            return constraints;
        };

        const constraints = createQuery(querySchema);
        const q = query(collection(db, ...(querySchema.path as [any])), ...(constraints));

        console.log("Fetching docs", constraints);

        if (snapshot) {
            // Use onSnapshot to get real-time updates
            const unsubscribe = onSnapshot(q, (querySnapshot) => {
                handleQuerySnapshot(querySnapshot);
            });
            // Save the unsubscribe function to state so we can dispose of it later
            setDataListenerUnsubscribe((d) => ({...(d || {}), [cursorPos]: unsubscribe}));
            return;
        } else {
            // Just get the docs without setting up a listener
            const queryData = await getDocs(q);
            handleQuerySnapshot(queryData);
        }

        // Function to handle query snapshot
        async function handleQuerySnapshot(querySnapshot: QuerySnapshot) {
            if (!Array.isArray(fData)) throw new Error("Called querySnapshot but data is defined.");
            if (!queryLimit) throw new Error("Firestore queries must have a limit defined.");

            const queryResults: { [key: string]: { [key: string]: unknown } } = {};
            let index = 0; // Declare the index variable

            const reverseIfBack = (docs: QueryDocumentSnapshot<DocumentData>[]) =>
                page[0] < page[1] ? docs.reverse() : docs;

            // Process each document in the querySnapshot
            for (const doc of reverseIfBack(querySnapshot.docs)) {
                if ((Object.keys(queryResults).length + Object.keys(itemList).length) === queryLimit) {
                    break;
                }

                let position = Object.keys(itemList).length+(page[0]-1)*queryLimit+index+1;
                if (page[0] < page[1]) {
                    position = (page[0])*queryLimit-index-Object.keys(itemList).length;
                }


                if (itemList[doc.id] || (prevEntries[doc.id] && prevEntries[doc.id] !== position)) {
                    console.log("Removing ", doc.id, ": E=", prevEntries[doc.id], ", G=", position);
                    continue;
                }

                let item: {[key: string]: unknown}|false = doc.data();
                item.id = doc.id;
                let key:string|undefined = doc.id;

                // Apply additionalEntryProcessing if provided
                if (additionalEntryProcessing) {
                    const [processedKey, processedItem] = await additionalEntryProcessing(doc.id, item) || [undefined, undefined];
                    key = processedKey;
                    item = processedItem;
                }
                if (!item || !key) continue;

                queryResults[key] = item;
                index += 1;

                if (prevEntries[key]) continue;
                prevEntries[key] = position;
            }

            if (cursorDirection === "decrease" || page[0] < page[1]) {
                itemList = {...Object.fromEntries(Object.entries(queryResults).reverse()), ...itemList};
            } else {
                itemList = {...itemList, ...queryResults};
            }


            console.log("ITEM LIST", itemList);
            // Updating state with the new data and query anchors


            setPrevEntryIds(prevEntries);

            if (querySnapshot.size < queryLimit && Object.keys(itemList).length < queryLimit) {
                // If we have ran out of entries, increase or decrease the index.
                if (page[0] > page[1] && cursorPos+1 < fData.length) {
                    return getDataFromQuery(itemList, {...currentQueryAnchor, endQueryPos: currentQueryAnchor.endQueryPos+1}, "increase", prevEntries);
                } else if (page[0] < page[1] && cursorPos > 0) {
                    return getDataFromQuery(itemList, {...currentQueryAnchor, startQueryPos: currentQueryAnchor.startQueryPos-1}, "decrease", prevEntries);
                }
            }

            if (Object.keys(itemList).length < queryLimit && querySnapshot.size === queryLimit) {
                console.log("Shorter than ten");
                return getDataFromQuery(itemList, {...currentQueryAnchor, startKey: Object.keys(itemList)[0], endKey: Object.keys(itemList).slice(-1)[0]}, undefined, prevEntries, true);
            }

            if (querySnapshot.size === 0 &&
                Object.keys(itemList).length === 0 &&
                currentQueryAnchor.endQueryPos+1 === fData.length &&
                page[0] > 1) {
                if (view === "table") {
                    setTableData({});
                }
                setQueryAnchor((a) => ({...a, startKey: ""}));
                setLoading("loaded");
                return;
            }

            if (Object.keys(itemList).length < queryLimit) {
                setLoading("loaded");
            } else {
                setLoading(false);
            }

            setQueryAnchor({...currentQueryAnchor, startKey: Object.keys(itemList)[0], endKey: Object.keys(itemList).slice(-1)[0]});

            setTableData((old) => {
                if (view === "table") {
                    return {...itemList};
                }
                return {...old, ...itemList};
            });
        }
    };

    const reset = () => {
        console.log("Resetting after filters?");
        setPage([1, 0]);
        setTableData({});
        setQueryAnchor({startKey: "", endKey: "", startQueryPos: 0, endQueryPos: 0});
        setPrevEntryIds({});
        dataListenerUnsubscribe && Object.values(dataListenerUnsubscribe).map((u) => u());
        setDataListenerUnsubscribe(undefined);
    };

    useEffect(() => {
        if (objectsEqualNew(fData, data)) return;
        console.log("data reset.", fData, data);
        setFData(data);
        reset();
    }, [data]);

    const updateFilters = (fFilters?: FilterObject) => {
        if (!objectsEqualNew(fFilters, filters)) {
            console.log("FILTER RESET", fFilters, filters);
            setFilters(fFilters);
            reset();
        }
    }

    
    const updateSearch = (search: string) => {
        if (searchString === search) return;
        console.log("SEARCH RESET", searchString, search);
        setSearchString(search);
        reset();
    }

    const updateView = (v: "list" | "table") => {
        if (v === view) return;
        console.log("View reset", view, v);
        setView(v);
        reset();
    }


    // Fetch new data when queries or page change
    useEffect(() => {
        getDataFromQuery();
        dataListenerUnsubscribe && Object.values(dataListenerUnsubscribe).map((u) => u());
    }, [page]);

    const pageUp = () => {
        setPage((p) => ([p[0]+1, p[0]]));
    };

    const pageDown = () => {
        setPage((p) => ([p[0]-1, p[0]]));
    };

    const updateSort = (sortLabel?: string) => {
        if (!sortLabel) {
            setSort(undefined);
        } else {
            if (!sorts || !sorts[sortLabel]) return;
            setSort([sortLabel, sorts[sortLabel]]);
        }
        reset();
    }

    return ({...{tableData, pageUp, pageDown, search: searchString, setFilters: updateFilters, page: page[0], sorts, loading, sort, updateSort: updateSort, setView: updateView, updateSearch}});
};
