import {Timestamp} from "firebase/firestore";
import {Address, CohortData, InstituteData, OrganisationAddress, Products, ProviderUserPermissions, StaffUserPermissions, UserData} from "../typeDefinitions";
import * as geoHash from "ngeohash";
import FirebaseQuery from "./firebaseQuery";
import {getDownloadURL, ref} from "firebase/storage";
import {storage} from "./firebaseConfig";

export const getProviderId = async () => {/*
    let user = await getDoc(doc(db, 'users', auth.currentUser.uid))
    return user.data().bi*/
};

export const getRandomNumber = (min = 0, max:number) => {
    return Math.random() * (max - min) + min;
};

export const getUniqueId = (object:object): number => {
    const id = Math.round(getRandomNumber(0, 1000000));

    if (!object) {
        return id;
    }

    if (Array.isArray(object)) {
        return object.some((el) => el.id === id) ? getUniqueId(object) : id;
    }

    return (Object.keys(object) as any[]).some((el) => el === id) ? getUniqueId(object) : id;
};

export const capitalise = (s="") => {
    const nS = s.toString().replace("_", " ");
    return nS.charAt(0).toUpperCase() + nS.slice(1);
};

export const capitaliseWords = (s="") => {
    return s.replace(/(^|\s)\S/g, (l) => l.toUpperCase());
};

export const snakeCase = (s="") => {
    const nS = s.toString().replace(" ", "_");
    return nS.toLowerCase();
};

export const validateEmail = (email: string) => {
    console.log(email);
    return email.match(
        /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
    );
};

export const getAccess = (user:UserData, ...perms: (keyof StaffUserPermissions | keyof ProviderUserPermissions)[]) => {
    // If the user has the permission given, grant access
    const finalPerms: {[key: string]: any} = {};
    perms.forEach((perm) => {
        finalPerms[perm] = (user.userGroup === "admin" || (user.groupData as object)?.[perm] || false);
    });
    // Otherwise, deny access
    return Object.keys(finalPerms).length > 1 ? finalPerms : Object.values(finalPerms)[0];
};

export const dateToString = (date:Date) => {
    const y = date.getFullYear();
    const m = ("0"+(date.getMonth() + 1)).slice(-2);
    const d = ("0"+(date.getDate())).slice(-2);
    return [y, m, d].join("-");
};

export const getPlacementDateArray = (start:string, end:string) => {
    const dates = [start];
    let currentDate = start;

    while (currentDate !== end) {
        const dateObj = new Date(currentDate);
        dateObj.setDate(dateObj.getDate()+1);
        const dateObjString = dateToString(dateObj);
        dates.push(dateObjString);
        currentDate = dateObjString;
    }

    return dates;
};

export const reformatDate = (date: Date|string) => {
    if (!date) {
        return;
    }
    return date.toString().split("-").reverse().join(".");
};

export const convertDate = (date?: string|Date, output: "dbstring"|"date"|"visual" = "date") => {
    // dbstring = yyyy-MM-dd
    // visual = dd MMM yyyy
    // date = Date
    if (!date) return;

    let year:number;
    let month:number;
    let day:number;

    const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

    if (typeof date === "string") {
        const dateType = date.includes(" ") ? "visual" : "dbstring";
        const dateArray = date.split(dateType === "visual" ? " " : "-");

        year = parseInt(dateArray[dateType === "visual" ? 2 : 0]);
        month = dateType === "visual" ? months.indexOf(dateArray[1]) : parseInt(dateArray[1]) - 1;
        day = parseInt(dateArray[dateType === "visual" ? 0 : 2]);
    } else {
        year = date.getFullYear();
        month = date.getMonth();
        day = date.getDate();
    }

    switch (output) {
    case "date":
        return new Date(year, month, day);
    case "dbstring":
        return [year, month+1 < 10 ? `0${month+1}` : month+1, day < 10 ? `0${day}` : day].join("-");
    case "visual":
        return [day, months[month], year].join(" ");
    }
};

export const getDateDiff = (start: Date, end:Date): number => {
    return (Math.round((new Date(dateToString(end)).getTime() - new Date(dateToString(start)).getTime()) / (1000 * 60 * 60 * 24)));
};

export const reformatDateTime = (timestamp: string|Timestamp) => {
    if (typeof timestamp === "string") {
        return `${timestamp.split("T")[1].substring(0, 8)} ${(timestamp.split("T")[0]).split("-").reverse().join(".")}`;
    }

    try {
        const created = new Date(timestamp.seconds * 1000).toISOString();
        return `${created.split("T")[1].substring(0, 8)} ${(created.split("T")[0]).split("-").reverse().join(".")}`;
    } catch (e) {
        return null;
    }
};

export function stringToDate(date: string) {
    const formatItems= date.split("/").map((i) => parseInt(i));
    console.log("date array", formatItems);
    return new Date(formatItems[2], formatItems[1]-1, formatItems[0]);
}

// export const editNestedObject = (loc:(string|number)[], data:any[]|{[key: string|number]: unknown}, value:any, objectId=false) => {
//     if (!data || !(Array.isArray(data) || typeof data === "object")) {
//         console.error("Error editing nested JSON. Invalid type");
//         return;
//     }

//     type IndexType = typeof data extends any[] ? number : number|string

//     const prevData: typeof data = Array.isArray(data) ? [...data] : {...data};
//     const locationIndex:IndexType = objectId && Array.isArray(data) ? (prevData as any[]).findIndex((x) => x.id === loc[0]) : loc[0];

//     if (loc.length > 1) {
//         prevData[locationIndex as any] = editNestedObject(loc.slice(1), data[locationIndex as any] || {}, value, objectId);
//     } else {
//         if (value === undefined) {
//             Array.isArray(prevData) ? prevData.splice(locationIndex as number, 1) : delete prevData[locationIndex];
//         } else {
//             prevData[locationIndex as any] = value;
//         }
//     }

//     return prevData;
// };

export const editNestedObject = (
    loc: (string | number)[], 
    data: any[] | { [key: string | number]: unknown }, 
    value: any, 
    objectId = false, 
    append = false  // New parameter to enable appending
) => {
    if (!data || !(Array.isArray(data) || typeof data === "object")) {
        console.error("Error editing nested JSON. Invalid type");
        return;
    }

    type IndexType = typeof data extends any[] ? number : number | string;
    const prevData: typeof data = Array.isArray(data) ? [...data] : { ...data };
    const locationIndex: IndexType = objectId && Array.isArray(data)
        ? (prevData as any[]).findIndex((x) => x.id === loc[0])
        : loc[0];

    if (loc.length > 1) {
        prevData[locationIndex as any] = editNestedObject(
            loc.slice(1), 
            data[locationIndex as any] || (append ? [] : {}), 
            value, 
            objectId,
            append
        );
    } else {
        if (value === undefined) {
            Array.isArray(prevData) ? prevData.splice(locationIndex as number, 1) : delete prevData[locationIndex];
        } else if (append && Array.isArray(prevData[locationIndex as any])) {
            (prevData[locationIndex as any] as any[]).push(value); // Append instead of replace
        } else {
            prevData[locationIndex as any] = value;
        }
    }

    return prevData;
};


export const getItemInNestedObject = (path:string[], object) => {
    if (path.length === 0) return;

    const item = object?.[path.shift() as string];
    if (item === undefined) return item;
    if (path.length > 0) {
        return getItemInNestedObject(path, item);
    }
    return item;
};

export const camelCaseToNormal = (string="") => {
    return string.replace(/([A-Z])/g, " $1").toLowerCase().trim().replace(/^./, function(str) {
        return str.toUpperCase();
    });
};

export const camelCase = (string="") => {
    return string.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase());
};

export const isJson = (item:any) => {
    item = typeof item !== "string" ?
        JSON.stringify(item) :
        item;

    try {
        item = JSON.parse(item);
    } catch (e) {
        return false;
    }

    return (typeof item === "object" && item !== null);
};

export const sortByReverseStringLength = (array: string[]) => {
    return array.sort(function(a, b) {
        return b.length - a.length;
    });
};

export const pathToArr = (path: any[]|any) => Array.isArray(path) ? path : [path];

export const arraysEqual = (a:any[], b:any[]) => {
    if (a === b) return true;
    if (a == null || b == null) return false;
    if (a.length !== b.length) return false;

    return a.every((val, idx) => val === b[idx]);
};


export const objectsEqual = (x:object, y:object):boolean => {
    const ok = Object.keys; const tx = typeof x; const ty = typeof y;
    return x && y && tx === "object" && tx === ty ? (
        ok(x).length === ok(y).length &&
        ok(x).every((key) => objectsEqual(x[key as keyof typeof x], y[key as keyof typeof x]))
    ) : (x === y);
};

export const arrayUniqueValues = (array:unknown[]) => {
    return array.filter((value, index, self) => self.indexOf(value) === index);
};


export const flattenObject = (obj) => {
    const flattened = {};

    Object.keys(obj).forEach((key) => {
        const value = obj[key];

        if (typeof value === "object" && value !== null && !Array.isArray(value)) {
            Object.assign(flattened, flattenObject(value));
        } else {
            flattened[key] = value;
        }
    });

    return flattened;
};


export const getGeoHash = async (data:Partial<Address>) => {
    if (!data?.["address-line1"] || !data.postal_code) return;
    const address = data?.["address-line1"] + " " + data?.["address-line2"] +
        " " + data?.locality + " " + data?.postal_code + " " + data?.country;
    address.replace(" ", "+");
    const apiQuery = "https://maps.googleapis.com/maps/api/geocode/json?address=" +
        address + "&key=AIzaSyBOSSi4iHxOoAS9tLAJUAC_46HlZ6-D5Ss";

    const location = await fetch(apiQuery)
        .then(async (res) =>
            await res.json() as {results: {geometry: {location: unknown}}[]})
        .then((res: {results: {geometry: {location: unknown}}[]}) => {
            return res;
        }, (error: Error) => {
            return error;
        });

    if (location instanceof Error || !location.results.length) {
        return;
    }

    const result = location.results[0].geometry.location as {lat:number, lng:number};

    return geoHash.encode(result.lat, result.lng, 12);
};

export const decodeGeoHash = (g: string) => {
    console.log("g", g);
    if (!g) return;
    return geoHash.decode(g);
};

export const randomPassword = () => {
    let text = "";
    const c = "ABCDEFGHIJKLMNNOPQRSTUVWXYZ" +
            "abcdefghijklmnopqrstuvwxyz0123456789";

    for (let i = 0; i < 12; i++) {
        text += c.charAt(Math.floor(Math.random() * c.length));
    }
    return text;
};

export const arrayEquals = (a:unknown, b:unknown) => {
    return Array.isArray(a) &&
        Array.isArray(b) &&
        a.length === b.length &&
        a.every((val, index) => val === b[index]);
};

export const average = (arr: number[]) => arr.reduce( ( p, c ) => p + c, 0 ) / arr.length;

export const readingTime = (text: string) => {
    if (!text) return;
    const wpm = 225;
    const words = text.trim().split(/\s+/).length;
    const time = Math.ceil(words / wpm);
    return `${time} minute${(time > 1) ? "s" : ""}`;
};
/*
export const imageToPdf = async (image: File):Promise<string> => {
    const doc = PDFDocument;

    const imgStream = await image.arrayBuffer();

    doc.image(imgStream, {
        fit: [500, 400],
        align: 'center',
        valign: 'center'
    });
     
    doc.end(); // will trigger the stream to end
    
    return await pdfToBase64(doc);
}

function pdfToBase64(doc): Promise<string> {
    return new Promise<string>((resolve, reject) => {
        // Create a buffer
        const buffers: Buffer[] = [];
        doc.on('data', (chunk: Buffer) => {
            buffers.push(chunk);
        });
        doc.on('end', () => {
            // Concatenate all the buffers to form a single buffer
            const pdfBuffer = Buffer.concat(buffers);

            // Convert the buffer to a base64 string
            const base64String = pdfBuffer.toString('base64');

            resolve(base64String);
        });

        // Handle errors
        doc.on('error', (error: Error) => {
            reject(error);
        });

        // End the document
        doc.end();
    });
}
*/

export function getMaximumStudentsFromListingCohorts(cohorts?: {[key: string]: {
    startDate?: string,
    studentPlacements?: string[]
}}): number {
    const today = new Date();
    const threeMonthsAgo = new Date();
    threeMonthsAgo.setMonth(today.getMonth() - 3);

    if (!cohorts) return 0;

    // Convert object values to an array
    const cohortArray = Object.values(cohorts);

    // Filter out cohorts within the last 3 months or in the future
    const validCohorts = cohortArray.filter((cohort) =>
        cohort.startDate ? new Date(cohort.startDate) < threeMonthsAgo : false
    );

    if (validCohorts.length > 0) {
        // Sort by startDate descending (most recent first)
        validCohorts.sort((a, b) => new Date(b.startDate!)
            .getTime() - new Date(a.startDate!).getTime());

        return validCohorts[0].studentPlacements?.length || 0;
    }
    return Math.max(...cohortArray.map((c) =>
        c.studentPlacements?.length || 0), 0);
}


export const getImageColorInstituteForEmail = async (product: Products,
    oId: string, cohort?: string, schoolId?: string):Promise<
    {image?: string, color?: string, name?: string, email?: string,
        package?: string}> => {
        const firebaseQuery = new FirebaseQuery();
    if (product !== "institutes") return {};
    const oIdRef = ref(storage, `${product}/${oId}/profilePic.png`);
    const oIdImage = await getDownloadURL(oIdRef).catch(() => undefined);
    const institute = await firebaseQuery.getDocData(["institutes", oId]) as InstituteData;

    if (!cohort && !schoolId) {
        const adminUser = await firebaseQuery.getDocData(["users",
            institute.admin]) as UserData;

        return {
            image: oIdImage,
            name: institute.name,
            email: adminUser.email,
            color: institute.color,
            package: institute.package,
        };
    }

    const cohortData = cohort ? (await firebaseQuery.getDocData(["cohorts",
        cohort]) as CohortData) : undefined;

    const mSchoolId = schoolId || cohortData?.schoolId;
    const adminUser = await firebaseQuery.getDocData(["users",
        cohortData?.designatedStaff || institute.admin]) as UserData;

    if (!mSchoolId) {
        return {
            image: oIdImage,
            name: institute.name,
            color: institute.color,
            email: adminUser.email,
        };
    }

    const schoolData = await firebaseQuery.getDocData(["schools",
        mSchoolId]) as OrganisationAddress;

    const instituteName = schoolData.name ||institute.name;

    const cohortRef = ref(storage, `${product}/${oId}/${mSchoolId}.png`);
    const cohortImage = await getDownloadURL(cohortRef).catch(() => undefined);

    return {
        image: cohortImage || oIdImage,
        color: schoolData.color,
        name: instituteName,
        email: adminUser.email,
    };
};


export const quoteAlgoliaFilterIfNeeded = (v: any) => /\s|:|"/.test(v) ? `'${v.replace(/'/g, "\\'")}'` : v;

