import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils";
import { ClientData, Restaurants, DayMenu, DepartureTime, DayData, WeekMenu, LocationKey } from "../../types";
import getStorage from "./storage";
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku, getMenuZastavkaUmichala } from "./restaurants";
import { getTodayMock } from "./mock";

const storage = getStorage();
const MENU_PREFIX = 'menu';

/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */
export function getToday(): Date {
    if (process.env.MOCK_DATA === 'true') {
        return getTodayMock();
    }
    return new Date();
}

/** Vrátí datum v aktuálním týdnu na základě předaného indexu (0 = pondělí). */
export const getDateForWeekIndex = (index: number) => {
    if (index < 0 || index > 4) {
        // Nechceme shodit server, vrátíme dnešek
        console.log('Neplatný index dne v týdnu: ' + index);
        return getToday();
    }
    const date = getToday();
    date.setDate(date.getDate() - getDayOfWeekIndex(date) + index);
    return date;
}

/** Vrátí "prázdná" (implicitní) data pro předaný den. */
function getEmptyData(date?: Date): ClientData {
    const usedDate = date || getToday();
    return {
        date: getHumanDate(usedDate),
        isWeekend: getIsWeekend(usedDate),
        weekIndex: getDayOfWeekIndex(usedDate),
        choices: {},
    };
}

/**
 * Přidá k datům "dopočítaná" data, která nejsou přímo uložena v databázi.
 * 
 * @param data data z databáze
 * @returns obohacená data
 */
export async function addVolatileData(data: ClientData): Promise<ClientData> {
    data.todayWeekIndex = getDayOfWeekIndex(getToday());
    return data;
}

/**
 * Vrátí veškerá klientská data pro předaný den, nebo aktuální den, pokud není předán.
 */
export async function getData(date?: Date): Promise<ClientData> {
    const targetDate = date ?? getToday();
    const dateString = formatDate(targetDate);
    const data: DayData = await storage.getData(dateString) || getEmptyData(date);
    let clientData: ClientData = { ...data };
    clientData.menus = {
        [Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, date),
        // [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, date),
        [Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, date),
        [Restaurants.ZASTAVKAUMICHALA]: await getRestaurantMenu(Restaurants.ZASTAVKAUMICHALA, date),
    }
    clientData = await addVolatileData(clientData);
    return clientData;
}

/**
 * Vrátí klíč, pod kterým je uloženo menu pro předané datum.
 *
 * @param date datum
 * @returns databázový klíč
 */
function getMenuKey(date: Date) {
    const weekNumber = getWeekNumber(date);
    return `${MENU_PREFIX}_${date.getFullYear()}_${weekNumber}`;
}

/**
 * Vrátí menu restaurací pro předané datum, pokud již existují.
 * 
 * @param date datum
 * @returns menu restaurací pro předané datum
 */
async function getMenu(date: Date): Promise<WeekMenu | undefined> {
    return await storage.getData(getMenuKey(date));
}

// TODO přesun do restaurants.ts
/**
 * Vrátí menu dané restaurace pro předaný den.
 * Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB.
 * 
 * @param restaurant restaurace
 * @param date datum, ke kterému získat menu
 * @param mock příznak, zda chceme pouze mock data
 */
export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): Promise<DayMenu> {
    const usedDate = date ?? getToday();
    const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
    const now = new Date().getTime();
    if (getIsWeekend(usedDate)) {
        return {
            lastUpdate: now,
            closed: true,
            food: [],
        };
    }

    let menus = await getMenu(usedDate);
    if (menus == null) {
        menus = [];
    }
    for (let i = 0; i < 5; i++) {
        if (menus[i] == null) {
            menus[i] = {};
        }
        if (menus[i][restaurant] == null) {
            menus[i][restaurant] = {
                lastUpdate: now,
                closed: false,
                food: [],
            };
        }
    }
    if (!menus[dayOfWeekIndex][restaurant]?.food?.length) {
        const firstDay = getFirstWorkDayOfWeek(usedDate);
        const mock = process.env.MOCK_DATA === 'true';
        switch (restaurant) {
            case Restaurants.SLADOVNICKA:
                try {
                    const sladovnickaFood = await getMenuSladovnicka(firstDay, mock);
                    for (let i = 0; i < sladovnickaFood.length; i++) {
                        menus[i][restaurant]!.food = sladovnickaFood[i];
                        // Velice chatrný a nespolehlivý způsob detekce uzavření...
                        if (sladovnickaFood[i].length === 1 && sladovnickaFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
                            menus[i][restaurant]!.closed = true;
                        }
                    }
                } catch (e: any) {
                    console.error("Selhalo načtení jídel pro podnik Sladovnická", e);
                }
                break;
            // case Restaurants.UMOTLIKU:
            //     try {
            //         const uMotlikuFood = await getMenuUMotliku(firstDay, mock);
            //         for (let i = 0; i < uMotlikuFood.length; i++) {
            //             menus[i][restaurant]!.food = uMotlikuFood[i];
            //             if (uMotlikuFood[i].length === 1 && uMotlikuFood[i][0].name.toLowerCase() === 'zavřeno') {
            //                 menus[i][restaurant]!.closed = true;
            //             }
            //         }
            //     } catch (e: any) {
            //         console.error("Selhalo načtení jídel pro podnik U Motlíků", e);
            //     }
            //     break;
            case Restaurants.TECHTOWER:
                try {
                    const techTowerFood = await getMenuTechTower(firstDay, mock);
                    for (let i = 0; i < techTowerFood.length; i++) {
                        menus[i][restaurant]!.food = techTowerFood[i];
                        if (techTowerFood[i]?.length === 1 && techTowerFood[i][0].name.toLowerCase() === 'svátek') {
                            menus[i][restaurant]!.closed = true;
                        }
                    }
                    break;
                } catch (e: any) {
                    console.error("Selhalo načtení jídel pro podnik TechTower", e);
                }
            case Restaurants.ZASTAVKAUMICHALA:
                try {
                    const zastavkaUmichalaFood = await getMenuZastavkaUmichala(firstDay, mock);
                    for (let i = 0; i < zastavkaUmichalaFood.length; i++) {
                        menus[i][restaurant]!.food = zastavkaUmichalaFood[i];
                        if (zastavkaUmichalaFood[i]?.length === 1 && zastavkaUmichalaFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
                            menus[i][restaurant]!.closed = true;
                        }
                    }
                    break;
                } catch (e: any) {
                    console.error("Selhalo načtení jídel pro podnik Zastávka u Michala", e);
                }
        }
        await storage.setData(getMenuKey(usedDate), menus);
    }
    return menus[dayOfWeekIndex][restaurant]!;
}

/**
 * Inicializuje výchozí data pro předané datum, nebo dnešek, pokud není datum předáno.
 * 
 * @param date datum
 */
export async function initIfNeeded(date?: Date) {
    const usedDate = formatDate(date ?? getToday());
    const hasData = await storage.hasData(usedDate);
    if (!hasData) {
        await storage.setData(usedDate, getEmptyData(date || getToday()));
    }
}

/**
 * Odstraní kompletně volbu uživatele (včetně případných podřízených jídel).
 * 
 * @param login login uživatele
 * @param trusted příznak, zda se jedná o ověřeného uživatele
 * @param locationKey vybrané "umístění"
 * @param date datum, ke kterému se volba vztahuje
 * @returns 
 */
export async function removeChoices(login: string, trusted: boolean, locationKey: LocationKey, date?: Date) {
    const selectedDay = formatDate(date ?? getToday());
    let data: DayData = await storage.getData(selectedDay);
    validateTrusted(data, login, trusted);
    if (locationKey in data.choices) {
        if (data.choices[locationKey] && login in data.choices[locationKey]) {
            delete data.choices[locationKey][login]
            if (Object.keys(data.choices[locationKey]).length === 0) {
                delete data.choices[locationKey]
            }
            await storage.setData(selectedDay, data);
        }
    }
    return data;
}

/**
 * Odstraní konkrétní volbu jídla uživatele.
 * Neodstraňuje volbu samotnou, k tomu slouží {@link removeChoices}.
 * 
 * @param login login uživatele
 * @param trusted příznak, zda se jedná o ověřeného uživatele
 * @param locationKey vybrané "umístění"
 * @param foodIndex index jídla v jídelním lístku daného umístění, pokud existuje
 * @param date datum, ke kterému se volba vztahuje
 * @returns 
 */
export async function removeChoice(login: string, trusted: boolean, locationKey: LocationKey, foodIndex: number, date?: Date) {
    const selectedDay = formatDate(date ?? getToday());
    let data: DayData = await storage.getData(selectedDay);
    validateTrusted(data, login, trusted);
    if (locationKey in data.choices) {
        if (data.choices[locationKey] && login in data.choices[locationKey]) {
            const index = data.choices[locationKey][login].options.indexOf(foodIndex);
            if (index > -1) {
                data.choices[locationKey][login].options.splice(index, 1)
                await storage.setData(selectedDay, data);
            }
        }
    }
    return data;
}

/**
 * Odstraní kompletně volbu uživatele.
 * 
 * @param login login uživatele
 */
async function removeChoiceIfPresent(login: string, date: string) {
    let data: DayData = await storage.getData(date);
    for (const key of Object.keys(data.choices)) {
        const locationKey = key as LocationKey;
        if (data.choices[locationKey] && login in data.choices[locationKey]) {
            delete data.choices[locationKey][login];
            if (Object.keys(data.choices[locationKey]).length === 0) {
                delete data.choices[locationKey];
            }
            await storage.setData(date, data);
        }
    }
    return data;
}

/**
 * Ověří, zda se neověřený uživatel nepokouší přepsat údaje ověřeného a případně vyhodí chybu.
 * 
 * @param data aktuální klientská data
 * @param login přihlašovací jméno uživatele
 * @param trusted příznak, zda se jedná o ověřeného uživatele
 */
function validateTrusted(data: ClientData, login: string, trusted: boolean) {
    const locations = Object.values(data?.choices);
    let found = false;
    if (!trusted) {
        for (const location of locations) {
            if (Object.keys(location).includes(login) && location[login].trusted) {
                found = true;
            }
        }
    }
    if (!trusted && found) {
        throw new InsufficientPermissions("Nelze změnit volbu ověřeného uživatele");
    }
}

/**
 * Přidá volbu uživatele.
 * 
 * @param login login uživatele
 * @param trusted příznak, zda se jedná o ověřeného uživatele
 * @param locationKey vybrané "umístění"
 * @param foodIndex volitelný index jídla v daném umístění
 * @param trusted příznak, zda se jedná o ověřeného uživatele
 * @param date datum, ke kterému se volba vztahuje
 * @returns aktuální data
 */
export async function addChoice(login: string, trusted: boolean, locationKey: LocationKey, foodIndex?: number, date?: Date) {
    const usedDate = date ?? getToday();
    await initIfNeeded(usedDate);
    const selectedDate = formatDate(usedDate);
    let data: DayData = await storage.getData(selectedDate);
    validateTrusted(data, login, trusted);
    // Pokud měníme pouze lokaci, mažeme případné předchozí
    if (foodIndex == null) {
        data = await removeChoiceIfPresent(login, selectedDate);
    }
    // TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce
    if (!(data.choices[locationKey])) {
        data.choices[locationKey] = {}
    }
    if (!(login in data.choices[locationKey])) {
        if (!data.choices[locationKey]) {
            data.choices[locationKey] = {}
        }
        data.choices[locationKey][login] = {
            trusted,
            options: []
        };
    }
    if (foodIndex != null && !data.choices[locationKey][login].options.includes(foodIndex)) {
        data.choices[locationKey][login].options.push(foodIndex);
    }
    await storage.setData(selectedDate, data);
    return data;
}

/**
 * Aktualizuje poznámku k aktuálně vybrané možnosti.
 * 
 * @param login login uživatele
 * @param trusted příznak, zda se jedná o ověřeného uživatele
 * @param note poznámka
 * @param date datum, ke kterému se volba vztahuje
 */
export async function updateNote(login: string, trusted: boolean, note?: string, date?: Date) {
    const usedDate = date ?? getToday();
    await initIfNeeded(usedDate);
    const selectedDate = formatDate(usedDate);
    let data: DayData = await storage.getData(selectedDate);
    validateTrusted(data, login, trusted);
    const userEntry = data.choices != null && Object.entries(data.choices).find(entry => entry[1][login] != null);
    if (userEntry) {
        if (!note || !note.length) {
            delete userEntry[1][login].note;
        } else {
            userEntry[1][login].note = note;
        }
        await storage.setData(selectedDate, data);
    }
    return data;
}

/**
 * Aktualizuje preferovaný čas odchodu strávníka.
 * 
 * @param login login uživatele
 * @param time preferovaný čas odchodu
 * @param date datum, ke kterému se čas vztahuje
 */
export async function updateDepartureTime(login: string, time?: string, date?: Date) {
    const selectedDate = formatDate(date ?? getToday());
    let clientData: DayData = await storage.getData(selectedDate);
    const found = Object.values(clientData.choices).find(location => login in location);
    // TODO validace, že se jedná o restauraci
    if (found) {
        if (!time?.length) {
            delete found[login].departureTime;
        } else {
            if (!Object.values<string>(DepartureTime).includes(time)) {
                throw Error(`Neplatný čas odchodu ${time}`);
            }
            found[login].departureTime = time;
        }
        await storage.setData(selectedDate, clientData);
    }
    return clientData;
}