import {addDoc, collection, CollectionReference, deleteDoc, doc,
    DocumentData, DocumentReference, FieldValue, getCountFromServer, getDoc,
    getDocs, onSnapshot, Query, query, QueryConstraint, QueryDocumentSnapshot, setDoc, updateDoc, where} from "firebase/firestore";
import {SetStateAction} from "react";
import {db} from "./firebaseConfig";
import {pathToArr} from "./util";
import {Products} from "../typeDefinitions";
import { AppDispatch } from "../config";

export default class FirebaseQuery {
    private createQuery(queryType:"collection"|"doc", path:string[]|string, constraints?:QueryConstraint[]|QueryConstraint) {
        switch (queryType) {
        case "collection":
            return query(collection(db, ...(pathToArr(path) as [any])), ...[...(constraints ? Array.isArray(constraints) ? constraints : [constraints] : [])]);
        case "doc":
            return doc(db, ...(pathToArr(path) as [any]));
        }
    }

    async getCount(path:string[]|string, constraints?:QueryConstraint[]|QueryConstraint):Promise<number> {
        console.log("GET COUNT", path, constraints);
        const countObject = await getCountFromServer(this.createQuery("collection", path, constraints) as Query).catch((e) => console.log(e));
        if (!countObject) return 0;
        return (countObject).data().count;
    }

    async add(path:string[]|string, data:{[key:string]:unknown}) {
        if (path.length % 2 === 0) {
            throw new Error("Provide a collection reference");
        }
        return await addDoc(this.createQuery("collection", path) as CollectionReference, data);
    }

    async set(path:string[]|string, data:{[key:string]:unknown}, merge=false) {
        if (path.length % 2 === 1) {
            throw new Error("Provide a document reference");
        }
        return await setDoc(this.createQuery("doc", path) as DocumentReference, data, {merge: merge});
    }

    async update(path:string[]|string, data:{ [key: string]: unknown}) {
        if (path.length % 2 === 1) {
            throw new Error("Provide a document reference");
        }
        console.log("update", path, data);
        return await updateDoc(this.createQuery("doc", path) as DocumentReference, data as { [key: string]: FieldValue | Partial<unknown> | undefined});
    }

    async delete(path:string[]|string) {
        if (path.length % 2 === 1) {
            throw new Error("Provide a document reference");
        }
        await deleteDoc(this.createQuery("doc", path) as DocumentReference);
    }

    async copy(path: string[]|string, id: string, oId: string, product: Products, additionalData?: {[key: string]: unknown}) {
        const existing = await this.getDocData([...path, id]);
        console.log("existing", existing);
        existing.name = await this.getUniqueDocName(path, existing.name, oId, product);
        if (existing.default) {
            existing.default = false;
        }
        return await this.add(path, {...existing, ...additionalData});
    }

    async getUniqueDocName(path: string[]|string, name: string, oId: string, product: Products): Promise<string> {
        const q = this.createQuery("collection", path, [where("name", "==", name), where("oId", "==", oId), where("product", "==", product)]);

        const querySnapshot = await getDocs(q as Query);
        if (!querySnapshot.empty) {
            return this.getUniqueDocName(path, name += "-", oId, product);
        }
        return name;
    }

    async getDocData(path:string[]|string) {
        console.log("GET DOC", path);
        if (path.length % 2 === 1) {
            throw new Error("Provide a document reference");
        }
        const result = await getDoc(this.createQuery("doc", path) as DocumentReference).catch((error) => {
            console.log(`Error in snapshot ${path}`, error);
            throw error;
        });
        if (!result.exists()) {
            throw new Error(`Document doesn't exist. ${typeof path === "string" ? path : path.join(", ")}`);
        }
        const resultData = result.data() || {};
        resultData.id = result.id;
        return resultData;
    }

    async getDocsWhere(path: string[]|string, constraints?:QueryConstraint[]|QueryConstraint, rawDocs=false) {
        const q = this.createQuery("collection", path, constraints);

        const docs = await getDocs(q as Query).catch((e) => console.log(e, q));
        if (rawDocs) return docs;
        if (!docs) return {};
        console.log("GET DOCS WHERE", docs.size, path, constraints);

        return docs.docs.reduce((acc: {[k: string]: any}, document) => {
            const docData = {...document.data(), id: document.id};
            acc[document.id] = docData;
            return acc;
        }, {});
    }

    async collectionUpdate(data:{[key:string]:unknown}, path: string[]|string, constraints?:QueryConstraint[]|QueryConstraint) {
        if (path.length % 2 === 0) {
            throw new Error("Provide a collection reference");
        }
        const q = this.createQuery("collection", path, constraints) as Query;
        (await getDocs(q)).docs.forEach((doc) => {
            this.update(doc.ref.path.split("/"), data);
        });
    }

    documentSnapshot(path: string[]|string, setState: SetStateAction<any>) {
        console.log("Building snapshot query: ", path);
        const q = this.createQuery("doc", path) as DocumentReference;
        console.log("Query created");
        return onSnapshot(q, (doc) => {
            console.log("Data", doc.data());
            setState({...doc.data(), id: doc.id});
        }, (error) => {
            console.log(`Error in snapshot ${path}`, error);
        });
    }

    collectionSnapshot(setState:SetStateAction<any>|((e: DocumentData|{[key: string]: any}) => void)|Promise<((e: DocumentData|{[key: string]: any}) => void)>, path: string[]|string, constraints?:QueryConstraint[]|QueryConstraint, dispatch?: AppDispatch, raw?: boolean) {
        const q = this.createQuery("collection", path, constraints) as Query<DocumentData>;

        return onSnapshot(q, (querySnapshot: DocumentData) => {
            if (raw) {
                setState(querySnapshot);
                return;
            }
            const itemList: {[key: string]: any} = {};

            querySnapshot.docs.forEach((doc: QueryDocumentSnapshot) => {
                const item = doc.data();
                item.id = doc.id;
                itemList[doc.id] = item;
            });
            dispatch ? dispatch(setState(itemList)) : setState(itemList);
        }, (error) => {
            console.log(`Error in snapshot ${path}`, error);
            console.log(constraints)
        });
    }
}
