import { AxiosInstance } from 'axios'
import queryString from 'query-string'

import {
	User,
	Roles,
	Wallet,
	NewUser,
	Organizer,
	UserInvite,
	UserUpdate,
	OrganizerId,
	NewOrganizer,
	EnrollmentId,
	SeatsWorkspace,
	PublicUserData,
	UserPermissions,
	UserPermissionChange,
	Email,
	MarketRuleset,
	Fee,
	FeeUpdate,
	FeeCreate,
	MarketRulesetCreate,
	MarketRulesetUpdate,
	MarketRulesetListQuery,
	GetOrganizerRequest,
	UserQuery,
	KYCLevel,
	ListQuery,
	Currency,
	ListUserQuery,
	OnboardStripePayload,
	OnboardStripeResponse,
	OnboardStripeStatus,
	OrganizerDomain,
	OrganizerDomainCreate,
	OrganizerDomainUpdate,
	OrganizerDomainQuery,
	NewsletterUpdate,
	Newsletter,
	NewsletterCreate,
} from './types'
import {
	HealthStatus,
	IdParam,
	QRCodePayload,
	StatusResponse,
	StringIdParam,
} from '../common/types'
import { getStringifiedQuery } from '../common/query'

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

	/**
	 * Returns true if the service is reachable
	 *
	 * @returns Services' online status
	 */
	async health(): Promise<HealthStatus> {
		try {
			const res = await this.client.get(`account/health`)
			if (res.data.status === 'ok') {
				return { online: true }
			}
		} catch (e) {
			// Do nothing
		}

		return { online: false }
	}

	/**
	 * Returns the organizer with the given name or id.
	 * Currently an organizer only has one workspace.
	 *
	 * @param req name of the organizer
	 * @returns Seats workspaces for the organizer
	 * @throws `NotFoundError`
	 */
	async getSeatsWorkspacesForOrganizer(
		req: OrganizerId
	): Promise<SeatsWorkspace[]> {
		const query = queryString.stringify(
			{
				organizer_name: req.id,
			},
			{ arrayFormat: 'comma' }
		)

		const res = await this.client.get(
			`account/${this.version}/seats-workspace?${query}`
		)

		return res.data.data
	}

	/**
	 * List all organizers
	 *
	 * @returns array of Organizer objects
	 */
	async listOrganizers(query: ListQuery = {}): Promise<Organizer[]> {
		const queryString = getStringifiedQuery(query)

		const res = await this.client.get(
			`account/${this.version}/organizer?${queryString}`
		)

		return res.data.data
	}

	/**
	 * Returns the organizer with the given name or id.
	 *
	 * @param req id or name of the organizer
	 * @returns Organizer object
	 * @throws `NotFoundError`
	 */
	async getOrganizer(req: GetOrganizerRequest): Promise<Organizer> {
		const query = queryString.stringify(
			{
				fields: req.fields,
			},
			{ arrayFormat: 'comma' }
		)

		const res = await this.client.get(
			`account/${this.version}/organizer/${req.id}?${query}`
		)

		return res.data.data
	}

	/**
	 * Create an organizer
	 *
	 * @param org New organizer data
	 * @returns Organizer object
	 * @throws `BadRequestError`
	 */
	async createOrganizer(org: NewOrganizer): Promise<Organizer> {
		const res = await this.client.post(`account/${this.version}/organizer`, org)

		return res.data.data
	}

	/**
	 * Updates an organizer
	 *
	 * @param id.id name or id of the organizer
	 * @param org updated organizer data
	 * @returns Organizer object
	 */
	async updateOrganizer(
		id: OrganizerId,
		org: Partial<Organizer>
	): Promise<Organizer> {
		const res = await this.client.patch(
			`account/${this.version}/organizer/${id.id}`,
			org
		)

		return res.data.data
	}

	/**
	 * Returns the user with the given id.
	 *
	 * @param req id of the user
	 * @returns User object
	 * @throws `NotFoundError`
	 */
	async getUser(req: IdParam): Promise<User> {
		const res = await this.client.get(`account/${this.version}/user/${req.id}`)

		return res.data.data
	}

	/**
	 * Returns the user with the given email.
	 *
	 * @param email email of the user
	 * @returns User object
	 * @throws `NotFoundError`
	 */
	async getUserByEmail(email: Email): Promise<PublicUserData> {
		const res = await this.client.get(
			`account/${this.version}/user/email/${email.email}`
		)

		return res.data.data
	}

	/**
	 * Creates a new user
	 *
	 * @param user  User data
	 * @returns User object
	 * @throws `BadRequestError`
	 */
	async createUser(user: NewUser): Promise<User> {
		const res = await this.client.post(`account/${this.version}/user`, user)

		return res.data.data
	}

	/**
	 * Creates and adds an user to an organizer
	 *
	 * @param user  User data and permissions
	 * @returns User object
	 * @throws `BadRequestError`
	 */
	async inviteUser(user: UserInvite): Promise<User> {
		if (user.grantedPermissions != null) {
			user.role = Roles.Custom
		}

		const res = await this.client.post(
			`account/${this.version}/user/invite`,
			user
		)

		return res.data.data
	}

	/**
	 * Update a user
	 * If a new email is sent, it needs to be confirmed
	 *
	 * @param user User data
	 * @returns User object
	 * @throws `BadRequestError`, `NotFoundError`
	 */
	async updateUser(id: IdParam, user: UserUpdate): Promise<User> {
		const res = await this.client.patch(
			`account/${this.version}/user/${id.id}`,
			user
		)

		return res.data.data
	}

	/**
	 * Initiate password reset process for a given email (i.e. user)
	 *
	 * @param email E-Mail of user which wants to reset the password
	 */
	async resetUserPassword(email: Email): Promise<StatusResponse> {
		const res = await this.client.post(
			`account/${this.version}/user/password/reset`,
			{ email: email.email }
		)

		return res.data.data
	}

	/**
	 * Resend the verification link to an unverified user.
	 *
	 * @param email Email of the user for which the verification link should be resent.
	 * @throws BadRequestError if the user is already verified.
	 */
	async resendUserVerification(email: Email): Promise<void> {
		await this.client.post(`account/${this.version}/user/verify/resend`, {
			email: email.email,
		})
	}
	/**
	 * Deletes a user
	 *
	 * @param id ID of the user to delete
	 * @throws `NotFoundError`
	 */
	async deleteUser(id: IdParam): Promise<void> {
		await this.client.delete(`account/${this.version}/user/${id.id}`)
	}

	/**
	 * List users belonging to a specific organizer.
	 *
	 * @param id.id Id or name of the organizer
	 * @param query Additional fields that should be fetched with the users.
	 */
	async listUsersInOrganization(
		id: OrganizerId,
		query: UserQuery = {}
	): Promise<User[]> {
		const queryString = getStringifiedQuery(query)

		const res = await this.client.get(
			`account/${this.version}/organizer/${id.id}/users?${queryString}`
		)

		return res.data.data
	}

	/**
	 * Use this method to list the users.
	 * @param query.email Query the users by the email
	 */
	async listUsers(query: ListUserQuery = {}): Promise<User[]> {
		const queryString = getStringifiedQuery(query)

		const res = await this.client.get(
			`account/${this.version}/user?${queryString}`
		)

		return res.data.data
	}

	/**
	 * Gets public info for a user with the enrollment id
	 *
	 * @param id enrollment ID of the user
	 * @throws `NotFoundError`
	 */
	async checkEnrollment(id: EnrollmentId): Promise<PublicUserData> {
		const res = await this.client.get(
			`account/${this.version}/user/enrollment/${id.enrollmentId}`
		)
		return res.data.data
	}

	/**
	 * Set a user's permissions.
	 * The caller need to have the `*.change_permission` permission for each class of permissions which are granted.
	 *
	 * @param id Id of the user
	 * @param perms Set of new permissions for the user
	 * @returns User object
	 * @throws `BadRequestError`, `NotFoundError`, `ForbiddenError`
	 */
	async setPermissions(
		id: IdParam,
		perms: UserPermissions
	): Promise<UserPermissionChange> {
		// Granted permissions object won't be used otherwise
		if (perms.grantedPermissions != null) {
			perms.role = Roles.Custom
		}

		const res = await this.client.patch(
			`account/${this.version}/user/${id.id}/permission`,
			perms
		)

		return res.data.data
	}

	/**
	 * Creates a new certificate and private key for a user.
	 * If there is no authorization, a new user and their wallet will be created.
	 *
	 * @returns User IDs, certificate and private key
	 */
	async createWallet(): Promise<Wallet> {
		const res = await this.client.post(
			`account/${this.version}/certificate/enroll`,
			{}
		)

		return res.data.data
	}

	/**
	 * Renews the certificate and private key for a user.
	 *
	 * @returns User IDs, certificate and private key
	 */
	async renewWallet(): Promise<Wallet> {
		const res = await this.client.post(
			`account/${this.version}/certificate/renew`,
			{}
		)

		return res.data.data
	}

	/**
	 * Return the market rules for the given ruleset
	 * @param marketRulesetId
	 * @returns
	 */
	async getMarketControlSettings(
		marketRulesetId: StringIdParam
	): Promise<MarketRuleset> {
		const res = await this.client.get(
			`account/${this.version}/market-control/${marketRulesetId.id}`
		)

		return res.data.data
	}

	/**
	 * Create a secondary market ruleset for an organizer
	 * @param marketRuleset Ruleset data
	 */
	async createMarketControlSettings(
		marketRuleset: MarketRulesetCreate
	): Promise<MarketRuleset> {
		const res = await this.client.post(
			`account/${this.version}/market-control`,
			marketRuleset
		)

		return res.data.data
	}

	/**
	 * Update a secondary market ruleset
	 * @param id Id of the ruleset
	 * @param marketRuleset Updated ruleset data
	 */
	async updateMarketControlSettings(
		id: StringIdParam,
		marketRuleset: MarketRulesetUpdate
	): Promise<MarketRuleset> {
		const res = await this.client.patch(
			`account/${this.version}/market-control/${id.id}`,
			marketRuleset
		)

		return res.data.data
	}

	/**
	 * Delete a secondary market ruleset
	 * @param id Id of the ruleset
	 */
	async deleteMarketControlSettings(id: StringIdParam): Promise<void> {
		await this.client.delete(`account/${this.version}/market-control/${id.id}`)
	}

	/**
	 * Fetch QR code data for publishing the ruleset on the chain
	 * @param id Id of the ruleset
	 */
	async getMarketControlSettingsQRCodePayload(
		id: StringIdParam
	): Promise<QRCodePayload> {
		const res = await this.client.get(
			`account/${this.version}/market-control/${id.id}/qr-payload`
		)

		return res.data.data
	}

	/**
	 * List the secondary market rulesets
	 */
	async listMarketControlSettings(
		query: MarketRulesetListQuery = {}
	): Promise<MarketRuleset[]> {
		const queryString = getStringifiedQuery(query)

		const res = await this.client.get(
			`account/${this.version}/market-control?${queryString}`
		)

		return res.data.data
	}

	/**
	 * Get fee by id
	 * @param id Id of the fee
	 */
	async getFee(id: IdParam): Promise<Fee> {
		const res = await this.client.get(`account/${this.version}/fee/${id.id}`)

		return res.data.data
	}

	/**
	 * Create a fee for the organizer. Requires administrative rights.
	 * @param fee Fee data
	 */
	async createFee(fee: FeeCreate): Promise<Fee> {
		const res = await this.client.post(`account/${this.version}/fee`, fee)

		return res.data.data
	}

	/**
	 * Update fee data. Requires administrative rights.
	 * @param id Id of the fee
	 * @param fee Updated fee data
	 */
	async updateFee(id: IdParam, fee: FeeUpdate): Promise<Fee> {
		const res = await this.client.patch(
			`account/${this.version}/fee/${id.id}`,
			fee
		)

		return res.data.data
	}

	/**
	 * List all registered KYC levels
	 */
	async listKycLevels(query: ListQuery = {}): Promise<KYCLevel[]> {
		const queryString = getStringifiedQuery(query)
		const res = await this.client.get(
			`account/${this.version}/kyc?${queryString}`
		)

		return res.data.data
	}

	/**
	 * List all registered currencies
	 */
	async listCurrencies(query: ListQuery = {}): Promise<Currency[]> {
		const queryString = getStringifiedQuery(query)
		const res = await this.client.get(
			`account/${this.version}/currency?${queryString}`
		)

		return res.data.data
	}

	/**
	 * Starts the onboarding process for the payment provider
	 *
	 * @param id.id name or id of the organizer
	 * @param onboardPayload return and refresh urls
	 * @returns redirect url
	 */
	async onboardStripeConnect(
		id: OrganizerId,
		onboardPayload: OnboardStripePayload
	): Promise<OnboardStripeResponse> {
		const res = await this.client.post(
			`account/${this.version}/organizer/${id.id}/onboard/stripe`,
			onboardPayload
		)

		return res.data.data
	}

	/**
	 * Gets the onboarding status for the payment provider
	 *
	 * @param id.id name or id of the organizer
	 * @returns data about the completeness of the onboarding process
	 */
	async getStripeConnectStatus(id: OrganizerId): Promise<OnboardStripeStatus> {
		const res = await this.client.get(
			`account/${this.version}/organizer/${id.id}/onboard/stripe`
		)

		return res.data.data
	}

	/**
	 * Get a domain by its ID
	 * @param id Id of the domain
	 */
	async getDomain(id: IdParam): Promise<OrganizerDomain> {
		const res = await this.client.get(`account/${this.version}/domain/${id.id}`)

		return res.data.data
	}

	/**
	 * Lists or searches for domains. Returns associated organizers for each domain.
	 */
	async listDomains(
		queryReq: OrganizerDomainQuery
	): Promise<OrganizerDomain[]> {
		const query = queryString.stringify(queryReq)
		const res = await this.client.get(`account/${this.version}/domain?${query}`)

		return res.data.data
	}

	/**
	 * Create a domain for the organizer. Requires permissions for that organizer.
	 * @param domain Domain name and either the organizer ID or the organizer name
	 */
	async createDomain(domain: OrganizerDomainCreate): Promise<OrganizerDomain> {
		const res = await this.client.post(`account/${this.version}/domain`, domain)

		return res.data.data
	}

	/**
	 * Update domain data. Requires permissions for that organizer.
	 * @param id Id of the domain
	 * @param domain Updated domain name
	 */
	async updateDomain(
		id: IdParam,
		domain: OrganizerDomainUpdate
	): Promise<OrganizerDomain> {
		const res = await this.client.patch(
			`account/${this.version}/domain/${id.id}`,
			domain
		)

		return res.data.data
	}

	/**
	 * Delete a domain. Requires permissions for that organizer.
	 * @param id Id of the domain
	 */
	async deleteDomain(id: IdParam): Promise<void> {
		await this.client.delete(`account/${this.version}/domain/${id.id}`)
	}

	/**
	 * Get a newsletter by its ID
	 */
	async getNewsletter(id: IdParam): Promise<Newsletter> {
		const res = await this.client.get(
			`account/${this.version}/newsletter/${id.id}`
		)

		return res.data.data
	}

	/**
	 * Lists all newsletters
	 */
	async listNewsletters(queryReq: OrganizerDomainQuery): Promise<Newsletter[]> {
		const query = queryString.stringify(queryReq)
		const res = await this.client.get(
			`account/${this.version}/newsletter?${query}`
		)

		return res.data.data
	}

	/**
	 * Create a newsletter. Requires permissions for the main organizer.
	 * @param newsletter Subject and content. It will be put in the `info` email template.
	 */
	async createNewsletter(newsletter: NewsletterCreate): Promise<Newsletter> {
		const res = await this.client.post(
			`account/${this.version}/newsletter`,
			newsletter
		)

		return res.data.data
	}

	/**
	 * Update newsletter content. Requires permissions for the main organizer.
	 * @param id ID of the newsletter
	 * @param newsletter Updated content
	 */
	async updateNewsletter(
		id: IdParam,
		newsletter: NewsletterUpdate
	): Promise<Newsletter> {
		const res = await this.client.patch(
			`account/${this.version}/newsletter/${id.id}`,
			newsletter
		)

		return res.data.data
	}

	/**
	 * Delete a newsletter. Requires permissions for the main organizer.
	 */
	async deleteNewsletter(id: IdParam): Promise<void> {
		await this.client.delete(`account/${this.version}/newsletter/${id.id}`)
	}

	/**
	 * Sends out the emails containing the newsletter content.
	 * Requires permissions for the main organizer.
	 * The newsletter will be marked as sent, but it can still be updated and resent.
	 *
	 * @param id ID of the newsletter to send
	 */
	async sendNewsletter(id: IdParam): Promise<Newsletter> {
		const res = await this.client.post(
			`account/${this.version}/newsletter/${id.id}/send`,
			{}
		)

		return res.data.data
	}
}
