import { InsufficientPermissions, PizzaDayConflictError, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils";
import getStorage from "./storage";
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova, StaleWeekError } from "./restaurants";
import { getTodayMock } from "./mock";
import { removeAllUserPizzas } from "./pizza";
import { getStores } from "./stores";
import { ClientData, DepartureTime, LunchChoice, MealSlot, PizzaDayState, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen";

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

function getDataKey(date: Date, slot?: MealSlot): string {
    const base = formatDate(date);
    return slot === MealSlot.EXTRA ? `${base}_extra` : base;
}

/** 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. */
export function getEmptyData(date?: Date): ClientData {
    const usedDate = date || getToday();
    return {
        todayDayIndex: getDayOfWeekIndex(getToday()),
        date: getHumanDate(usedDate),
        isWeekend: getIsWeekend(usedDate),
        dayIndex: getDayOfWeekIndex(usedDate),
        choices: {},
    };
}

/**
 * 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, slot?: MealSlot): Promise<ClientData> {
    const clientData = await getClientData(date, slot);
    if (slot === MealSlot.EXTRA) {
        clientData.stores = await getStores();
    } else {
        clientData.menus = {
            SLADOVNICKA: await getRestaurantMenu('SLADOVNICKA', date),
            // UMOTLIKU: await getRestaurantMenu('UMOTLIKU', date),
            TECHTOWER: await getRestaurantMenu('TECHTOWER', date),
            ZASTAVKAUMICHALA: await getRestaurantMenu('ZASTAVKAUMICHALA', date),
            SENKSERIKOVA: await getRestaurantMenu('SENKSERIKOVA', date),
        }
    }
    return clientData;
}

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

/**
 * Vrátí menu všech podniků pro celý týden do kterého spadá předané datum, pokud již existují.
 * 
 * @param date datum
 * @returns menu restaurací pro týden příslušící předanému datu
 */
async function getMenu(date: Date): Promise<WeekMenu | undefined> {
    return await storage.getData<WeekMenu | undefined>(getMenuKey(date));
}

// TODO přesun do restaurants.ts
/**
 * Načte menu dané restaurace pro celý týden bez ukládání do storage.
 * Používá se pro validaci dat před uložením.
 * 
 * @param restaurant restaurace
 * @param firstDay první pracovní den týdne
 * @returns pole menu pro jednotlivé dny týdne
 */
export async function fetchRestaurantWeekMenuData(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
    return await fetchRestaurantWeekMenu(restaurant, firstDay);
}

/**
 * Uloží týdenní menu restaurace do storage.
 * 
 * @param restaurant restaurace
 * @param date datum z týdne, pro který ukládat menu
 * @param weekData data týdenního menu
 */
export async function saveRestaurantWeekMenu(restaurant: Restaurant, date: Date, weekData: any[]): Promise<void> {
    const now = new Date().getTime();
    let weekMenu = await getMenu(date);
    weekMenu ??= [{}, {}, {}, {}, {}];

    // Inicializace struktury pro restauraci
    for (let i = 0; i < 5; i++) {
        weekMenu[i] ??= {};
        weekMenu[i][restaurant] ??= {
            lastUpdate: now,
            closed: false,
            food: [],
        };
    }

    // Uložení dat pro všechny dny
    for (let i = 0; i < weekData.length && i < weekMenu.length; i++) {
        weekMenu[i][restaurant]!.food = weekData[i];
        weekMenu[i][restaurant]!.lastUpdate = now;

        // Detekce uzavření pro každou restauraci
        switch (restaurant) {
            case 'SLADOVNICKA':
                if (weekData[i].length === 1 && weekData[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
                    weekMenu[i][restaurant]!.closed = true;
                }
                break;
            case 'TECHTOWER':
                if (weekData[i]?.length === 1 && weekData[i][0].name.toLowerCase() === 'svátek') {
                    weekMenu[i][restaurant]!.closed = true;
                }
                break;
            case 'ZASTAVKAUMICHALA':
                if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
                    weekMenu[i][restaurant]!.closed = true;
                }
                break;
            case 'SENKSERIKOVA':
                if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den nebylo zadáno menu.') {
                    weekMenu[i][restaurant]!.closed = true;
                }
                break;
        }
    }

    // Uložení do storage
    await storage.setData(getMenuKey(date), weekMenu);
}

/**
 * Načte menu dané restaurace pro celý týden bez ukládání do storage.
 * 
 * @param restaurant restaurace
 * @param firstDay první pracovní den týdne
 * @returns pole menu pro jednotlivé dny týdne
 */
async function fetchRestaurantWeekMenu(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
    const mock = process.env.MOCK_DATA === 'true';

    switch (restaurant) {
        case 'SLADOVNICKA':
            return await getMenuSladovnicka(firstDay, mock);
        case 'TECHTOWER':
            return await getMenuTechTower(firstDay, mock);
        case 'ZASTAVKAUMICHALA':
            return await getMenuZastavkaUmichala(firstDay, mock);
        case 'SENKSERIKOVA':
            return await getMenuSenkSerikova(firstDay, mock);
        default:
            throw new Error(`Nepodporovaná restaurace: ${restaurant}`);
    }
}

/**
 * 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 forceRefresh příznak vynuceného obnovení
 */
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, forceRefresh = false): Promise<RestaurantDayMenu> {
    const usedDate = date ?? getToday();
    const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
    const now = new Date().getTime();
    if (getIsWeekend(usedDate)) {
        return {
            lastUpdate: now,
            closed: true,
            food: [],
        };
    }

    let weekMenu = await getMenu(usedDate);
    weekMenu ??= [{}, {}, {}, {}, {}];
    for (let i = 0; i < 5; i++) {
        weekMenu[i] ??= {};
        weekMenu[i][restaurant] ??= {
            lastUpdate: now,
            closed: false,
            food: [],
        };
    }
    const MENU_REFETCH_TTL_MS = 60 * 60 * 1000; // 1 hour
    const existingMenu = weekMenu[dayOfWeekIndex][restaurant];
    const lastFetchExpired = !existingMenu?.lastUpdate ||
        existingMenu.lastUpdate === now || // freshly initialized, never fetched
        (now - existingMenu.lastUpdate) > MENU_REFETCH_TTL_MS;
    const shouldFetch = forceRefresh ||
        (!existingMenu?.food?.length && !existingMenu?.closed && lastFetchExpired);
    if (shouldFetch) {
        const firstDay = getFirstWorkDayOfWeek(usedDate);

        try {
            const restaurantWeekFood = await fetchRestaurantWeekMenu(restaurant, firstDay);

            // Aktualizace menu pro všechny dny
            for (let i = 0; i < restaurantWeekFood.length && i < weekMenu.length; i++) {
                weekMenu[i][restaurant]!.food = restaurantWeekFood[i];
                weekMenu[i][restaurant]!.lastUpdate = now;
                weekMenu[i][restaurant]!.isStale = false;

                // Detekce uzavření pro každou restauraci
                switch (restaurant) {
                    case 'SLADOVNICKA':
                        if (restaurantWeekFood[i].length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
                            weekMenu[i][restaurant]!.closed = true;
                        }
                        break;
                    case 'TECHTOWER':
                        if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'svátek') {
                            weekMenu[i][restaurant]!.closed = true;
                        }
                        break;
                    case 'ZASTAVKAUMICHALA':
                        if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
                            weekMenu[i][restaurant]!.closed = true;
                        }
                        break;
                    case 'SENKSERIKOVA':
                        if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den nebylo zadáno menu.') {
                            weekMenu[i][restaurant]!.closed = true;
                        }
                        break;
                }
            }

            // Uložení do storage
            await storage.setData(getMenuKey(usedDate), weekMenu);
        } catch (e: any) {
            if (e instanceof StaleWeekError) {
                for (let i = 0; i < e.food.length && i < weekMenu.length; i++) {
                    weekMenu[i][restaurant]!.food = e.food[i];
                    weekMenu[i][restaurant]!.lastUpdate = now;
                    weekMenu[i][restaurant]!.isStale = true;
                }
                await storage.setData(getMenuKey(usedDate), weekMenu);
            } else {
                console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e);
            }
        }
    }
    const result = weekMenu[dayOfWeekIndex][restaurant]!;
    result.warnings = generateMenuWarnings(result);
    return result;
}

/**
 * Generuje varování o kvalitě/úplnosti dat menu restaurace.
 */
function generateMenuWarnings(menu: RestaurantDayMenu): string[] {
    const warnings: string[] = [];
    if (!menu.food?.length || menu.closed) {
        return warnings;
    }
    if (menu.isStale) {
        warnings.push('Data jsou z minulého týdne');
    }
    const hasSoup = menu.food.some(f => f.isSoup);
    if (!hasSoup) {
        warnings.push('Chybí polévka');
    }
    const missingPrice = menu.food.some(f => !f.isSoup && (!f.price || f.price.trim() === ''));
    if (missingPrice) {
        warnings.push('U některých jídel chybí cena');
    }
    return warnings;
}

/**
 * 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, slot?: MealSlot) {
    const usedDate = getDataKey(date ?? getToday(), slot);
    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: LunchChoice, date?: Date, slot?: MealSlot) {
    const selectedDay = getDataKey(date ?? getToday(), slot);
    let data = await getClientData(date, slot);
    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: LunchChoice, foodIndex: number, date?: Date, slot?: MealSlot) {
    const selectedDay = getDataKey(date ?? getToday(), slot);
    let data = await getClientData(date, slot);
    validateTrusted(data, login, trusted);
    if (locationKey in data.choices) {
        if (data.choices[locationKey] && login in data.choices[locationKey]) {
            const index = data.choices[locationKey][login].selectedFoods?.indexOf(foodIndex);
            if (index != null && index > -1) {
                data.choices[locationKey][login].selectedFoods?.splice(index, 1);
                await storage.setData(selectedDay, data);
            }
        }
    }
    return data;
}

/**
 * Odstraní kompletně volbu uživatele, vyjma ignoredLocationKey (pokud byla předána a existuje).
 * 
 * @param login login uživatele
 * @param date datum, ke kterému se volby vztahují
 * @param ignoredLocationKey volba, která nebude odstraněna, pokud existuje
 */
async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocationKey?: LunchChoice, slot?: MealSlot) {
    const usedDate = date ?? getToday();
    let data = await getClientData(usedDate, slot);
    for (const key of Object.keys(data.choices)) {
        const locationKey = key as LunchChoice;
        if (ignoredLocationKey != null && ignoredLocationKey == locationKey) {
            continue;
        }
        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(getDataKey(usedDate, slot), 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: LunchChoice, foodIndex?: number, date?: Date, slot?: MealSlot) {
    const usedDate = date ?? getToday();
    await initIfNeeded(usedDate, slot);
    let data = await getClientData(usedDate, slot);
    validateTrusted(data, login, trusted);
    await validateFoodIndex(locationKey, foodIndex, date);

    if (!slot || slot === MealSlot.OBED) {
        // Pokud uživatel měl vybranou PIZZA a mění na něco jiného
        const hadPizzaChoice = data.choices.PIZZA && login in data.choices.PIZZA;
        if (hadPizzaChoice && locationKey !== LunchChoice.PIZZA && foodIndex == null) {
            // Kontrola, zda existuje Pizza day a uživatel je jeho zakladatel
            if (data.pizzaDay && data.pizzaDay.creator === login) {
                // Pokud Pizza day není ve stavu CREATED, nelze změnit volbu
                if (data.pizzaDay.state !== PizzaDayState.CREATED) {
                    throw new PizzaDayConflictError(
                        `Nelze změnit volbu. Pizza day je ve stavu "${data.pizzaDay.state}" a musí být nejprve dokončen nebo smazán.`
                    );
                }
                // Pizza day je ve stavu CREATED - bude smazán frontendem po potvrzení uživatelem
                // (frontend volá nejprve deletePizzaDay, pak teprve addChoice)
            }

            // Smažeme pizzy uživatele (pokud Pizza day nebyl založen tímto uživatelem,
            // nebo byl již smazán frontendem)
            await removeAllUserPizzas(login, usedDate);
            // Znovu načteme data, protože removeAllUserPizzas je upravila
            data = await getClientData(usedDate, slot);
        }
    }

    // Pokud měníme pouze lokaci, mažeme případné předchozí
    if (foodIndex == null) {
        data = await removeChoiceIfPresent(login, usedDate, undefined, slot);
    } else {
        // Mažeme případné ostatní volby (měla by být maximálně jedna)
        data = await removeChoiceIfPresent(login, usedDate, locationKey, slot);
    }
    // TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce
    data.choices[locationKey] ??= {};
    if (!(login in data.choices[locationKey])) {
        if (!data.choices[locationKey]) {
            data.choices[locationKey] = {}
        }
        data.choices[locationKey][login] = {
            trusted,
            selectedFoods: []
        };
    }
    if (foodIndex != null && !data.choices[locationKey][login].selectedFoods?.includes(foodIndex)) {
        data.choices[locationKey][login].selectedFoods?.push(foodIndex);
    }
    await storage.setData(getDataKey(usedDate, slot), data);
    return data;
}

/**
 * Zvaliduje platnost indexu jídla pro vybranou lokalitu a datum.
 * 
 * @param locationKey vybraná lokalita
 * @param foodIndex index jídla pro danou lokalitu
 * @param date datum, pro které je validace prováděna
 */
async function validateFoodIndex(locationKey: LunchChoice, foodIndex?: number, date?: Date) {
    if (foodIndex != null) {
        if (typeof foodIndex !== 'number') {
            throw Error(`Neplatný index ${foodIndex} typu ${typeof foodIndex}`);
        }
        if (foodIndex < 0) {
            throw Error(`Neplatný index ${foodIndex}`);
        }
        if (!Object.keys(Restaurant).includes(locationKey)) {
            throw Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey} nepodporující indexy`);
        }
        const usedDate = date ?? getToday();
        const menu = await getRestaurantMenu(locationKey as Restaurant, usedDate);
        if (menu.food?.length && foodIndex > (menu.food.length - 1)) {
            throw new Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey}`);
        }
    }
}

/**
 * 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, slot?: MealSlot) {
    const usedDate = date ?? getToday();
    await initIfNeeded(usedDate, slot);
    let data = await getClientData(usedDate, slot);
    validateTrusted(data, login, trusted);
    const userEntry = data.choices != null && Object.entries(data.choices).find(entry => entry[1][login] != null);
    if (userEntry) {
        if (!note?.length) {
            delete userEntry[1][login].note;
        } else {
            userEntry[1][login].note = note;
        }
        await storage.setData(getDataKey(usedDate, slot), 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 usedDate = date ?? getToday();
    let clientData = await getClientData(usedDate);
    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(getDataKey(usedDate), clientData);
    }
    return clientData;
}

/**
 * Nastaví/odnastaví uživatele jako objednatele pro dnešní den.
 * Objednatelů může být více.
 * 
 * @param login přihlašovací jméno uživatele
 */
export async function updateBuyer(login: string, slot?: MealSlot) {
    const usedDate = getToday();
    let clientData = await getClientData(usedDate, slot);
    const userEntry = clientData.choices?.['OBJEDNAVAM']?.[login];
    if (!userEntry) {
        throw new Error("Nelze nastavit objednatele pro uživatele s jinou volbou než \"Budu objednávat\"");
    }
    userEntry.isBuyer = !(userEntry.isBuyer || false);
    await storage.setData(getDataKey(usedDate, slot), clientData);
    return clientData;
}

/**
 * Vrátí data pro klienta pro předaný nebo aktuální den.
 * 
 * @param date datum pro který vrátit data, pokud není vyplněno, je použit dnešní den
 * @returns data pro klienta
 */
export async function getClientData(date?: Date, slot?: MealSlot): Promise<ClientData> {
    const targetDate = date ?? getToday();
    const dateString = getDataKey(targetDate, slot);
    const clientData = await storage.getData<ClientData>(dateString) || getEmptyData(date);
    return {
        ...clientData,
        todayDayIndex: getDayOfWeekIndex(getToday()),
        ...(slot === MealSlot.EXTRA ? { slot: MealSlot.EXTRA } : {}),
    }
}