// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { AmplifyContext } from '@aws-amplify/core';
import { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils';

import { AuthValidationErrorCode } from '../../../errors/types/validation';
import { assertServiceError } from '../../../errors/utils/assertServiceError';
import { assertValidationError } from '../../../errors/utils/assertValidationError';
import {
	ChallengeName,
	ChallengeParameters,
} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types';
import {
	getActiveSignInUsername,
	getSignInResult,
	getSignInResultFromError,
	handleUserPasswordAuthFlow,
} from '../utils/signInHelpers';
import { InitiateAuthException } from '../types/errors';
import {
	CognitoAuthSignInDetails,
	SignInWithUserPasswordInput,
	SignInWithUserPasswordOutput,
} from '../types';
import {
	resetActiveSignInState,
	setActiveSignInState,
} from '../../../client/utils/store/signInStore';
import { cacheCognitoTokens } from '../tokenProvider/cacheTokens';
import { resolveTokenOrchestrator } from '../tokenProvider';
import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent';
import { retryOnResourceNotFoundException } from '../utils/retryOnResourceNotFoundException';
import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata';

import { resetAutoSignIn } from './autoSignIn';

/**
 * Signs a user in using USER_PASSWORD_AUTH AuthFlowType
 *
 * @param ctx - The AmplifyContext
 * @param input - The SignInWithUserPasswordInput object
 * @returns SignInWithUserPasswordOutput
 * @throws service: {@link InitiateAuthException } - Cognito service error thrown during the sign-in process.
 * @throws validation: {@link AuthValidationErrorCode  } - Validation errors thrown when either username or password
 *  are not defined.
 * @throws AuthTokenConfigException - Thrown when the token provider config is invalid.
 */
export async function signInWithUserPassword(
	ctx: AmplifyContext,
	input: SignInWithUserPasswordInput,
): Promise<SignInWithUserPasswordOutput> {
	const { username, password, options } = input;
	const authConfig = ctx.resourcesConfig.Auth?.Cognito;
	const signInDetails: CognitoAuthSignInDetails = {
		loginId: username,
		authFlowType: 'USER_PASSWORD_AUTH',
	};
	assertTokenProviderConfig(authConfig);
	const metadata = options?.clientMetadata;
	assertValidationError(
		!!username,
		AuthValidationErrorCode.EmptySignInUsername,
	);
	assertValidationError(
		!!password,
		AuthValidationErrorCode.EmptySignInPassword,
	);
	// Resolve the per-context orchestrator ONCE at the entry point so every step
	// of the flow uses the context's configured orchestrator.
	const tokenOrchestrator = resolveTokenOrchestrator(ctx);

	try {
		const {
			ChallengeName: retiredChallengeName,
			ChallengeParameters: retriedChallengeParameters,
			AuthenticationResult,
			Session,
		} = await retryOnResourceNotFoundException(
			handleUserPasswordAuthFlow,
			[username, password, metadata, authConfig, tokenOrchestrator],
			username,
			tokenOrchestrator,
		);
		const activeUsername = getActiveSignInUsername(username);
		// sets up local state used during the sign-in process
		setActiveSignInState({
			signInSession: Session,
			username: activeUsername,
			challengeName: retiredChallengeName as ChallengeName,
			signInDetails,
		});
		if (AuthenticationResult) {
			await cacheCognitoTokens(
				{
					...AuthenticationResult,
					username: activeUsername,
					NewDeviceMetadata: await getNewDeviceMetadata({
						userPoolId: authConfig.userPoolId,
						userPoolEndpoint: authConfig.userPoolEndpoint,
						newDeviceMetadata: AuthenticationResult.NewDeviceMetadata,
						accessToken: AuthenticationResult.AccessToken,
					}),
					signInDetails,
				},
				tokenOrchestrator,
			);
			resetActiveSignInState();

			await dispatchSignedInHubEvent(ctx);

			resetAutoSignIn();

			return {
				isSignedIn: true,
				nextStep: { signInStep: 'DONE' },
			};
		}

		return getSignInResult(ctx, {
			challengeName: retiredChallengeName as ChallengeName,
			challengeParameters: retriedChallengeParameters as ChallengeParameters,
		});
	} catch (error) {
		resetActiveSignInState();
		resetAutoSignIn();
		assertServiceError(error);
		const result = getSignInResultFromError(error.name);
		if (result) return result;
		throw error;
	}
}
