import {
  AuthStateSignup,
  ConstructorOpts,
  ParaCore,
  Environment,
  PlatformUtils,
  EventData,
  Wallet,
  decryptPrivateKeyAndDecryptShare,
  encryptPrivateKey,
  getAsymmetricKeyPair,
  getDerivedPrivateKeyAndDecrypt,
  getPublicKeyHex,
  getSHA256HashHex,
  parseCredentialCreationRes,
} from '@getpara/web-sdk';

import { ReactNativeUtils } from './ReactNativeUtils.js';
import {
  Passkey,
  PasskeyCreateRequest,
  PasskeyCreateResult,
  PasskeyGetRequest,
  PasskeyGetResult,
} from 'react-native-passkey';
import { CurrentWalletIds, AuthMethodStatus, TWalletScheme } from '@getpara/user-management-client';
import { mapPasskeyError, logPasskeyDiagnostic } from './errors.js';
import { setEnv } from '../config.js';
import base64url from 'base64url';

const ES256_ALGORITHM = -7;
const RS256_ALGORITHM = -257;

/**
 * Represents a mobile implementation of the Para SDK.
 * @extends ParaCore
 *
 * @example
 * const para = new ParaMobile(Environment.BETA, "api_key");
 */
export class ParaMobile extends ParaCore {
  isNativePasskey = true;

  private relyingPartyId: string;
  /**
   * Creates an instance of ParaMobile.
   * @param {Environment} env - The environment to use (DEV, SANDBOX, BETA, or PROD). Optional if the apiKey contains an environment prefix (e.g., "prod_your_api_key"). Updated API keys can be found at https://developer.getpara.com.
   * @param {string} [apiKey] - The API key for authentication.
   * @param {string} [relyingPartyId] - The relying party ID for WebAuthn.
   * @param {ConstructorOpts} [opts] - Additional constructor options.
   */
  constructor(env: Environment | undefined, apiKey: string, relyingPartyId?: string, opts?: ConstructorOpts);
  constructor(apiKey: string, relyingPartyId?: string, opts?: ConstructorOpts);
  constructor(
    envOrApiKey: Environment | undefined | string,
    apiKeyOrRelyingPartyId?: string,
    relyingPartyIdOrOpts?: string | ConstructorOpts,
    optsArg?: ConstructorOpts,
  ) {
    let env: Environment | undefined, apiKey: string, relyingPartyId: string | undefined, opts: ConstructorOpts | undefined;

    // Helper function to check if value is a valid Environment
    const isEnvironment = (value: any): value is Environment => {
      return Object.values(Environment).includes(value);
    };

    if (arguments.length === 1) {
      // 1-parameter case: (apiKey)
      env = undefined;
      apiKey = envOrApiKey as string;
      relyingPartyId = undefined;
      opts = undefined;
    } else if (arguments.length === 2) {
      if (isEnvironment(envOrApiKey)) {
        // 2-parameter case: (env, apiKey)
        env = envOrApiKey;
        apiKey = apiKeyOrRelyingPartyId as string;
        relyingPartyId = undefined;
        opts = undefined;
      } else {
        // 2-parameter case: (apiKey, relyingPartyId)
        env = undefined;
        apiKey = envOrApiKey as string;
        relyingPartyId = apiKeyOrRelyingPartyId;
        opts = undefined;
      }
    } else if (arguments.length === 3) {
      if (isEnvironment(envOrApiKey)) {
        // 3-parameter case: (env, apiKey, relyingPartyId)
        env = envOrApiKey;
        apiKey = apiKeyOrRelyingPartyId as string;
        relyingPartyId = relyingPartyIdOrOpts as string;
        opts = undefined;
      } else {
        // 3-parameter case: (apiKey, relyingPartyId, opts)
        env = undefined;
        apiKey = envOrApiKey as string;
        relyingPartyId = apiKeyOrRelyingPartyId;
        opts = relyingPartyIdOrOpts as ConstructorOpts;
      }
    } else {
      // 4-parameter case: (env, apiKey, relyingPartyId, opts)
      env = envOrApiKey as Environment | undefined;
      apiKey = apiKeyOrRelyingPartyId as string;
      relyingPartyId = relyingPartyIdOrOpts as string | undefined;
      opts = optsArg;
    }

    env = ParaCore.resolveEnvironment(env, apiKey); // Ensure the environment is resolved before calling super

    super(env, apiKey, opts);

    setEnv(env);

    if (relyingPartyId) {
      this.relyingPartyId = relyingPartyId;
    } else {
      switch (env) {
        case Environment.DEV:
          throw new Error('relyingPartyId is required');
        case Environment.SANDBOX:
          this.relyingPartyId = 'app.sandbox.usecapsule.com';
          break;
        case Environment.BETA:
          this.relyingPartyId = 'app.beta.usecapsule.com';
          break;
        case Environment.PROD:
          this.relyingPartyId = 'app.usecapsule.com';
          break;
      }
    }
  }

  async setup() {
    this.isSetup = true;
  }

  protected getPlatformUtils(): PlatformUtils {
    return new ReactNativeUtils();
  }

  async isPasskeySupported(): Promise<boolean> {
    return Passkey.isSupported();
  }

  /**
   * Registers a passkey for the user.
   * @param authState - Either an AuthStateSignup object or a passkeyId string.
   * @returns {Promise<void>}
   */
  async registerPasskey(authState: AuthStateSignup): Promise<void>;
  async registerPasskey(passkeyId: string): Promise<void>;
  async registerPasskey(authStateOrPasskeyId: AuthStateSignup | string): Promise<void> {
    const passkeyId = typeof authStateOrPasskeyId === 'string' ? authStateOrPasskeyId : authStateOrPasskeyId.passkeyId;
    if (!passkeyId) {
      throw new Error('Passkey ID not found. Make sure you have enabled passkey logins in the Para Developer Portal.');
    }

    const userId = this.assertUserId();
    const authInfo = this.assertIsAuthSet();

    if (!globalThis.crypto || !globalThis.crypto.getRandomValues) {
      throw new Error('Web crypto is not available. Ensure you have imported the shim from @getpara/react-native-wallet.');
    }
    const userHandle = new Uint8Array(32);
    globalThis.crypto.getRandomValues(userHandle);
    const userHandleEncoded = base64url.encode(userHandle as any);

    const requestJson: PasskeyCreateRequest = {
      authenticatorSelection: {
        authenticatorAttachment: 'platform' as any,
        requireResidentKey: true,
        residentKey: 'required' as any,
        userVerification: 'required' as any,
      },
      rp: {
        id: this.relyingPartyId,
        name: 'Para',
      },
      user: {
        id: userHandleEncoded,
        name: authInfo.identifier,
        displayName: authInfo.identifier,
      },
      pubKeyCredParams: [
        {
          type: 'public-key',
          alg: ES256_ALGORITHM,
        },
        {
          type: 'public-key',
          alg: RS256_ALGORITHM,
        },
      ],
      attestation: 'direct' as any,
      timeout: 60000,
      challenge: base64url.encode('somechallenge'),
    };

    let result: PasskeyCreateResult;
    try {
      result = await Passkey.create(requestJson);
    } catch (error: unknown) {
      const enriched = mapPasskeyError(error);
      logPasskeyDiagnostic(enriched, 'registration', this.relyingPartyId);
      throw enriched;
    }
    let resultJson;

    if (typeof result === 'string') {
      resultJson = JSON.parse(result);
    } else {
      resultJson = result;
    }

    const { cosePublicKey, clientDataJSON } = parseCredentialCreationRes(resultJson, ES256_ALGORITHM);

    const keyPair = await getAsymmetricKeyPair(this.ctx);
    const publicKeyHex = getPublicKeyHex(keyPair);

    const encryptionKeyHash = getSHA256HashHex(userHandleEncoded);
    const encryptedPrivateKeyHex = await encryptPrivateKey(keyPair, userHandleEncoded);

    const { partnerId } = await this.ctx.client.touchSession();
    await this.ctx.client.patchSessionPublicKey(partnerId, userId, passkeyId, {
      publicKey: resultJson.id,
      sigDerivedPublicKey: publicKeyHex,
      cosePublicKey,
      clientDataJSON,
      status: AuthMethodStatus.COMPLETE,
    });

    await this.ctx.client.uploadEncryptedWalletPrivateKey(userId, encryptedPrivateKeyHex, encryptionKeyHash, resultJson.id);
  }

  /**
   * Logs in the user using their authentication credentials.
   * @param {AuthParams} params - The authentication parameters.
   * @returns {Promise<void>}
   */
  async loginWithPasskey(): Promise<void> {
    this.assertIsAuthSet();
    const userId = this.assertUserId();

    const { challenge, allowedPublicKeys } = await this.ctx.client.getWebChallenge({ userId });

    const requestJson: PasskeyGetRequest = {
      challenge,
      timeout: 60000,
      rpId: this.relyingPartyId,
      allowCredentials: allowedPublicKeys?.[0] ? [{ type: 'public-key', id: allowedPublicKeys[0] }] : [],
    };

    let result: PasskeyGetResult;
    try {
      result = await Passkey.get(requestJson);
    } catch (error: unknown) {
      const enriched = mapPasskeyError(error);
      logPasskeyDiagnostic(enriched, 'login', this.relyingPartyId);
      throw enriched;
    }

    let resultJson;

    if (typeof result === 'string') {
      resultJson = JSON.parse(result);
    } else {
      resultJson = result;
    }

    const { partnerId, sessionLookupId } = await this.ctx.client.touchSession();
    const publicKey = resultJson.id;
    const verifyWebChallengeResult = await this.ctx.client.verifyWebChallenge(partnerId, {
      publicKey,
      signature: {
        clientDataJSON: resultJson.response.clientDataJSON,
        authenticatorData: resultJson.response.authenticatorData,
        signature: resultJson.response.signature,
      },
    });

    if (userId !== verifyWebChallengeResult.data.userId) {
      throw new Error('User ID mismatch');
    }

    const encryptedSharesResult = await this.ctx.client.getBiometricKeyshares(userId, resultJson.id);

    const encryptionKeyHash = getSHA256HashHex(resultJson.response.userHandle);
    const { encryptedPrivateKeys } = await this.ctx.client.getEncryptedWalletPrivateKeys(userId, encryptionKeyHash);

    let decryptedShares;
    if (encryptedPrivateKeys.length === 0) {
      decryptedShares = await getDerivedPrivateKeyAndDecrypt(
        this.ctx,
        resultJson.response.userHandle,
        encryptedSharesResult.data.keyShares,
      );
      const keyPair = await getAsymmetricKeyPair(this.ctx, resultJson.response.userHandle);
      const encryptedPrivateKeyHex = await encryptPrivateKey(keyPair, resultJson.response.userHandle);
      await this.ctx.client.uploadEncryptedWalletPrivateKey(
        userId,
        encryptedPrivateKeyHex,
        encryptionKeyHash,
        resultJson.id,
      );
    } else {
      decryptedShares = await decryptPrivateKeyAndDecryptShare(
        resultJson.response.userHandle,
        encryptedSharesResult.data.keyShares,
        encryptedPrivateKeys[0]!.encryptedPrivateKey,
      );
    }

    const walletsRes = await this.ctx.client.getWallets(userId);
    const desiredWallets = walletsRes.data.wallets;

    const walletsToInsert: { [id: string]: Wallet } = {};
    for (let desiredWallet of desiredWallets) {
      const decryptedShare = decryptedShares.find(share => share.walletId === desiredWallet.id)!;
      walletsToInsert[decryptedShare.walletId] = {
        id: decryptedShare.walletId,
        signer: decryptedShare.signer,
        address: desiredWallet.address || undefined,
        publicKey: desiredWallet.publicKey || undefined,
        scheme: desiredWallet.scheme as TWalletScheme,
        type: desiredWallet.type || undefined,
      };
    }

    const currentWalletIds: CurrentWalletIds = {};
    for (const wallet of Object.values(walletsToInsert)) {
      const { id, type } = wallet;
      const currentIdsForType = currentWalletIds[type || 'EVM'] || [];
      currentWalletIds[type || 'EVM'] = [...currentIdsForType, id];
    }

    await this.setWallets(walletsToInsert);
    await this.setCurrentWalletIds(currentWalletIds, {
      sessionLookupId,
    });
  }

  /**
   * Sets a global handler for transaction review URLs. When a signing operation requires
   * user approval (e.g., due to permissions policies), the SDK needs to open a review URL.
   * On React Native, provide a handler that opens the URL in an in-app browser.
   *
   * @example
   * ```ts
   * import { openBrowserAsync } from 'expo-web-browser';
   * para.setTransactionReviewHandler((url) => openBrowserAsync(url));
   * ```
   */
  setTransactionReviewHandler(handler: (url: string) => void): void {
    (this.platformUtils as ReactNativeUtils).transactionReviewHandler = handler;
  }

  /**
   * Handles a message from an in-app browser (WebView).
   * Call this from your WebView's `onMessage` callback to bridge portal events
   * (e.g. TELEGRAM_SUCCESS, FARCASTER_SUCCESS) into the SDK's event system.
   *
   * @example
   * ```tsx
   * <WebView
   *   source={{ uri: portalUrl }}
   *   onMessage={(event) => {
   *     const data = JSON.parse(event.nativeEvent.data);
   *     para.handleWebViewMessage(data);
   *   }}
   * />
   * ```
   */
  handleWebViewMessage(data: EventData): void {
    this.platformUtils.postMessage?.({ data });
  }
}
