import axios, { 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, useState } from "react"
import { useQueryClient } from "react-query"
import { API_URL, reportAxiosError } from "../../utils/RequestUtils"
import { Landmark } from "../landmarks"
import { queryKeys } from "../query-keys"
import {sha256} from 'react-native-sha256'
import {v4} from 'uuid'

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>,
    refreshToken: string,
    userId: string,
    anonUserId: string,
    loading: boolean,
    error: string,
    setLoading: (state: boolean) => void,
    setError: (error: string) => void,
    sendApiRequestAsync: (config: RequestConfig) => Promise<any>,
    login: () => Promise<void>,
    logout: () => Promise<void>,
    refreshAccessToken: () => Promise<void>,
    getNotificationTokenFromServer: () => Promise<string>,
    landmarkOwnedByUser: (landmark: Landmark) => boolean,
}

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

export interface IdToken {
    sub: string
}

interface AuthenticationResult {
    success: boolean
    errorMessage?: string
}

const setStorageItem = async (key: string, value: string) => {
    if (value) {
        await setItemAsync(key, value)
    }
    else {
        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 [loading, setLoading] = useState<boolean>(false)
    const [error, setError] = useState<string>('')

    const queryClient = useQueryClient()

    useEffect(() => {
        const loadAuthStateFromStorageOnAppLoad = async () => {
            const accessTokenFromStorage = await getItemAsync(SECURESTORE_ACCESSTOKEN)

            if (accessTokenFromStorage) {
                try {
                    await sendApiRequestAsync({
                        axiosConfig: {
                            method: 'GET',
                            url: '/api/me/',
                            headers: {Authorization: 'Bearer ' + accessTokenFromStorage}
                        }, 
                        authorized: false,
                        errorMessage: 'Failed to retrieve user data from server'})

                    setAccessToken(accessTokenFromStorage)
                    setRefreshToken(await getItemAsync(SECURESTORE_REFRESHTOKEN))
                    setNotificationToken(await getItemAsync(SECURESTORE_NOTIFTOKEN))
                    setUserId(await getItemAsync(SECURESTORE_ID))
                    setAnonUserId('')
                    return
                }
                catch {}
            }

            setAccessTokenAsync("")
            setRefreshTokenAsync("")
            setNotificationTokenAsync('')
            setUserIdAsync('')
            
            let anonUserId = await getItemAsync(SECURESTORE_ANONID)
            if (anonUserId) {
                setAnonUserId(anonUserId)
            }
            else {
                anonUserId = v4()
                await setItemAsync(SECURESTORE_ANONID, anonUserId)
                setAnonUserId(anonUserId)
            }
        }
        loadAuthStateFromStorageOnAppLoad()
    }, [])

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

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

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

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

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

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

        axiosConfig.baseURL = API_URL

        try {
            return await axios(axiosConfig)
        } catch (error) {
            reportAxiosError(errorMessage, error)
            console.log(error.response)
        }
    }

    const login = async (): Promise<AuthenticationResult> => {
        setLoading(true)
        console.log('[Authentication]: User is attempting to login, opening web portal login...')
    
        // 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
        const response = await request.promptAsync(discovery);
    
        // if succesful, prepare a request for an access/id token
        if (response.type == "success" && request.codeVerifier) {
            console.log('[Authentication]: User authentication was successful.')
            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);  
            console.log('[Authentication]: Attempting to retrieve 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;

                setLoading(false)                
    
                await setAccessTokenAsync(tokenResponse.access_token);
                await setRefreshTokenAsync(tokenResponse.refresh_token);
                await setUserIdAsync(idToken.sub)
                setAnonUserId('')

                console.log('[Authentication]: Tokens successfully retrieved.')
                
    
                return {success: true}
            } catch (error) {
                reportAxiosError("Something went wrong when retrieving access token", error);
                setLoading(false)
                setError("Something went wrong while logging in. Please try again.")
            } 
        }
        else if (response.type == "cancel") {
            setLoading(false)
        }
        else {
            setError("Something went wrong while logging in. Please try again.")
        }
    } 

    const logout = async () => {
        setLoading(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.getOwnedProfile, null)
            await clearAuthStorage()

        } catch (error) {
            reportAxiosError("Something went wrong when logging out", error);
            setError("Something went wrong while logging out. Please try again.")
        } 
        setLoading(false)
    }

    const refreshAccessToken = async () => {
        setLoading(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) {
                reportAxiosError("[Authentication]: Error when trying to refresh access token", error);            
            }
        }

        setLoading(false)
    }

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

    const authState = useMemo(() => ({
        accessToken,
        notificationToken,
        setNotificationTokenAsync,
        setAccessTokenAsync,
        setRefreshTokenAsync,
        setUserIdAsync,
        clearAuthStorage,
        landmarkOwnedByUser,
        refreshToken,
        userId,
        loading,
        error,
        anonUserId,
        setLoading,
        setError,
        sendApiRequestAsync,
        login,
        logout,
        refreshAccessToken,
    }), [accessToken, refreshToken, userId, loading, error, 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
}
