import { OAuthProcess, apiURL } from './defines'

import {
    type RegistrationResponseJSON,
    type AuthenticationResponseJSON,
} from '@simplewebauthn/browser'

import type {
    // Core types
    AuthError,
    User,
    Flow,
    Authenticated,
    AuthenticationMeta,
    // Request types
    LoginRequest,
    SignupRequest,
    ProviderSignupRequest,
    ReauthenticateRequest,
    ChangePasswordRequest,
    ResetPasswordRequest,
    MFAAuthenticateRequest,
    WebAuthnUpdateRequest,
    // Response types
    AllauthResponse,
    AuthenticatedResponse,
    AuthenticationRequiredResponse,
    ReauthenticationRequiredResponse,
    ConfigurationResponse,
    EmailListResponse,
    SessionListResponse,
    AuthenticatorListResponse,
    ProviderAccountListResponse,
    TOTPStatusResponse,
    RecoveryCodesResponse,
    WebAuthnCreationOptionsResponse,
    WebAuthnRequestOptionsResponse,
    EmailVerificationInfoResponse,
    ErrorResponse,
} from './types'

export type { AuthError } from './types'

// Registration = creating new credentials (signup, add)
// Authentication = verifying existing credentials (login, authenticate, reauthenticate)
type RegistrationCredential = RegistrationResponseJSON
type AuthenticationCredential = AuthenticationResponseJSON

/**
 * Union of all possible auth responses
 */
export type AuthResponse =
    | AuthenticatedResponse
    | AuthenticationRequiredResponse
    | ReauthenticationRequiredResponse
    | ConfigurationResponse
    | EmailListResponse
    | SessionListResponse
    | AuthenticatorListResponse
    | ProviderAccountListResponse
    | TOTPStatusResponse
    | RecoveryCodesResponse
    | WebAuthnCreationOptionsResponse
    | WebAuthnRequestOptionsResponse
    | EmailVerificationInfoResponse
    | ErrorResponse
    | AllauthResponse

export interface AuthDetails {
    isAuthenticated: boolean
    requiresReauthentication: boolean
    user: User | null
    pendingFlow: Flow | undefined
}

export const getAuthDetails = (auth: AllauthResponse | null | undefined): AuthDetails => {
    const meta = auth?.meta as AuthenticationMeta | undefined
    const isAuthenticated = !!auth && (auth?.status === 200 || (auth?.status === 401 && !!meta?.is_authenticated))
    const requiresReauthentication = !!(isAuthenticated && auth?.status === 401)
    const data = auth?.data as Authenticated | { flows?: Flow[]; user?: User } | undefined
    const pendingFlow = (data as { flows?: Flow[] })?.flows?.find((flow: Flow) => flow.is_pending)

    return {
        isAuthenticated,
        requiresReauthentication,
        user: isAuthenticated ? (data as Authenticated)?.user ?? null : null,
        pendingFlow
    }
}

export type BrowserFormAction = (action: string, data: Record<string, string>) => void

type RequestFn = (method: string, path: string, data?: unknown, headers?: Record<string, string>) => Promise<AllauthResponse>

export const createAPI = (
    request: RequestFn,
    browserFormAction?: BrowserFormAction
) => {
    return {
        getConfig: async (): Promise<ConfigurationResponse | ErrorResponse> =>
            await request('GET', apiURL.CONFIG) as ConfigurationResponse | ErrorResponse,

        session: {
            getStatus: async (): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
                await request('GET', apiURL.SESSION) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,

            list: async (): Promise<SessionListResponse | ErrorResponse> =>
                await request('GET', apiURL.SESSIONS) as SessionListResponse | ErrorResponse,

            logout: async (): Promise<AllauthResponse> =>
                await request('DELETE', apiURL.SESSION),

            remove: async (ids: number[]): Promise<AllauthResponse> =>
                await request('DELETE', apiURL.SESSIONS, { sessions: ids }),
        },

        account: {
            signup: async (data: SignupRequest): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
                await request('POST', apiURL.SIGNUP, data) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,

            login: async (data: LoginRequest): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
                await request('POST', apiURL.LOGIN, data) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,

            reauthenticate: async (data: ReauthenticateRequest): Promise<AuthenticatedResponse | ErrorResponse> =>
                await request('POST', apiURL.REAUTHENTICATE, data) as AuthenticatedResponse | ErrorResponse,

            emails: {
                list: async (): Promise<EmailListResponse | ErrorResponse> =>
                    await request('GET', apiURL.EMAIL) as EmailListResponse | ErrorResponse,

                add: async (email: string): Promise<EmailListResponse | ErrorResponse> =>
                    await request('POST', apiURL.EMAIL, { email }) as EmailListResponse | ErrorResponse,

                remove: async (email: string): Promise<EmailListResponse | ErrorResponse> =>
                    await request('DELETE', apiURL.EMAIL, { email }) as EmailListResponse | ErrorResponse,

                setPrimary: async (email: string): Promise<EmailListResponse | ErrorResponse> =>
                    await request('PATCH', apiURL.EMAIL, { email, primary: true }) as EmailListResponse | ErrorResponse,

                verification: {
                    dispatch: async (email: string): Promise<AllauthResponse> =>
                        await request('PUT', apiURL.EMAIL, { email }),

                    checkKey: async (key: string): Promise<EmailVerificationInfoResponse | ErrorResponse> =>
                        await request('GET', apiURL.VERIFY_EMAIL, undefined, { 'X-Email-Verification-Key': key }) as EmailVerificationInfoResponse | ErrorResponse,

                    confirmKey: async (key: string): Promise<AuthenticatedResponse | ErrorResponse> =>
                        await request('POST', apiURL.VERIFY_EMAIL, { key }) as AuthenticatedResponse | ErrorResponse,
                }
            },

            password: {
                set: async (data: ResetPasswordRequest): Promise<AuthenticatedResponse | ErrorResponse> =>
                    await request('POST', apiURL.RESET_PASSWORD, data) as AuthenticatedResponse | ErrorResponse,

                change: async (data: ChangePasswordRequest): Promise<AllauthResponse> =>
                    await request('POST', apiURL.CHANGE_PASSWORD, data),

                reset: {
                    dispatch: async (email: string): Promise<AllauthResponse> =>
                        await request('POST', apiURL.REQUEST_PASSWORD_RESET, { email }),

                    checkKey: async (key: string): Promise<AllauthResponse> =>
                        await request('GET', apiURL.RESET_PASSWORD, undefined, { 'X-Password-Reset-Key': key }),
                }
            }
        },

        loginCodes: {
            request: async (email: string): Promise<AuthenticationRequiredResponse | ErrorResponse> =>
                await request('POST', apiURL.REQUEST_LOGIN_CODE, { email }) as AuthenticationRequiredResponse | ErrorResponse,

            confirm: async (code: string): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
                await request('POST', apiURL.CONFIRM_LOGIN_CODE, { code }) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
        },

        oauth: {
            list: async (): Promise<ProviderAccountListResponse | ErrorResponse> =>
                await request('GET', apiURL.PROVIDERS) as ProviderAccountListResponse | ErrorResponse,

            signup: async (data: ProviderSignupRequest): Promise<AuthenticatedResponse | ErrorResponse> =>
                await request('POST', apiURL.PROVIDER_SIGNUP, data) as AuthenticatedResponse | ErrorResponse,

            provider: (providerID: string) => {
                const buildAuths = (processType: string) => {
                    return {
                        withToken: async (token: string): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
                            await request(
                                'POST',
                                apiURL.PROVIDER_TOKEN,
                                {
                                    provider: providerID,
                                    process: processType,
                                    token: token,
                                }
                            ) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,

                        withRedirect: (endpoint: string): void => {
                            if (browserFormAction) {
                                if (!process.env.NEXT_PUBLIC_HOST_URL) {
                                    throw new Error('NEXT_PUBLIC_HOST_URL environment variable is not set. OAuth redirects require this to be set at build time.')
                                }
                                browserFormAction(
                                    apiURL.REDIRECT_TO_PROVIDER,
                                    {
                                        provider: providerID,
                                        process: processType,
                                        callback_url: new URL(`${process.env.NEXT_PUBLIC_HOST_URL}/${endpoint}`).toString(),
                                    }
                                )
                            }
                        }
                    }
                }

                return {
                    removeFrom: async (accountUID: string): Promise<ProviderAccountListResponse | ErrorResponse> =>
                        await request('DELETE', apiURL.PROVIDERS, { provider: providerID, account: accountUID }) as ProviderAccountListResponse | ErrorResponse,

                    login: buildAuths(OAuthProcess.LOGIN),
                    connect: buildAuths(OAuthProcess.CONNECT),
                }
            }
        },

        mfa: {
            list: async (): Promise<AuthenticatorListResponse | ErrorResponse> =>
                await request('GET', apiURL.AUTHENTICATORS) as AuthenticatorListResponse | ErrorResponse,

            authenticate: async (code: string): Promise<AuthenticatedResponse | ErrorResponse> =>
                await request('POST', apiURL.MFA_AUTHENTICATE, { code } as MFAAuthenticateRequest) as AuthenticatedResponse | ErrorResponse,

            reauthenticate: async (code: string): Promise<AuthenticatedResponse | ErrorResponse> =>
                await request('POST', apiURL.MFA_REAUTHENTICATE, { code } as MFAAuthenticateRequest) as AuthenticatedResponse | ErrorResponse,

            trust: async (trust: boolean): Promise<AllauthResponse> =>
                await request('POST', apiURL.MFA_TRUST, { trust }),

            totp: {
                getStatus: async (): Promise<TOTPStatusResponse | ErrorResponse> =>
                    await request('GET', apiURL.TOTP_AUTHENTICATOR) as TOTPStatusResponse | ErrorResponse,

                activate: async (code: string): Promise<TOTPStatusResponse | ErrorResponse> =>
                    await request('POST', apiURL.TOTP_AUTHENTICATOR, { code }) as TOTPStatusResponse | ErrorResponse,

                deactivate: async (): Promise<AllauthResponse> =>
                    await request('DELETE', apiURL.TOTP_AUTHENTICATOR),
            },

            recoveryCodes: {
                list: async (): Promise<RecoveryCodesResponse | ErrorResponse> =>
                    await request('GET', apiURL.RECOVERY_CODES) as RecoveryCodesResponse | ErrorResponse,

                regenerate: async (): Promise<RecoveryCodesResponse | ErrorResponse> =>
                    await request('POST', apiURL.RECOVERY_CODES) as RecoveryCodesResponse | ErrorResponse,
            }
        },

        webauthn: {
            signup: async (name: string, credential: RegistrationCredential): Promise<AuthenticatedResponse | ErrorResponse> =>
                await request('PUT', apiURL.SIGNUP_WEBAUTHN, { name, credential }) as AuthenticatedResponse | ErrorResponse,

            add: async (name: string, credential: RegistrationCredential): Promise<AllauthResponse> =>
                await request('POST', apiURL.WEBAUTHN_AUTHENTICATOR, { name, credential }),

            login: async (credential: AuthenticationCredential): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
                await request('POST', apiURL.LOGIN_WEBAUTHN, { credential }) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,

            authenticate: async (credential: AuthenticationCredential): Promise<AuthenticatedResponse | ErrorResponse> =>
                await request('POST', apiURL.AUTHENTICATE_WEBAUTHN, { credential }) as AuthenticatedResponse | ErrorResponse,

            reauthenticate: async (credential: AuthenticationCredential): Promise<AuthenticatedResponse | ErrorResponse> =>
                await request('POST', apiURL.REAUTHENTICATE_WEBAUTHN, { credential }) as AuthenticatedResponse | ErrorResponse,

            update: async (id: number, data: Omit<WebAuthnUpdateRequest, 'id'>): Promise<AllauthResponse> =>
                await request('PUT', apiURL.WEBAUTHN_AUTHENTICATOR, { id, ...data }),

            delete: async (ids: number[]): Promise<AllauthResponse> =>
                await request('DELETE', apiURL.WEBAUTHN_AUTHENTICATOR, { authenticators: ids }),

            passkey: {
                signup: async (email: string): Promise<AllauthResponse> =>
                    await request('POST', apiURL.SIGNUP_WEBAUTHN, { email }),

                confirm: async (): Promise<AuthenticatedResponse | ErrorResponse> =>
                    await request('PUT', apiURL.SIGNUP_WEBAUTHN) as AuthenticatedResponse | ErrorResponse,
            },

            requestOptions: {
                creation: async (passwordless: boolean): Promise<WebAuthnCreationOptionsResponse | ErrorResponse> =>
                    await request('GET', apiURL.WEBAUTHN_AUTHENTICATOR + (passwordless ? '?passwordless' : '')) as WebAuthnCreationOptionsResponse | ErrorResponse,

                creationAtSignup: async (): Promise<WebAuthnCreationOptionsResponse | ErrorResponse> =>
                    await request('GET', apiURL.SIGNUP_WEBAUTHN) as WebAuthnCreationOptionsResponse | ErrorResponse,

                login: async (): Promise<WebAuthnRequestOptionsResponse | ErrorResponse> =>
                    await request('GET', apiURL.LOGIN_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse,

                authentication: async (): Promise<WebAuthnRequestOptionsResponse | ErrorResponse> =>
                    await request('GET', apiURL.AUTHENTICATE_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse,

                reauthentication: async (): Promise<WebAuthnRequestOptionsResponse | ErrorResponse> =>
                    await request('GET', apiURL.REAUTHENTICATE_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse,
            },
        }
    }
}

export type AllauthAPI = ReturnType<typeof createAPI>
