/**
 * Auth Resource
 *
 * Handles authentication with the privacy system.
 */
import { PrivacyBoostError } from '../errors';
import { createChainClient } from '../core/chains';
import { silentLogger } from '../utils/logger';
/**
 * Auth Resource for authentication operations
 */
const TAG = 'Auth';
export class AuthResource {
    config;
    walletBridge = null;
    onLoginComplete = null;
    onJwtUpdate = null;
    stores;
    keyVault;
    authClient;
    log;
    _updatePublicClient = null;
    eventCleanups = [];
    _onAccountChange = null;
    _onChainChange = null;
    _onSessionRestored = null;
    _onSessionExpired = null;
    _skipAcquireJwt = false;
    constructor(config, stores, keyVault, authClient, logger) {
        this.config = config;
        this.stores = stores;
        this.keyVault = keyVault;
        this.authClient = authClient ?? null;
        this.log = logger ?? silentLogger;
    }
    get walletStore() {
        return this.stores.wallet;
    }
    setClientJwt(token) {
        this.onJwtUpdate?.(token);
    }
    wrapTokenProvider(tp) {
        if (!tp) {
            return {
                callback: null,
                wasUsed: () => false,
                getToken: () => null,
            };
        }
        let used = false;
        let token = null;
        return {
            callback: async (payload) => {
                used = true;
                const result = await tp(payload);
                token = result.token;
                return result;
            },
            wasUsed: () => used,
            getToken: () => token,
        };
    }
    /**
     * Set a callback to be called after successful login.
     * Used by PrivacyBoost to trigger balance sync.
     * @internal
     */
    setOnLoginComplete(callback) {
        this.onLoginComplete = callback;
    }
    /**
     * Set a callback to propagate JWT tokens to the parent SDK.
     * Called after successful wallet login so IndexerClient/AuthClient
     * can make authenticated HTTP requests.
     * @internal
     */
    setOnJwtUpdate(callback) {
        this.onJwtUpdate = callback;
    }
    /**
     * Switch the public client used by the wallet proxy's
     * `waitForTransactionReceipt` to point at the given chain.
     *
     * Must be called before performing vault operations on a different
     * chain than the one used during initial `authenticate()`.
     */
    switchReceiptChain(chainId, rpcUrl) {
        this._updatePublicClient?.(chainId, rpcUrl);
    }
    /**
     * Set a callback for when the wallet account changes.
     * The handler receives the new list of accounts (empty = disconnected).
     * By default, AuthResource clears auth state and calls this handler.
     */
    /**
     * Skip parent-side JWT acquisition. In iframe mode, the WASM inside the
     * iframe manages its own JWT. The proxy routes requests through the iframe
     * which adds auth automatically, so the parent doesn't need its own JWT.
     * @internal
     */
    setSkipAcquireJwt(skip) {
        this._skipAcquireJwt = skip;
    }
    /** @internal */
    setOnAccountChange(handler) {
        this._onAccountChange = handler;
    }
    /**
     * Set a callback for when the wallet chain changes.
     * @internal
     */
    setOnChainChange(handler) {
        this._onChainChange = handler;
    }
    /** @internal */
    setOnSessionRestored(handler) {
        this._onSessionRestored = handler;
    }
    /** @internal */
    setOnSessionExpired(handler) {
        this._onSessionExpired = handler;
    }
    /**
     * Subscribe to wallet provider events after authentication.
     * On account change: clears auth state (protocol keys are wallet-specific).
     * On chain change: notifies via callback for upstream handling.
     */
    subscribeToWalletEvents(adapter) {
        this.cleanupEventSubscriptions();
        if (adapter.onAccountsChanged) {
            const unsubscribe = adapter.onAccountsChanged(async (accounts) => {
                try {
                    this.log.warn(TAG, 'Wallet account changed, clearing auth state');
                    this.cleanupEventSubscriptions();
                    try {
                        await this.keyVault.logout();
                    }
                    catch {
                        // best-effort
                    }
                    this.walletStore.getState().reset();
                    this.setClientJwt(null);
                    this.walletBridge = null;
                    this._onSessionExpired?.({ reason: 'account_changed' });
                    this._onAccountChange?.(accounts);
                }
                catch (error) {
                    this.log.error(TAG, 'Error handling account change', error);
                }
            });
            this.eventCleanups.push(unsubscribe);
        }
        if (adapter.onChainChanged) {
            const unsubscribe = adapter.onChainChanged((chainId) => {
                this.log.warn(TAG, `Wallet chain changed to ${chainId}`);
                this._onChainChange?.(chainId);
            });
            this.eventCleanups.push(unsubscribe);
        }
    }
    cleanupEventSubscriptions() {
        for (const cleanup of this.eventCleanups) {
            cleanup();
        }
        this.eventCleanups = [];
    }
    /**
     * Logout: end session completely, clear all state.
     * After this, the user must re-authenticate from scratch.
     */
    async logout() {
        try {
            this.cleanupEventSubscriptions();
            if (this.walletBridge) {
                await this.walletBridge.disconnect();
            }
            await this.keyVault.logout();
            this.walletStore.getState().reset();
            this.setClientJwt(null);
            this.walletBridge = null;
        }
        catch (error) {
            throw PrivacyBoostError.from(error);
        }
    }
    /**
     * Authenticate with the privacy system using a two-phase pattern.
     *
     * This is the recommended way to authenticate. It handles wallet connection,
     * key derivation, and server login in a single call.
     *
     * Returns either:
     * - `{ status: "authenticated", privacyAddress, mpk }` — fully authenticated
     * - `{ status: "credentialRequired", action, unlockType, submit }` — a PIN or password is needed
     * - `{ status: "recoveryRequired", reason, recover }` — vault is corrupted, needs mnemonic recovery
     *
     * When `credentialRequired` is returned, call `result.submit(credential)` to complete authentication.
     *
     * @param adapter - Wallet bridge implementation (see {@link AuthWalletBridge})
     * @param keySource - Optional key source for key derivation (mnemonic, rawSeed, or walletDerived)
     * @param tokenProvider - Optional callback to obtain a JWT from the login payload
     * @returns AuthResult discriminated union
     *
     * @example
     * ```typescript
     * const result = await sdk.auth.authenticate(walletBridge);
     *
     * if (result.status === 'authenticated') {
     *   console.log('Logged in:', result.privacyAddress);
     * } else {
     *   // Prompt user for PIN/password
     *   const pin = await promptUser(result.action, result.unlockType);
     *   const authenticated = await result.submit(pin);
     *   console.log('Logged in:', authenticated.privacyAddress);
     * }
     * ```
     */
    async authenticate(adapter, options) {
        // Check if a persisted session exists so we can emit sessionRestored
        // after WASM auth completes. The actual restore is handled by the WASM
        // layer (restore basic JWT → skip wallet popup → upgrade to full JWT).
        const hadStoredSession = this.config.persistence
            ? (await this.keyVault.getStoredSession()) !== null
            : false;
        this.walletBridge = adapter;
        const { keySource, tokenProvider } = options ?? {};
        const wrappedTokenProvider = this.wrapTokenProvider(tokenProvider);
        // Register wallet adapter with KeyVault for iframe wallet bridge
        this.keyVault.setWalletBridge({
            signMessage: (msg) => adapter.signMessage(msg),
            signTypedData: (data) => adapter.signTypedData(data),
            sendTransaction: (tx) => adapter.sendTransaction(tx),
            getAddress: () => adapter.getAddress(),
            getChainId: () => adapter.getChainId(),
        });
        // Create an initial public client for the root chain. ChainClients that
        // operate on a different chain will call updatePublicClient() to swap in
        // a client pointing at the correct RPC so that waitForTransactionReceipt
        // queries the right chain.
        let activePublicClient = createChainClient(this.config.chainId, this.config.rpcUrl);
        const walletProxy = {
            signMessage: async (message) => adapter.signMessage(message),
            signTypedData: async (typedData) => adapter.signTypedData(typedData),
            sendTransaction: async (tx) => adapter.sendTransaction(tx),
            getAddress: () => adapter.getAddress(),
            getChainId: () => adapter.getChainId(),
            waitForTransactionReceipt: async (txHash) => {
                const receipt = await activePublicClient.waitForTransactionReceipt({
                    hash: txHash,
                });
                return {
                    transactionHash: receipt.transactionHash,
                    status: receipt.status === 'success',
                    logs: receipt.logs.map((log) => ({
                        address: log.address,
                        topics: [...log.topics],
                        data: log.data,
                    })),
                };
            },
            /** Swap the public client used by waitForTransactionReceipt. */
            updatePublicClient: (chainId, rpcUrl) => {
                activePublicClient = createChainClient(chainId, rpcUrl);
            },
        };
        // Store the updater so ChainClients can swap the public client
        this._updatePublicClient = walletProxy.updatePublicClient;
        // Suppress unused variable — walletProxy is used by local-mode WASM
        // authentication but we keep building it so switchReceiptChain works.
        void walletProxy;
        let raw;
        try {
            raw = await this.keyVault.authenticate(keySource ?? null, wrappedTokenProvider.callback);
        }
        catch (error) {
            throw PrivacyBoostError.from(error);
        }
        if (raw.status === 'authenticated') {
            const result = {
                status: 'authenticated',
                privacyAddress: raw.privacyAddress,
                mpk: raw.mpk,
                isNewUser: raw.isNewUser,
                chainRegistrations: raw.chainRegistrations,
            };
            // Update store
            const walletStore = this.walletStore.getState();
            const address = await adapter.getAddress();
            walletStore.setConnected(true, address);
            walletStore.setAuthenticated(true, result.privacyAddress, result.mpk);
            // Custom token providers already emitted a JWT via wrapTokenProvider().
            if (wrappedTokenProvider.wasUsed()) {
                this.setClientJwt(wrappedTokenProvider.getToken());
            }
            else {
                await this.acquireJwt(adapter);
            }
            this.subscribeToWalletEvents(adapter);
            // Call post-login callback (e.g., to sync balances)
            if (this.onLoginComplete) {
                try {
                    await this.onLoginComplete();
                }
                catch (error) {
                    this.log.warn(TAG, 'onLoginComplete callback failed', error);
                }
            }
            // Emit sessionRestored if the WASM path used a persisted basic JWT
            if (hadStoredSession) {
                this._onSessionRestored?.({
                    address,
                    privacyAddress: result.privacyAddress,
                });
            }
            return result;
        }
        if (raw.status === 'mnemonicGenerated') {
            const mnemonicResult = {
                status: 'mnemonicGenerated',
                mnemonic: raw.mnemonic,
                proceed: async (credential) => {
                    let proceedRaw;
                    try {
                        proceedRaw = await this.keyVault.proceedAfterMnemonic(credential ?? null, wrappedTokenProvider.callback);
                    }
                    catch (error) {
                        throw PrivacyBoostError.from(error);
                    }
                    const authenticated = {
                        status: 'authenticated',
                        privacyAddress: proceedRaw.privacyAddress,
                        mpk: proceedRaw.mpk,
                        isNewUser: proceedRaw.isNewUser,
                        chainRegistrations: proceedRaw.chainRegistrations,
                    };
                    // Update store
                    const walletStore = this.walletStore.getState();
                    const addr = await adapter.getAddress();
                    walletStore.setConnected(true, addr);
                    walletStore.setAuthenticated(true, authenticated.privacyAddress, authenticated.mpk);
                    if (wrappedTokenProvider.wasUsed()) {
                        this.setClientJwt(wrappedTokenProvider.getToken());
                    }
                    else {
                        await this.acquireJwt(adapter);
                    }
                    this.subscribeToWalletEvents(adapter);
                    // Clear sensitive mnemonic from the result object
                    mnemonicResult.mnemonic = '';
                    if (this.onLoginComplete) {
                        try {
                            await this.onLoginComplete();
                        }
                        catch (error) {
                            this.log.warn(TAG, 'onLoginComplete callback failed', error);
                        }
                    }
                    return authenticated;
                },
            };
            // Update store with connected state
            const walletStore = this.walletStore.getState();
            const address = await adapter.getAddress();
            walletStore.setConnected(true, address);
            return mnemonicResult;
        }
        if (raw.status === 'recoveryRequired') {
            const recoveryResult = {
                status: 'recoveryRequired',
                reason: raw.reason ?? 'Vault decryption failed',
                recover: this.buildRecoveryClosure(adapter, options),
            };
            return recoveryResult;
        }
        // credentialRequired — return with submit() closure.
        // The submit() function can be called multiple times if the credential is wrong
        // (e.g., incorrect PIN). It will throw an error on failure but remains callable
        // for retry without re-running authenticate().
        const credentialResult = {
            status: 'credentialRequired',
            action: raw.action,
            unlockType: raw.unlockType,
            submit: async (credential) => {
                let loginRaw;
                try {
                    loginRaw = await this.keyVault.submitCredential(credential, wrappedTokenProvider.callback);
                }
                catch (error) {
                    throw PrivacyBoostError.from(error);
                }
                const authenticated = {
                    status: 'authenticated',
                    privacyAddress: loginRaw.privacyAddress,
                    mpk: loginRaw.mpk,
                    isNewUser: loginRaw.isNewUser,
                    chainRegistrations: loginRaw.chainRegistrations,
                };
                // Update store
                const walletStore = this.walletStore.getState();
                const addr = await adapter.getAddress();
                walletStore.setConnected(true, addr);
                walletStore.setAuthenticated(true, authenticated.privacyAddress, authenticated.mpk);
                if (wrappedTokenProvider.wasUsed()) {
                    this.setClientJwt(wrappedTokenProvider.getToken());
                }
                else {
                    await this.acquireJwt(adapter);
                }
                this.subscribeToWalletEvents(adapter);
                if (this.onLoginComplete) {
                    try {
                        await this.onLoginComplete();
                    }
                    catch (error) {
                        this.log.warn(TAG, 'onLoginComplete callback failed', error);
                    }
                }
                return authenticated;
            },
        };
        // Update store with connected state (wallet is connected even if credential needed)
        const walletStore = this.walletStore.getState();
        const address = await adapter.getAddress();
        walletStore.setConnected(true, address);
        return credentialResult;
    }
    buildRecoveryClosure(adapter, options) {
        return async (mnemonic) => {
            const result = await this.authenticate(adapter, {
                ...options,
                keySource: { type: 'mnemonic', phrase: mnemonic },
            });
            if (result.status !== 'authenticated') {
                throw new PrivacyBoostError('RECOVERY_FAILED', `Mnemonic recovery did not complete authentication (status: ${result.status})`);
            }
            return result;
        };
    }
    /**
     * Acquire a JWT for the TypeScript-side HTTP clients by performing
     * a wallet login + optional privacy-key upgrade via AuthClient.
     * This runs independently of the KeyVault auth flow.
     */
    async acquireJwt(adapter) {
        if (this._skipAcquireJwt)
            return;
        if (!this.authClient || !this.onJwtUpdate)
            return;
        try {
            const address = await adapter.getAddress();
            const chainId = await adapter.getChainId();
            const mpk = this.keyVault.getMpk();
            if (mpk) {
                // Combined flow: fetch two challenges in parallel, bundle ViewAuth into login
                const [loginChallenge, upgradeChallenge] = await Promise.all([
                    this.authClient.getChallenge(),
                    this.authClient.getChallenge(),
                ]);
                const loginMessage = `Login to PrivacyBoost\nApp: ${this.config.appId}\nWallet: ${address}\nNonce: ${loginChallenge.nonce}`;
                const signature = await adapter.signMessage(loginMessage);
                const viewAuth = await this.keyVault.signViewAuth(mpk, upgradeChallenge.nonce);
                const publicKeys = await this.keyVault.getPublicKeys();
                const regData = await this.keyVault.getRegistrationData();
                const loginResult = await this.authClient.walletLogin({
                    address,
                    signature,
                    nonce: loginChallenge.nonce,
                    nonceHmac: loginChallenge.nonceHmac,
                    chainId,
                    appId: this.config.appId,
                    viewauthSignature: viewAuth.signature,
                    mpk,
                    upgradeNonce: upgradeChallenge.nonce,
                    upgradeNonceHmac: upgradeChallenge.nonceHmac,
                    accountId: publicKeys.accountId,
                    nullifyingKey: publicKeys.nullifyingPublicKey.x,
                    authPkX: regData.authPkX,
                    authPkY: regData.authPkY,
                });
                let token = loginResult.token;
                // If server didn't return full tier (e.g. new user), fall back to the
                // separate /auth/upgrade call. The upgrade nonce from upgradeChallenge is
                // safe to reuse here: the server only consumes it when canUpgrade is true,
                // which wasn't the case for this response.
                if (loginResult.tier !== 'full') {
                    try {
                        const upgradeResult = await this.authClient.authUpgrade({
                            mpk,
                            viewauthSignature: viewAuth.signature,
                            nonce: upgradeChallenge.nonce,
                            accountId: publicKeys.accountId,
                            viewingPublicKey: viewAuth.viewingPublicKey,
                            nullifyingKey: publicKeys.nullifyingPublicKey.x,
                            walletAddress: address,
                            authPkX: regData.authPkX,
                            authPkY: regData.authPkY,
                        });
                        token = upgradeResult.token;
                    }
                    catch (error) {
                        this.log.warn(TAG, 'JWT upgrade with privacy keys failed, using basic token', error);
                    }
                }
                this.setClientJwt(token);
            }
            else {
                // No keys available — basic login only
                const { nonce: loginNonce } = await this.authClient.getChallenge();
                const loginMessage = `Login to PrivacyBoost\nApp: ${this.config.appId}\nWallet: ${address}\nNonce: ${loginNonce}`;
                const signature = await adapter.signMessage(loginMessage);
                const loginResult = await this.authClient.walletLogin({
                    address,
                    signature,
                    nonce: loginNonce,
                    chainId,
                    appId: this.config.appId,
                });
                this.setClientJwt(loginResult.token);
            }
        }
        catch (error) {
            this.log.warn(TAG, 'Failed to acquire JWT for HTTP clients', error);
        }
    }
    /**
     * Clear session: clear JWT only, keep keys for quick re-auth.
     */
    async clearSession() {
        try {
            await this.keyVault.clearSession();
        }
        catch (error) {
            throw PrivacyBoostError.from(error);
        }
        // Clear the TypeScript-side JWT
        this.setClientJwt(null);
        const walletStore = this.walletStore.getState();
        walletStore.setAuthenticated(false);
    }
    /**
     * Get the current wallet bridge (for internal use by other resources).
     * Returns null if no wallet is connected.
     * @internal
     */
    getWalletBridge() {
        return this.walletBridge;
    }
    /**
     * Check if wallet is connected.
     * Uses the wallet store rather than wasm.isConnected because in iframe
     * mode the parent's WASM instance does not track wallet state.
     */
    isConnected() {
        return this.walletStore.getState().isConnected;
    }
    /**
     * Check if user is authenticated
     */
    isAuthenticated() {
        return this.keyVault.isAuthenticated();
    }
    /**
     * Get the user's privacy address
     */
    getPrivacyAddress() {
        return this.keyVault.getPrivacyAddress();
    }
    /**
     * Get the user's MPK (master public key)
     */
    getMpk() {
        return this.keyVault.getMpk();
    }
    /**
     * Permanently delete the server vault and clear local state.
     * Requires wallet signature for ownership proof.
     */
    async deleteVault() {
        try {
            await this.keyVault.deleteVault();
            await this.logout();
        }
        catch (error) {
            throw PrivacyBoostError.from(error);
        }
    }
    // NOTE: getMnemonic() is intentionally NOT exposed here.
    // Mnemonic retrieval is only available inside the secure iframe host
    // (via wasm.getMnemonic()) to prevent the parent page from accessing
    // the recovery phrase.
    /**
     * Get session info from the server.
     */
    async getSessionInfo() {
        if (!this.authClient) {
            return null;
        }
        try {
            return await this.authClient.getSessionInfo();
        }
        catch (error) {
            const wrapped = PrivacyBoostError.from(error);
            this.log.error(TAG, `Failed to get session info: ${wrapped.code}`);
            return null;
        }
    }
    /**
     * Clean up event subscriptions without a full logout.
     * Called by PrivacyBoost.dispose() to prevent leaked listeners.
     */
    dispose() {
        this.cleanupEventSubscriptions();
    }
}
//# sourceMappingURL=Auth.js.map