import { AxiosInstance, AxiosResponse } from 'axios'

import {
	Jwt,
	Nonce,
	UserPassword,
	RefreshToken,
	RefreshableJwt,
	SignedWalletChallenge,
	WalletChallengeRequest,
	ServiceAccessData,
	ThirdPartyAuth,
	ThirdPartyAuthResponse,
	ImpersonateRequest,
} from './types'
import { KeyPair } from '..'
import { sign, extractEnrollment } from '../utils/certificate'

/**
 * Service class for account API calls.
 */
export class AuthService {
	constructor(readonly client: AxiosInstance, readonly version: string) {}

	/**
	 * Returns a JWT and its refresh token.
	 *
	 * @param refreshToken refresh token object
	 * @returns JWT and refresh token
	 * @throws `NotFoundError`, `UnauthorizedError`
	 */
	async refreshToken(refreshToken: RefreshToken): Promise<RefreshableJwt> {
		const res = await this.client.post(
			`account/${this.version}/auth/refresh`,
			refreshToken
		)

		return res.data.data
	}

	/**
	 * Returns a JWT and its refresh token.
	 * Enables accessing secured BAM endpoints.
	 * Username can also be the user's email.
	 *
	 * @param credentials username/email and password
	 * @returns JWT and refresh token
	 * @throws `NotFoundError`, `UnauthorizedError`
	 */
	async login(credentials: UserPassword): Promise<RefreshableJwt> {
		const res = await this.client.post(
			`account/${this.version}/auth/login`,
			credentials
		)

		return res.data.data
	}

	/**
	 * Returns a JWT for a newly-created anonymous user.
	 * This user has no permissions.
	 *
	 * @returns JWT for an generated user
	 */
	async guestLogin(): Promise<Jwt> {
		// Empty body because there has to be one for application/json
		const res = await this.client.post(
			`account/${this.version}/guest/login`,
			{}
		)

		return res.data.data
	}

	/**
	 * Returns a nonce which has to be signed with the user's wallet to obtain a JWT.
	 * Enables accessing secured BAM endpoints.
	 * After signing you need to call walletLogin.
	 *
	 * @param req enroll
	 * @returns nonce to sign
	 * @throws `BadRequestError`
	 */
	async getWalletChallenge(req: WalletChallengeRequest): Promise<Nonce> {
		const res = await this.client.post(
			`account/${this.version}/auth/challenge`,
			req
		)

		return res.data.data
	}

	/**
	 * Returns a JWT and its refresh token.
	 * Enables accessing secured BAM endpoints.
	 * Requires the device ID header to be set.
	 *
	 * @param signedChallenge contains a nonce signed by the users wallet
	 * @returns JWT and refresh token
	 * @throws `NotFoundError`, `BadRequestError`
	 */
	async walletLogin(
		signedChallenge: SignedWalletChallenge
	): Promise<RefreshableJwt> {
		const res = await this.client.post(
			`account/${this.version}/auth/wallet`,
			signedChallenge
		)

		return res.data.data
	}

	/**
	 * Returns a JWT and its refresh token.
	 * Enables accessing secured BAM endpoints.
	 *
	 * @param keyPair contains a nonce signed by the users wallet
	 * @returns JWT and refresh token
	 * @throws `NotFoundError`, `BadRequestError`
	 */
	async loginWithCertificate(
		wallet: KeyPair,
		organizerId?: string
	): Promise<RefreshableJwt> {
		// Get a challenge for the user
		const challengeRequest = {
			clientNonce: Date.now().toString(),
			enrollmentId: extractEnrollment(wallet.certificate),
		}
		const challenge = await this.getWalletChallenge(challengeRequest)

		// Sign the challenge
		const signedChallenge = {
			clientNonce: challengeRequest.clientNonce,
			nonce: challenge.nonce,
			signedNonce: sign(challenge.nonce, wallet.privateKey),
			organizerId,
		}
		// Login
		return this.walletLogin(signedChallenge)
	}

	/**
	 * Returns a JWT for a service.
	 * Enables accessing secured BAM endpoints.
	 *
	 * @param credentials service name, org and password
	 * @returns JWT
	 * @throws `NotFoundError`, `UnauthorizedError`
	 */
	async serviceLogin(credentials: ServiceAccessData): Promise<Jwt> {
		const res = await this.client.post(
			`account/${this.version}/auth/service`,
			credentials
		)
		return res.data.data
	}

	/**
	 * Returns a JWT for a service.
	 * Enables external login via OAuth.
	 * Token is provided by the third party.
	 *
	 * @param req User data for login
	 * @returns JWT and refresh token
	 */
	async externalLogin(req: ThirdPartyAuth): Promise<ThirdPartyAuthResponse> {
		const res = await this.client.post(
			`account/${this.version}/auth/external`,
			req
		)
		return res.data.data
	}

	/**
	 * Returns an impersonated JWT, if you have the permissions to get it.
	 * If you specify an organizerId, the requested JWT will contain the permissions
	 * that the userId user has for that organizerId
	 *
	 * @param req Impersonated user data
	 * @param adminCredentials if provided, this token will be used for the request
	 * @returns Impersonation JWT
	 */
	async impersonate(
		req: ImpersonateRequest,
		adminCredentials?: Jwt
	): Promise<Jwt> {
		let res: AxiosResponse
		if (adminCredentials) {
			res = await this.client.post(
				`account/${this.version}/auth/impersonate`,
				req,
				{
					headers: {
						Authorization: `Bearer ${adminCredentials.token}`,
					},
				}
			)
		} else {
			res = await this.client.post(
				`account/${this.version}/auth/impersonate`,
				req
			)
		}
		return res.data.data
	}
}
