import {
    createContext,
    useCallback,
    useEffect,
    useMemo,
    useState,
} from 'react';
import type { PropsWithChildren } from 'react';

export type ThemePreference = 'light' | 'dark' | 'auto';
export type ResolvedTheme = 'light' | 'dark';

export type ThemeContextValue = {
    theme: ThemePreference;
    resolvedTheme: ResolvedTheme;
    setTheme: (theme: ThemePreference) => void;
};

export const ThemeContext = createContext<ThemeContextValue | undefined>(
    undefined,
);

const storageKey = 'theme';
const darkThemeQuery = '(prefers-color-scheme: dark)';
const resolvedThemeClasses: ResolvedTheme[] = ['light', 'dark'];

function isThemePreference(value: string | null): value is ThemePreference {
    return value === 'light' || value === 'dark' || value === 'auto';
}

function getSystemTheme(mediaQuery?: MediaQueryList): ResolvedTheme {
    if (mediaQuery) {
        return mediaQuery.matches ? 'dark' : 'light';
    }

    if (typeof window === 'undefined' || !window.matchMedia) {
        return 'light';
    }

    return window.matchMedia(darkThemeQuery).matches ? 'dark' : 'light';
}

export function getStoredThemePreference(): ThemePreference {
    if (typeof window === 'undefined') {
        return 'auto';
    }

    try {
        const storedTheme = window.localStorage.getItem(storageKey);

        return isThemePreference(storedTheme) ? storedTheme : 'auto';
    } catch {
        return 'auto';
    }
}

export function resolveThemePreference(
    theme: ThemePreference,
    mediaQuery?: MediaQueryList,
): ResolvedTheme {
    return theme === 'auto' ? getSystemTheme(mediaQuery) : theme;
}

export function applyResolvedTheme(resolvedTheme: ResolvedTheme): void {
    if (typeof document === 'undefined') {
        return;
    }

    document.documentElement.classList.remove(...resolvedThemeClasses);
    document.documentElement.classList.add(resolvedTheme);
    document.documentElement.style.colorScheme = resolvedTheme;
}

export function applyInitialThemePreference(): void {
    const theme = getStoredThemePreference();

    applyResolvedTheme(resolveThemePreference(theme));
}

function persistThemePreference(theme: ThemePreference): void {
    if (typeof window === 'undefined') {
        return;
    }

    try {
        window.localStorage.setItem(storageKey, theme);
    } catch {
        // Storage can be unavailable in private browsing or locked-down contexts.
    }
}

export function ThemeProvider({ children }: PropsWithChildren) {
    const [theme, setThemeState] = useState<ThemePreference>(() =>
        getStoredThemePreference(),
    );
    const [systemTheme, setSystemTheme] = useState<ResolvedTheme>(() =>
        getSystemTheme(),
    );
    const resolvedTheme = useMemo(
        () => (theme === 'auto' ? systemTheme : theme),
        [systemTheme, theme],
    );

    useEffect(() => {
        applyResolvedTheme(resolvedTheme);
    }, [resolvedTheme]);

    useEffect(() => {
        if (
            theme !== 'auto' ||
            typeof window === 'undefined' ||
            !window.matchMedia
        ) {
            return;
        }

        const mediaQuery = window.matchMedia(darkThemeQuery);
        const syncSystemTheme = (event: MediaQueryListEvent) => {
            setSystemTheme(event.matches ? 'dark' : 'light');
        };

        if (mediaQuery.addEventListener) {
            mediaQuery.addEventListener('change', syncSystemTheme);
        } else {
            mediaQuery.addListener(syncSystemTheme);
        }

        return () => {
            if (mediaQuery.removeEventListener) {
                mediaQuery.removeEventListener('change', syncSystemTheme);
            } else {
                mediaQuery.removeListener(syncSystemTheme);
            }
        };
    }, [theme]);

    useEffect(() => {
        if (typeof window === 'undefined') {
            return;
        }

        const syncStoredTheme = (event: StorageEvent) => {
            if (event.key !== storageKey) {
                return;
            }

            const nextTheme = isThemePreference(event.newValue)
                ? event.newValue
                : 'auto';

            if (nextTheme === 'auto') {
                setSystemTheme(getSystemTheme());
            }

            setThemeState(nextTheme);
        };

        window.addEventListener('storage', syncStoredTheme);

        return () => {
            window.removeEventListener('storage', syncStoredTheme);
        };
    }, []);

    const setTheme = useCallback((nextTheme: ThemePreference) => {
        if (nextTheme === 'auto') {
            setSystemTheme(getSystemTheme());
        }

        setThemeState(nextTheme);
        persistThemePreference(nextTheme);
    }, []);

    const value = useMemo(
        () => ({
            theme,
            resolvedTheme,
            setTheme,
        }),
        [resolvedTheme, setTheme, theme],
    );

    return (
        <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
    );
}
