import {Dispatch, SetStateAction, useContext, useEffect, useRef, useState} from "react";
import useLoading from "./useLoading";
import useAbortController from "./useAbortController";
import {RecordsResponse} from "../models/Axios";
import {AxiosResponse} from "axios";
import {useSearchParams} from "react-router-dom";
import HeaderControlsContext from "../context/headerControls-context";
import {toastError, toastSuccess} from "../services/toaster";
import {useEncodedParams} from "./useEncodedParams";
import dayjs from "dayjs";

/**
 * GUIDE TO LISTS - MOST OF THE DUA LISTS USE THESE COMPONENTS / HOOKS
 *
 * A.hook -> useListControl
 *  gives access to all the state we need for our list
 *  [*Results*, Params, Set Params method, method to force update the list]
 *
 *  we will need to fetch any extra data we need for the lookups/filters etc...
 *
 * B.Components To Use for the filters control:
 *  1.SearchBar
 *      (with any extra dropdowns as filters,
 *       the lookups for the dropdowns needs to be fetched on the main component)
 *
 *  2.Status Filter
 *  (this need as prop the method to fetch the filters )
 *
 * C.DuaList Component ->
 *   this component auto-handles pagination,
 *   makes the header for the list (with ordering)
 *   shows empty list message
 *   needs the list *Results* (with metadata and records)
 *   needs a method to change page
 *   (optional) needs order params (current order and methods)
 *
 */

type CsvHeaders = {
    label: string,
    key: string
}[]

type UseListControl = <Params extends {}, Model>(initParams: Params, method: (params: Params) => Promise<AxiosResponse<RecordsResponse<Model>, any>>, csvHeaders?: CsvHeaders) => [RecordsResponse<Model> | undefined, Params, Dispatch<SetStateAction<Params>>, () => Promise<void>, Dispatch<SetStateAction<RecordsResponse<Model> | undefined>>, any]

/***
 * Hook to be used in all lists components
 * Handles state change, showing loading, preserves state between refreshing page, can read prams from url
 * Uses the {HeaderControlsContext} to demand update or refresh from the header
 *
 * @param initParams -> the initial search parameters
 * @param method -> the method used to the fetch the list !! IMPORTANT : THE METHODS RESPONSE MUST BE OF THE TYPE RecordResponse<Model> !!
 * @param csvHeaders -> if this parameter is passed it adds functionality to download as CSV the button need to be added manually with ref as csvDownloadRef (CHECK SOME EXAMPLE FOR THE FORMAT THIS OBJECT NEED TO HAVE)
 * @TSparam1 -> the TS-model for of the search parameters
 * @TSparam1 -> the TS-model of the entity we fetch e.g. = AccountRequest
 *
 * returns an array with
 * [recordResponse        : the response from the fetch
 * params                 : the current params
 * setParams              : callback to change the params (when params are changed the list automatically refetches)
 * fetchList              : callback to demand update of the list
 * setRecordResponse      : callback to set the record response (this is needed only in very fringe cases, do not use unless you have a good reason)
 * exportList             : function that exports the list as csv or xlsx
 * ]
 */

const useListControl: UseListControl = (initParams, method, csvHeaders) => {

    var XLSX = require("xlsx");
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const [isLoading, setIsLoading] = useLoading(true)
    const [params, setParams] = useState(initParams)
    const [searchParams, setSearchParams] = useEncodedParams();
    const {demandUpdate} = useContext(HeaderControlsContext)
    const [recordResponse, setRecordResponse] = useState<RecordsResponse<any> | undefined>()
    const mounted = useRef(false);

    useAbortController()

    const downloadCsv = async () => {

        if (!csvHeaders) return

        /** fetch all the pages of the current criteria */
        const newParams: typeof params = {
            ...params,
            page_size: 99999,
            page: 1
        }

        const res = await method(newParams)

        /**construct data array based on instructions **/
        const csvData = res.data.records.map(rec => {
            return csvHeaders.map(({ label, key }) => {
                let f: any = rec;

                key.split('.').forEach((k: string) => {
                    f = f?.[k] ?? '';
                });

                return f != null ? String(f) : '';
            }).join(';');
        });

        /**prepend header **/
        csvData.unshift(csvHeaders.map(h => h.label).join(';'))

        const csvDataString = csvData.join('\n')

        const blob = new Blob(['\uFEFF' + csvDataString], {type: 'text/csv;charset=utf-8,'})
        const objUrl = URL.createObjectURL(blob)
        const link = document.createElement('a')
        link.setAttribute('href', objUrl)
        link.setAttribute('download', 'export.csv')
        link.click()


    }

    const downloadXlsx = async () => {


        const newParams: typeof params = {
            ...params,
            page_size: 9999,
            page: 1
        }

        const res = await method(newParams)

        const data2 = res.data.records.map(rec => {

            const obj: any = {}
            csvHeaders?.forEach(({label, key}) => {

                let f = rec
                // @ts-ignore
                key.split('.').forEach((k: string) => {
                    // @ts-ignore
                    f = f[k] ?? ''
                })
                obj[label] = f

            })
            return obj


        })

        const worksheet = XLSX.utils.json_to_sheet(data2);
        const workbook = XLSX.utils.book_new();


        // const max_width = data2.reduce((w, r) => Math.max(w, r['Όνοματεπώνυμο'].length), 10);


        // const max_widthOLD = data2.reduce((w, r) => Math.max(w, csvHeaders!.reduce((w2, r2) => Math.max(w2, r[r2.label].length), 10)), 10);

        const max_wch_init = new Array(csvHeaders?.length).fill({wch: 10})
        /**
         * constructing array of max length for each column for setting the widths
         */
        const max_wch = data2.reduce((max_wch, entry) => Object.values(entry).map((value: any, index) => {
            return {wch: Math.max(value.length, max_wch[index].wch)}
        }), max_wch_init);


        worksheet["!cols"] = max_wch;

        XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");

        /* fix headers */
        // XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });

        XLSX.writeFile(workbook, "export.xlsx", {compression: true});

    }

    const exportList = async (type: 'csv' | 'xlsx') => {

        if (type === 'csv') {
            toastSuccess('Προετοιμασία αρχείου (Θα κατέβει αυτόματα)')
            await downloadCsv()
        } else if (type === 'xlsx') {
            toastSuccess('Προετοιμασία αρχείου (Θα κατέβει αυτόματα)')
            await downloadXlsx()
        } else {
            console.log('unexpected type')
        }
    }

    const syncFromSearchParams = () => {
        const newParams: any = {}
        Object.entries(params).forEach(([key, value]) => {
            if (Array.isArray(value)) {
                const fromUrl = searchParams.getAll(key)
                // URLSearchParams.getAll(key) returns [''] (not []) when the
                // key is present in the query string with an empty value —
                // e.g. after syncToSearchParams previously wrote an empty
                // array out as `key=`. Treat that the same as "not present"
                // and fall back to the current/default value instead of
                // passing the empty-string marker downstream to the API.
                const isEmptyMarker = fromUrl.length === 1 && fromUrl[0] === ''
                newParams[key] = (fromUrl.length > 0 && !isEmptyMarker) ? fromUrl : value
            } else {
                const paramValue = searchParams.get(key);
                if (paramValue !== null) {
                    // ✅ coerce back to the same type as the initial param
                    if (typeof value === 'number') {
                        newParams[key] = Number(paramValue);
                    } else {
                        newParams[key] = paramValue;
                    }
                } else {
                    newParams[key] = value;
                }
            }
        })
        setParams(newParams)
    }

    const syncToSearchParams = () => {

        const newSearchParams = {...params}

        /**
         * Ι hate this shouldn't have the extra hardcoded D:
         * V2: keep ANY extra params
         */
        // @ts-ignore
        newSearchParams.activeRole = searchParams.get('activeRole')

        setSearchParams(newSearchParams)

    }

    const fetchList = async () => {

        try {

            setIsLoading(true, true)
            const res = await method(params)
            setRecordResponse(res.data)

        } catch (e: any) {

            toastError('Κάτι πήγε στραβά.')

        } finally {
            setIsLoading(false)
        }
    }

    useEffect(() => {

        /**
         * if its on mount (first render) change params from url and DO NOT fetch
         * the update of tha Params from the url with force a rerender and...
         * fetch list will happen on the next render cycle
         */
        if (!mounted.current) {
            syncFromSearchParams()
            mounted.current = true;
            return
        }

        syncToSearchParams()
        fetchList()

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [params, demandUpdate])

    return [recordResponse, params, setParams, fetchList, setRecordResponse, exportList]

}

export default useListControl
