import axios, { AxiosError, AxiosRequestConfig } from "axios"
import { loadAsync, makeRedirectUri, ResponseType } from "expo-auth-session"
import { deleteItemAsync, getItemAsync, setItemAsync } from "expo-secure-store"
import jwt_decode from 'jwt-decode'
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"
import { Alert, AppState } from "react-native"
import { useQueryClient } from "react-query"
import { v4 } from 'uuid'
import { navigate } from "../../navigation/root-navigator"
import { LogCategory, LOGGING } from "../../utils/logging"
import { API_URL } from "../../utils/RequestUtils"
import { Landmark } from "./landmarks"
import { queryKeys } from "./query-keys"

export const SECURESTORE_ACCESSTOKEN = "access"
export const SECURESTORE_REFRESHTOKEN = "refresh"
export const SECURESTORE_NOTIFTOKEN = 'notif'
export const SECURESTORE_ID = 'id'
export const SECURESTORE_ANONID = 'anon'

interface AuthState {
    accessToken: string,
    notificationToken: string,
    setNotificationTokenAsync: (token: string) => Promise<void>,
    setAccessTokenAsync: (token: string) => Promise<void>,
    setRefreshTokenAsync: (token: string) => Promise<void>,
    setUserIdAsync: (id: string) => Promise<void>,
    clearAuthStorage: () => Promise<void>,
    setAlert: (alert: GlobalAlert) => void,
    refreshToken: string,
    userId: string,
    anonUserId: string,
    authStateLoading: boolean,
    setAuthStateLoading: (state: boolean) => void,
    sendApiRequestAsync: (config: RequestConfig) => Promise<any>,
    login: () => Promise<AuthenticationResult>,
    logout: () => Promise<void>,
    landmarkOwnedByUser: (landmark: Landmark) => boolean,
}

interface RequestConfig {
    axiosConfig: AxiosRequestConfig,
    authorized: boolean
    errorMessage: string
    loggingCategory: LogCategory
}

export interface IdToken {
    sub: string
}

interface AuthenticationResult {
    success: boolean
    errorMessage?: string
}

interface GlobalAlert {
    title: string
    message: string
    type: 'success' | 'error' | 'warning'
    callback?: () => void,
    callbackButtonText?: string
}

const setStorageItem = async (key: string, value: string) => {
    if (value) {
        LOGGING.log('SYSTEM', 'info', "Setting storage item for: " + key)
        await setItemAsync(key, value)
    }
    else {
        LOGGING.log('SYSTEM', 'info', "Deleting storage item: " + key)
        await deleteItemAsync(key)
    }
}

const AuthContext = createContext(null)

/**
 * A base url for the api's authorization endpoints
 */
 const issuer = API_URL + "/o";

 /**
  * An object containing the discovery endpoints for the api, necessary for OIDC authentication {@link https://swagger.io/docs/specification/authentication/openid-connect-discovery/}
  */
 const discovery = {
     authorizationEndpoint: issuer + "/authorize/",
     tokenEndpoint: issuer + "/token/",
     revocationEndpoint: issuer + "/revoke/",
 };

 const redirectUri = makeRedirectUri({
    path: 'callback'
});

export const AuthContextProvider: React.FC = ({children}) => {
    const [accessToken, _setAccessToken] = useState<string>()
    const [refreshToken, setRefreshToken] = useState<string>()
    const [notificationToken, setNotificationToken] = useState<string>()
    const [userId, setUserId] = useState<string>()
    const [anonUserId, setAnonUserId] = useState<string>()
    const [authStateLoading, setAuthStateLoading] = useState<boolean>(false)
    const [alert, setAlert] = useState<GlobalAlert>()

    const refreshingToken = useRef<boolean>(false)
    const accessTokenRef = useRef<string>(accessToken)

    const setAccessToken = (token: string) => {
        accessTokenRef.current = token
        _setAccessToken(token)
    }

    const queryClient = useQueryClient()

    useEffect(() => {
        const loadAuthStateFromStorageOnAppLoad = async () => {
            LOGGING.log("AUTH", 'info', "App started, loading existing auth state from storage...")
            const accessTokenFromStorage = await getItemAsync(SECURESTORE_ACCESSTOKEN)

            if (accessTokenFromStorage) {
                LOGGING.log("AUTH", 'info', "Access token found in storage, testing if it is valid...")
                try {
                    const response = await sendApiRequestAsync({
                        axiosConfig: {
                            method: 'GET',
                            url: '/api/me/',
                            headers: {Authorization: 'Bearer ' + accessTokenFromStorage}
                        }, 
                        authorized: false,
                        errorMessage: 'Failed to retrieve user data from server',
                        loggingCategory: 'AUTH'
                    })

                    if (response.status == 200) {
                        setAccessToken(accessTokenFromStorage)
                        setRefreshToken(await getItemAsync(SECURESTORE_REFRESHTOKEN))
                        setNotificationToken(await getItemAsync(SECURESTORE_NOTIFTOKEN))
                        setUserId(await getItemAsync(SECURESTORE_ID))
                        LOGGING.log("AUTH", 'info', "Access token is valid, auth state has been loaded...")
                        return
                    }
                }
                catch {
                }
            }

            await setAccessTokenAsync("")
            await setRefreshTokenAsync("")
            await setNotificationTokenAsync('')
            await setUserIdAsync('')
            LOGGING.log("AUTH", 'info', "No auth state found in storage, starting with an empty auth state...")
            
            let anonUserId = await getItemAsync(SECURESTORE_ANONID)
            if (anonUserId) {
                setAnonUserId(anonUserId)
            }
            else {
                anonUserId = v4()
                await setItemAsync(SECURESTORE_ANONID, anonUserId)
                setAnonUserId(anonUserId)
            }
            LOGGING.log("AUTH", 'info', "Created anonymous id for non account user: " + anonUserId)
        }
        loadAuthStateFromStorageOnAppLoad()
    }, [])

    useEffect(() => {
        const checkAuthStateOnAppForeground = async (state) => {
            let currentAccessToken = accessTokenRef.current
            if (currentAccessToken === undefined) {
                currentAccessToken = await getItemAsync(SECURESTORE_ACCESSTOKEN)
            }

            if (currentAccessToken && state == 'active') {
                LOGGING.log("AUTH", 'info', "App was foregrounded and access token found, checking if it is still valid...")
                try {   
                    const response = await sendApiRequestAsync({
                        axiosConfig: {
                            method: 'GET',
                            url: '/api/me/',
                            headers: {Authorization: 'Bearer ' + accessTokenRef.current}
                        }, 
                        authorized: false,
                        errorMessage: 'Failed to retrieve user data from server while checking access token',
                        loggingCategory: 'AUTH'
                    })
                    if (response.status == 200) {
                        LOGGING.log("AUTH", 'info', "Access token is valid, no action required")
                        return
                    }
                } catch (error) {}
            }
        }

        AppState.addEventListener('change', checkAuthStateOnAppForeground)

        return () => {
            AppState.removeEventListener('change', checkAuthStateOnAppForeground)
        }
    }, [])

    useEffect(() => {
        if (alert) {
            LOGGING.log("SYSTEM", 'info', "Showing alert")
            let buttons = [{text: alert.callbackButtonText ? alert.callbackButtonText : 'OK', onPress: alert.callback}, {text: 'Cancel', onPress: () => console.log('canceled')}]
            const alertTitle = alert.title
            Alert.alert(alertTitle, alert.message, buttons)
            setAlert(undefined)
        }
    }, [alert])

    useEffect(() => {
        const convertExistingAnonymousLandmarksOnAccessTokenChange = async () => {
            if (accessToken && anonUserId) {
                await convertExistingAnonymousLandmarks()
            }
        }
        convertExistingAnonymousLandmarksOnAccessTokenChange()
    }, [accessToken])

    const setAccessTokenAsync = async (token: string) => {
        setAccessToken(token)
        await setStorageItem(SECURESTORE_ACCESSTOKEN, token)
    }

    const setRefreshTokenAsync = async (token: string) => {
        setRefreshToken(token)
        await setStorageItem(SECURESTORE_REFRESHTOKEN, token)
    }

    const setUserIdAsync = async (id: string) => {
        setUserId(id)
        await setStorageItem(SECURESTORE_ID, id)
    }

    const setNotificationTokenAsync = async (token: string) => {
        setNotificationToken(token)
        await setStorageItem(SECURESTORE_NOTIFTOKEN, token)
    }

    const clearAuthStorage = async () => {
        await Promise.all([
            setAccessTokenAsync(""),
            setRefreshTokenAsync(""),
            setNotificationTokenAsync(""),
            setUserIdAsync("")
        ])
    }

    const sendApiRequestAsync = async ({axiosConfig, authorized = false, errorMessage = 'An error occured', loggingCategory = "SYSTEM"}: RequestConfig) => {
        if (authorized && !axiosConfig?.headers?.Authorization) {
            axiosConfig.headers = {
                ...axiosConfig.headers,
                Authorization: `Bearer ${accessToken}`,
            }   
        }

        axiosConfig.baseURL = API_URL

        try {
            return await axios(axiosConfig)
        } catch (error) {
            const axiosError = error as AxiosError
            if (axiosError.response.status == 401 || axiosError.response.status == 403) {
                if (!refreshingToken.current) {
                    await refreshAccessToken()
                }
            }
            console.log(error)
            LOGGING.log("AUTH", 'error', errorMessage)
        }
    }

    const login = async (): Promise<AuthenticationResult> => {
        try {
            setAuthStateLoading(true)
            LOGGING.log('AUTH', 'info', "Starting login process...")
        
            // initiate authentication request to the server 
            const request = await loadAsync({
                clientId: "atlas.mobile",
                responseType: ResponseType.Code,
                redirectUri,
                usePKCE: true,
                scopes: ['openid']                
            }, discovery)  
        
            // handle authentication response from the server
            LOGGING.log('AUTH', 'info', "Prompting user with web browser...")
            const response = await request.promptAsync(discovery, {createTask: false});
        
            // if succesful, prepare a request for an access/id token
            if (response.type == "success" && request.codeVerifier) {
                const tokenData = new URLSearchParams();
                tokenData.append('grant_type', 'authorization_code');
                tokenData.append('client_id', 'atlas.mobile');
                tokenData.append('code', response.params.code);
                tokenData.append('redirect_uri', request.redirectUri);
                tokenData.append('code_verifier', request.codeVerifier);  
                LOGGING.log('AUTH', 'info', "User successfully authenticated, attempting to get access token...")
        
                // send the token request
                try {
                    const response = await axios.post(API_URL + `/o/token/`, tokenData, {
                        headers: {
                            'Content-Type': "application/x-www-form-urlencoded"
                        },
                    });  
        
                    const tokenResponse = response.data;
        
                    // if its a successful response, decode the jwt id token, and store the tokens in the corresponding stores
                    const idToken = jwt_decode(tokenResponse.id_token) as IdToken;

                    setAuthStateLoading(false)                
        
                    await setAccessTokenAsync(tokenResponse.access_token);
                    await setRefreshTokenAsync(tokenResponse.refresh_token);
                    await setUserIdAsync(idToken.sub)
                    
                    LOGGING.log('AUTH', 'info', "Successfully retrieved access token, login process completed")
        
                    return {success: true}
                } catch (error) {
                    LOGGING.log('AUTH', 'error', "An error occured when trying to retrieve access token: " + error)
                    setAuthStateLoading(false)
                } 
            }
            else if (response.type == "cancel") {
                LOGGING.log('AUTH', 'info', "User canceled login")
                setAuthStateLoading(false)
            }
            else if (response.type == "dismiss") {
                LOGGING.log('AUTH', 'info', "User canceled login")
                setAuthStateLoading(false)
            }
            else {
                LOGGING.log('AUTH', 'error', "An error occured when trying to authenticate user: " + response.type)
                setAlert({title: 'Login error', message: "Something went wrong while logging in. Please try again.", callback: () => setAuthStateLoading(false), type: 'error'})
            }
        }
        catch (error) {
            LOGGING.log('AUTH', 'error', "An error occured when trying to authenticate user: " + error)
            setAlert({title: 'Login error', message: "Something went wrong while logging in. Please try again.", callback: () => setAuthStateLoading(false), type: 'error'})
        }

        return {success: false}
    } 

    const logout = async () => {
        LOGGING.log('AUTH', 'info', "Starting logout process...")
        setAuthStateLoading(true)
        try {
            const tokenParams = new URLSearchParams();
            tokenParams.append('client_id', 'atlas.mobile');
            tokenParams.append('token', accessToken as string);
            
            await axios.post(API_URL + `/o/revoke-token/`, tokenParams, {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
            });  

            queryClient.setQueryData(queryKeys.GET_OWN_PROFILE, null)
            await setAnonUserId(await getItemAsync(SECURESTORE_ANONID))
            await clearAuthStorage()
            LOGGING.log('AUTH', 'info', "Successfully logged out. Local data has been cleared, and the device's anonymous id has been reloaded.")

        } catch (error) {
            LOGGING.log('AUTH', 'info', "Something when wrong while logging out: " + error)
            setAlert({title: 'Error', message: "Something went wrong while logging out. Please try again.", callback: () => {}, type: 'error'})
        } 
        setAuthStateLoading(false)
    }

    const refreshAccessToken = async () => {
        let success = true;
        refreshingToken.current = true
        let currentRefreshToken = refreshToken
        if (!currentRefreshToken) {
            currentRefreshToken = await getItemAsync(SECURESTORE_REFRESHTOKEN);
        }

        if (currentRefreshToken) {
            try {
                const tokenData = new URLSearchParams();
                tokenData.append('grant_type', 'refresh_token');
                tokenData.append('refresh_token', currentRefreshToken);
                tokenData.append('client_id', 'atlas.mobile');
                console.log('[Authentication]: Attempting to refresh token...')
                const { data: refreshResponseData } = await axios.post(API_URL + "/o/token/", tokenData, {
                    headers: { 'Content-Type': "application/x-www-form-urlencoded" }
                });   
                
                await setRefreshTokenAsync(refreshResponseData.refresh_token);
                await setAccessTokenAsync(refreshResponseData.access_token);

                console.info('Successfully refreshed access token.')
            }
            catch (error) {
                setAlert({
                    title: 'Login timeout', 
                    message: "It looks like you've been away for awhile! Unfortunately we weren't able to log you back in, you'll have to do that manually.", 
                    callback: () => {navigate('Account')},
                    type: 'error',
                    callbackButtonText: "Go to login"
                })
                success = false
            }
        }

        refreshingToken.current = false
        return success
    }

    const landmarkOwnedByUser = (landmark: Landmark) => {
        const owned = landmark?.user && landmark?.user == userId || landmark?.anonymous && landmark?.anonymous == anonUserId
        return owned
    }

    const convertExistingAnonymousLandmarks = async () => {
        try {
            LOGGING.log("AUTH", 'info', "Checking server to see if new logged in user has any anonymous landmarks...")
            const response = await sendApiRequestAsync({
                axiosConfig: {
                    method: 'GET',
                    url: `/api/landmarks/anon/${anonUserId}/`
                },
                authorized: true,
                errorMessage: 'An error occured while checking for anonymous landmarks',
                loggingCategory: "AUTH",
            })
    
            if (response?.data?.has_landmark) {
                // send request to convert landarks
                LOGGING.log("AUTH", 'info', "Anonymous landmarks found belonging to user. Converting to owned landmarks...")
                await sendApiRequestAsync({
                    axiosConfig: {
                        method: 'POST',
                        url: `/api/landmarks/convert/${anonUserId}/`
                    },
                    authorized: true,
                    errorMessage: 'Something went wrong when converting anonymous landmarks',
                    loggingCategory: "AUTH",
                })

                LOGGING.log("AUTH", 'info', "Successfully converted anonymous landmarks to owned landmarks.")
                
                setAlert({
                    title: 'Heads up',
                    message: "It looks like you added some landmarks before creating an account, so those landmarks are now owned by your newly created account.", 
                    callback: () => LOGGING.log('AUTH', 'info', 'Notifying user of converted landmarks'),
                    type: 'warning'
                })
            }

            setAnonUserId('')
        } catch (error) {
            LOGGING.log("AUTH", 'error', "An error occured while converting anonymous landmarks: " + error)

            // TODO: implement transfer anonymus landmarks in accounts
            // setAlert({
            //     title: 'Error',
            //     message: "Failed to convert your old anonymous landmarks to your account. Please try again manually by going to Account -> Information -> Transfer anonymous landmarks.",
            //     callback: () => navigate("Account"),
            //     type: 'error'
            // })
            return false
        }
    }

    const authState = useMemo<AuthState>(() => ({
        accessToken,
        notificationToken,
        setNotificationTokenAsync,
        setAccessTokenAsync,
        setRefreshTokenAsync,
        setUserIdAsync,
        clearAuthStorage,
        landmarkOwnedByUser,
        refreshToken,
        userId,
        authStateLoading,
        anonUserId,
        setAuthStateLoading,
        setAlert,
        sendApiRequestAsync,
        login,
        logout,
    }), [accessToken, refreshToken, userId, authStateLoading, anonUserId])

    return (
        <AuthContext.Provider value={authState}>
            {children}
        </AuthContext.Provider>
    )
}

export const useAuth = () => {
    const context = useContext<AuthState>(AuthContext)
    if (context === undefined) {
        throw new Error('useAuth must be used within a AuthProvider')
    }
    return context
}
