import React, {ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react';
import {BookContext} from '@/context/BookContext';
import {ChapterProps} from '@/lib/types/chapter';
import {ChapterContext} from '@/context/ChapterContext';
import {EditorContext} from '@/context/EditorContext';
import {Editor, useEditor} from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import {AlertContext, AlertContextProps, AlertProvider} from '@/context/AlertContext';
import {usePathname} from '@/lib/navigation';
import {apiPost} from '@/lib/api/client';
import {getCookie} from '@/lib/utils/cookies';
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {BookProps} from '@/lib/types/book';
import ScribeControllerBar from '@/components/layout/ScribeControllerBar';
import ScribeLeftBar from '@/components/leftbar/ScribeLeftBar';
import ComposerRightBar from '@/components/rightbar/ComposerRightBar';
import ScribeFooterBar from '@/components/layout/ScribeFooterBar';
import GuideTour from '@/components/GuideTour';
import TermsOfUse from '@/components/TermsOfUse';
import {useTranslations, changeLanguage} from '@/lib/i18n';
import {isSupportedLocale, LangContext, LangContextProps, SupportedLocale} from '@/context/LangContext';
import {ThemeContext} from '@/context/ThemeContext';
import useTheme from '@/hooks/useTheme';
import {AIUsageContext} from '@/context/AIUsageContext';
import {BookSyncCompare, compareBookSyncs, SyncedBook} from '@/lib/types/synced-book';
import {SeriesSyncCompare, compareSeriesSyncs, SyncedSeries} from '@/lib/types/synced-series';
import {BooksSyncContext} from '@/context/BooksSyncContext';
import {SeriesSyncContext} from '@/context/SeriesSyncContext';
import {LocalSyncQueueContext, LocalSyncOperation} from '@/context/SyncQueueContext';
import useAuthentication from '@/hooks/useAuthentication';
import useOnboarding from '@/hooks/useOnboarding';
import {SettingBookContext} from '@/context/SettingBookContext';
import PulseLoader from '@/components/ui/PulseLoader';
import {initCrashReporter} from '@/lib/crashReporter';
import OfflineProvider from '@/context/OfflineProvider';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import OfflinePinSetup from '@/components/offline/OfflinePinSetup';
import OfflinePinVerify from '@/components/offline/OfflinePinVerify';
import MigrationModal from '@/components/migration/MigrationModal';
import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri';
import useSyncBooks from '@/hooks/useSyncBooks';
import useSyncSeries from '@/hooks/useSyncSeries';

function AutoSyncOnReconnect() {
    const {offlineMode}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
    const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
    const {syncAllToServer: syncAllBooksToServer, syncAllFromServer: syncAllBooksFromServer, refreshBooks, booksToSyncToServer, booksToSyncFromServer} = useSyncBooks();
    const {syncAllToServer: syncAllSeriesToServer, syncAllFromServer: syncAllSeriesFromServer, refreshSeries, seriesToSyncToServer, seriesToSyncFromServer} = useSyncSeries();
    const isSyncingRef = useRef<boolean>(false);
    const hasRefreshedRef = useRef<boolean>(false);

    const saveLastOnlineTimestamp = useCallback((): void => {
        const timestamp: number = Math.floor(Date.now() / 1000);
        localStorage.setItem('lastOnlineTimestamp', timestamp.toString());
    }, []);

    // Refresh sync data when online + authenticated + DB ready
    useEffect((): void => {
        if (!offlineMode.isOffline && session.isConnected && offlineMode.isDatabaseInitialized) {
            hasRefreshedRef.current = true;
            Promise.all([refreshBooks(), refreshSeries()]);
        }
    }, [offlineMode.isOffline, session.isConnected, offlineMode.isDatabaseInitialized]);

    // Auto-sync when diffs become available
    useEffect((): void => {
        if (offlineMode.isOffline || !session.isConnected || isSyncingRef.current || !hasRefreshedRef.current) return;

        const syncPromises: Promise<void>[] = [];

        if (booksToSyncToServer.length > 0) syncPromises.push(syncAllBooksToServer());
        if (booksToSyncFromServer.length > 0) syncPromises.push(syncAllBooksFromServer());
        if (seriesToSyncToServer.length > 0) syncPromises.push(syncAllSeriesToServer());
        if (seriesToSyncFromServer.length > 0) syncPromises.push(syncAllSeriesFromServer());

        if (syncPromises.length > 0) {
            isSyncingRef.current = true;
            Promise.all(syncPromises).then((): void => {
                saveLastOnlineTimestamp();
                isSyncingRef.current = false;
            }).catch((): void => {
                isSyncingRef.current = false;
            });
        }
    }, [booksToSyncToServer, booksToSyncFromServer, seriesToSyncToServer, seriesToSyncFromServer]);

    // Update lastOnlineTimestamp every 5 minutes while online
    useEffect((): (() => void) | void => {
        if (!offlineMode.isOffline && session.isConnected) {
            const intervalId: ReturnType<typeof setInterval> = setInterval((): void => {
                saveLastOnlineTimestamp();
            }, 5 * 60 * 1000);
            return (): void => clearInterval(intervalId);
        }
    }, [offlineMode.isOffline, session.isConnected, saveLastOnlineTimestamp]);

    return null;
}

function ScribeContent({children}: { children: ReactNode }) {
    const t = useTranslations();
    const {lang: locale}: LangContextProps = useContext<LangContextProps>(LangContext);
    const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
    const {isCurrentlyOffline, offlineMode}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);

    const {
        session, setSession, isLoading,
        currentCredits, setCurrentCredits,
        amountSpent, setAmountSpent,
        showPinSetup, setShowPinSetup,
        showPinVerify, setShowPinVerify,
        handlePinVerifySuccess
    } = useAuthentication();

    const {
        isTermsAccepted, homeStepsGuide, setHomeStepsGuide,
        handleTermsAcceptance, handleHomeTour, homeSteps
    } = useOnboarding({session, setSession});

    const editor: Editor | null = useEditor({
        extensions: [
            StarterKit,
            Underline,
            TextAlign.configure({
                types: ['heading', 'paragraph'],
            }),
        ],
        injectCSS: false,
        immediatelyRender: false,
        shouldRerenderOnTransaction: true,
    });

    const pathname: string = usePathname();
    const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
    const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
    const [bookSettingId, setBookSettingId] = useState<string>('');
    const [showMigrationPopup, setShowMigrationPopup] = useState<boolean>(false);

    useEffect(function (): void {
        if (!isDesktop) return;
        const done: boolean = localStorage.getItem('electron_migration_done') === 'true';
        const dismissed: boolean = localStorage.getItem('electron_migration_dismissed') === 'true';
        if (!done && !dismissed) {
            setShowMigrationPopup(true);
        }
    }, []);

    const [serverSyncedBooks, setServerSyncedBooks] = useState<SyncedBook[]>([]);
    const [localSyncedBooks, setLocalSyncedBooks] = useState<SyncedBook[]>([]);
    const [bookSyncDiffsFromServer, setBookSyncDiffsFromServer] = useState<BookSyncCompare[]>([]);
    const [bookSyncDiffsToServer, setBookSyncDiffsToServer] = useState<BookSyncCompare[]>([]);
    const [serverOnlyBooks, setServerOnlyBooks] = useState<SyncedBook[]>([]);
    const [localOnlyBooks, setLocalOnlyBooks] = useState<SyncedBook[]>([]);

    const [serverSyncedSeries, setServerSyncedSeries] = useState<SyncedSeries[]>([]);
    const [localSyncedSeries, setLocalSyncedSeries] = useState<SyncedSeries[]>([]);
    const [seriesSyncDiffsFromServer, setSeriesSyncDiffsFromServer] = useState<SeriesSyncCompare[]>([]);
    const [seriesSyncDiffsToServer, setSeriesSyncDiffsToServer] = useState<SeriesSyncCompare[]>([]);
    const [serverOnlySeries, setServerOnlySeries] = useState<SyncedSeries[]>([]);
    const [localOnlySeries, setLocalOnlySeries] = useState<SyncedSeries[]>([]);

    const [localSyncQueue, setLocalSyncQueue] = useState<LocalSyncOperation[]>([]);
    const [isQueueProcessing, setIsQueueProcessing] = useState<boolean>(false);

    function addToLocalSyncQueue(channel: string, data: Record<string, unknown>): void {
        const operation: LocalSyncOperation = {
            id: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
            channel,
            data,
            timestamp: Date.now(),
        };
        setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] => [...prev, operation]);
    }

    useEffect((): void => {
        if (localSyncQueue.length === 0 || isQueueProcessing) return;

        async function processQueue(): Promise<void> {
            setIsQueueProcessing(true);
            const queueCopy: LocalSyncOperation[] = [...localSyncQueue];
            for (const operation of queueCopy) {
                try {
                    await tauri.invoke(operation.channel, {data: operation.data});
                    setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] =>
                        prev.filter((op: LocalSyncOperation): boolean => op.id !== operation.id)
                    );
                } catch (_e) { /* queue retry on next cycle */ }
            }
            setIsQueueProcessing(false);
        }

        processQueue().then();
    }, [localSyncQueue, isQueueProcessing]);

    useEffect((): void => {
        if (pathname === '/') {
            setCurrentBook(null);
            setCurrentChapter(undefined);
        }
    }, [pathname]);

    useEffect((): void => {
        if (!session.isConnected) return;
        // On desktop with DB initialized, AutoSyncOnReconnect handles fetching + tombstone exchange
        if (isDesktop && offlineMode.isDatabaseInitialized) return;
        getBooks().then();
        getSeries().then();
    }, [session, offlineMode.isDatabaseInitialized]);

    useEffect((): void => {
        const diffsFromServer: BookSyncCompare[] = [];
        const diffsToServer: BookSyncCompare[] = [];

        serverSyncedBooks.forEach((serverBook: SyncedBook): void => {
            const localBook: SyncedBook | undefined = localSyncedBooks.find((book: SyncedBook): boolean => book.id === serverBook.id);
            if (!localBook) return;
            const diff: BookSyncCompare | null = compareBookSyncs(serverBook, localBook);
            if (diff) diffsFromServer.push(diff);
        });

        localSyncedBooks.forEach((localBook: SyncedBook): void => {
            const serverBook: SyncedBook | undefined = serverSyncedBooks.find((book: SyncedBook): boolean => book.id === localBook.id);
            if (!serverBook) return;
            const diff: BookSyncCompare | null = compareBookSyncs(localBook, serverBook);
            if (diff) diffsToServer.push(diff);
        });

        setBookSyncDiffsFromServer(diffsFromServer);
        setBookSyncDiffsToServer(diffsToServer);
        setServerOnlyBooks(serverSyncedBooks.filter((serverBook: SyncedBook): boolean => !localSyncedBooks.find((localBook: SyncedBook): boolean => localBook.id === serverBook.id)));
        setLocalOnlyBooks(localSyncedBooks.filter((localBook: SyncedBook): boolean => !serverSyncedBooks.find((serverBook: SyncedBook): boolean => serverBook.id === localBook.id)));
    }, [localSyncedBooks, serverSyncedBooks]);

    useEffect((): void => {
        const diffsFromServer: SeriesSyncCompare[] = [];
        const diffsToServer: SeriesSyncCompare[] = [];

        serverSyncedSeries.forEach((serverSeries: SyncedSeries): void => {
            const localSeries: SyncedSeries | undefined = localSyncedSeries.find((series: SyncedSeries): boolean => series.id === serverSeries.id);
            if (!localSeries) return;
            const diff: SeriesSyncCompare | null = compareSeriesSyncs(serverSeries, localSeries);
            if (diff) diffsFromServer.push(diff);
        });

        localSyncedSeries.forEach((localSeries: SyncedSeries): void => {
            const serverSeries: SyncedSeries | undefined = serverSyncedSeries.find((series: SyncedSeries): boolean => series.id === localSeries.id);
            if (!serverSeries) return;
            const diff: SeriesSyncCompare | null = compareSeriesSyncs(localSeries, serverSeries);
            if (diff) diffsToServer.push(diff);
        });

        setSeriesSyncDiffsFromServer(diffsFromServer);
        setSeriesSyncDiffsToServer(diffsToServer);
        setServerOnlySeries(serverSyncedSeries.filter((s: SyncedSeries): boolean => !localSyncedSeries.find((l: SyncedSeries): boolean => l.id === s.id)));
        setLocalOnlySeries(localSyncedSeries.filter((l: SyncedSeries): boolean => !serverSyncedSeries.find((s: SyncedSeries): boolean => s.id === l.id)));
    }, [localSyncedSeries, serverSyncedSeries]);

    async function getBooks(): Promise<void> {
        try {
            let localBooksResponse: SyncedBook[] = [];
            let serverBooksResponse: SyncedBook[] = [];

            if (!isCurrentlyOffline()) {
                if (isDesktop && offlineMode.isDatabaseInitialized) {
                    localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];

                    const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
                    const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;

                    const localTombstones: tauri.TombstoneRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as tauri.TombstoneRecord[];

                    const serverResponse: { books: SyncedBook[]; tombstones: tauri.TombstoneRecord[] } = await apiPost<{
                        books: SyncedBook[]; tombstones: tauri.TombstoneRecord[];
                    }>('books/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);

                    serverBooksResponse = serverResponse.books;
                    await tauri.applyBookTombstones(serverResponse.tombstones ?? []);
                } else {
                    const serverResponse: { books: SyncedBook[] } = await apiPost<{
                        books: SyncedBook[]
                    }>('books/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
                    if (serverResponse) serverBooksResponse = serverResponse.books;
                }
            } else {
                if (isDesktop && offlineMode.isDatabaseInitialized) {
                    localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
                }
            }

            setServerSyncedBooks(serverBooksResponse);
            setLocalSyncedBooks(localBooksResponse);
        } catch (e: unknown) {
            if (e instanceof Error) {
                errorMessage(e.message);
            } else {
                errorMessage(t('homePage.errors.fetchBooksError'));
            }
        }
    }

    async function getSeries(): Promise<void> {
        try {
            let localSeriesResponse: SyncedSeries[] = [];
            let serverSeriesResponse: SyncedSeries[] = [];

            if (!isCurrentlyOffline()) {
                if (isDesktop && offlineMode.isDatabaseInitialized) {
                    localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];

                    const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
                    const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;

                    const localTombstones: tauri.TombstoneRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as tauri.TombstoneRecord[];

                    const serverResponse: { series: SyncedSeries[]; tombstones: tauri.TombstoneRecord[] } = await apiPost<{
                        series: SyncedSeries[]; tombstones: tauri.TombstoneRecord[];
                    }>('series/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);

                    serverSeriesResponse = serverResponse.series;
                    await tauri.applySeriesTombstones(serverResponse.tombstones ?? []);
                } else {
                    const serverResponse: { series: SyncedSeries[] } = await apiPost<{
                        series: SyncedSeries[]
                    }>('series/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
                    if (serverResponse) serverSeriesResponse = serverResponse.series;
                }
            } else {
                if (isDesktop && offlineMode.isDatabaseInitialized) {
                    localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
                }
            }

            setServerSyncedSeries(serverSeriesResponse);
            setLocalSyncedSeries(localSeriesResponse);
        } catch (e: unknown) {
            if (e instanceof Error) {
                errorMessage(e.message);
            } else {
                errorMessage(t('homePage.errors.fetchBooksError'));
            }
        }
    }

    if (isLoading) {
        return (
            <div
                className="bg-background text-text-primary h-screen flex flex-col items-center justify-center font-['Lora']">
                <div className="flex flex-col items-center space-y-6">
                    <div className="animate-pulse">
                        <img src="/logo.png" alt="ERitors Logo" width={400} height={400}/>
                    </div>
                    <PulseLoader text={t('homePage.loading')}/>
                </div>
            </div>
        );
    }

    return (
        <SessionContext.Provider value={{session: session, setSession: setSession}}>
            <LocalSyncQueueContext.Provider value={{
                queue: localSyncQueue,
                setQueue: setLocalSyncQueue,
                addToQueue: addToLocalSyncQueue,
                isProcessing: isQueueProcessing,
            }}>
            <BooksSyncContext.Provider value={{
                serverSyncedBooks,
                setServerSyncedBooks,
                localSyncedBooks,
                setLocalSyncedBooks,
                booksToSyncFromServer: bookSyncDiffsFromServer,
                setBooksToSyncFromServer: setBookSyncDiffsFromServer,
                booksToSyncToServer: bookSyncDiffsToServer,
                setBooksToSyncToServer: setBookSyncDiffsToServer,
                setServerOnlyBooks,
                setLocalOnlyBooks,
                serverOnlyBooks,
                localOnlyBooks
            }}>
            <SeriesSyncContext.Provider value={{
                serverSyncedSeries,
                localSyncedSeries,
                seriesToSyncFromServer: seriesSyncDiffsFromServer,
                seriesToSyncToServer: seriesSyncDiffsToServer,
                setServerSyncedSeries,
                setLocalSyncedSeries,
                setServerOnlySeries,
                setLocalOnlySeries,
                setSeriesToSyncFromServer: setSeriesSyncDiffsFromServer,
                setSeriesToSyncToServer: setSeriesSyncDiffsToServer,
                serverOnlySeries,
                localOnlySeries
            }}>
                {isDesktop && <AutoSyncOnReconnect/>}
                <BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
                    <ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
                        <AIUsageContext.Provider value={{
                            totalCredits: currentCredits,
                            setTotalCredits: setCurrentCredits,
                            totalPrice: amountSpent,
                            setTotalPrice: setAmountSpent
                        }}>
                            <SettingBookContext.Provider value={{bookSettingId, setBookSettingId}}>
                                <div
                                    className="bg-tertiary text-text-primary h-screen flex flex-col font-['Lora']">
                                    <EditorContext.Provider value={{editor: editor}}>
                                        <ScribeControllerBar/>
                                        <div className="flex-1 flex overflow-hidden">
                                            <ScribeLeftBar/>
                                            <div
                                                className="flex-1 min-w-0 bg-darkest-background rounded-xl m-1 overflow-hidden">
                                                {children}
                                            </div>
                                            <ComposerRightBar/>
                                        </div>
                                        <ScribeFooterBar/>
                                    </EditorContext.Provider>
                                </div>
                                {homeStepsGuide &&
                                    <GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
                                               onClose={(): void => setHomeStepsGuide(false)}/>
                                }
                                {!isCurrentlyOffline() && !isTermsAccepted && <TermsOfUse onAccept={handleTermsAcceptance}/>}
                                {isDesktop && showPinSetup && (
                                    <OfflinePinSetup
                                        showOnFirstLogin={true}
                                        onClose={(): void => setShowPinSetup(false)}
                                        onSuccess={(): void => setShowPinSetup(false)}
                                    />
                                )}
                                {isDesktop && showPinVerify && (
                                    <OfflinePinVerify
                                        onSuccess={handlePinVerifySuccess}
                                        onCancel={(): void => {}}
                                    />
                                )}
                                {showMigrationPopup && (
                                    <MigrationModal
                                        onClose={function (): void { setShowMigrationPopup(false); }}
                                        onSuccess={function (): void { setShowMigrationPopup(false); window.location.reload(); }}
                                    />
                                )}
                            </SettingBookContext.Provider>
                        </AIUsageContext.Provider>
                    </ChapterContext.Provider>
                </BookContext.Provider>
            </SeriesSyncContext.Provider>
            </BooksSyncContext.Provider>
            </LocalSyncQueueContext.Provider>
        </SessionContext.Provider>
    );
}

export default function ScribeShell({children}: { children: ReactNode }) {
    const [locale, setLocale] = useState<SupportedLocale>('fr');
    const {theme, setTheme} = useTheme();

    useEffect((): void => {
        initCrashReporter();
    }, []);

    useEffect((): void => {
        const lang: string | null = getCookie('lang');
        if (lang && isSupportedLocale(lang)) {
            setLocale(lang);
        }
    }, []);

    useEffect((): void => {
        changeLanguage(locale);
    }, [locale]);

    return (
        <ThemeContext.Provider value={{theme, setTheme}}>
            <LangContext.Provider value={{lang: locale, setLang: setLocale}}>
                <OfflineProvider>
                    <AlertProvider>
                        <ScribeContent>{children}</ScribeContent>
                    </AlertProvider>
                </OfflineProvider>
            </LangContext.Provider>
        </ThemeContext.Provider>
    );
}
