import { Deserializer, Serializer } from "../../bcs/index.js";
import {
  AnyPublicKeyVariant,
  AnySignatureVariant,
  SigningScheme as AuthenticationKeyScheme,
  HexInput,
} from "../../types/index.js";
import { AuthenticationKey } from "../authenticationKey.js";
import { Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature } from "./ed25519.js";
import { AccountPublicKey, PublicKey } from "./publicKey.js";
import { Secp256k1PrivateKey, Secp256k1PublicKey, Secp256k1Signature } from "./secp256k1.js";
import { Signature } from "./signature.js";
import { AptosConfig } from "../../api/aptosConfig.js";
import { Secp256r1PublicKey, WebAuthnSignature } from "./secp256r1.js";
import {
  detectPublicKeyVariant,
  detectSignatureVariant,
  getPublicKeyDeserializer,
  getSignatureDeserializer,
} from "./anyKeyRegistry.js";

export type PrivateKeyInput = Ed25519PrivateKey | Secp256k1PrivateKey;

/**
 * Represents any public key supported by Aptos.
 *
 * Since [AIP-55](https://github.com/aptos-foundation/AIPs/pull/263) Aptos supports
 * `Legacy` and `Unified` authentication keys.
 *
 * Any unified authentication key is represented in the SDK as `AnyPublicKey`.
 * @group Implementation
 * @category Serialization
 */
export class AnyPublicKey extends AccountPublicKey {
  /**
   * Reference to the inner public key
   * @group Implementation
   * @category Serialization
   */
  public readonly publicKey: PublicKey;

  /**
   * Index of the underlying enum variant
   * @group Implementation
   * @category Serialization
   */
  public readonly variant: AnyPublicKeyVariant;

  // region Constructors

  /**
   * Creates an instance of the signature class based on the provided signature type.
   * This allows for the handling of different signature variants such as Ed25519, Secp256k1, and Keyless.
   *
   * @param publicKey - The publicKey object which determines the variant to be used.
   * @throws Error if the provided signature type is unsupported.
   * @group Implementation
   * @category Serialization
   */
  constructor(publicKey: PublicKey, variant?: AnyPublicKeyVariant) {
    super();
    this.publicKey = publicKey;
    if (variant !== undefined) {
      this.variant = variant;
    } else if (publicKey instanceof Ed25519PublicKey) {
      this.variant = AnyPublicKeyVariant.Ed25519;
    } else if (publicKey instanceof Secp256k1PublicKey) {
      this.variant = AnyPublicKeyVariant.Secp256k1;
    } else if (publicKey instanceof Secp256r1PublicKey) {
      this.variant = AnyPublicKeyVariant.Secp256r1;
    } else {
      // Check registered variants (e.g., keyless, federated keyless)
      const registeredVariant = detectPublicKeyVariant(publicKey);
      if (registeredVariant !== undefined) {
        this.variant = registeredVariant;
      } else {
        throw new Error("Unsupported public key type");
      }
    }
  }

  // endregion

  // region AccountPublicKey

  /**
   * Verifies the provided signature against the given message.
   * This function helps ensure the integrity and authenticity of the message by confirming that the signature is valid.
   *
   * @param args - The arguments for signature verification.
   * @param args.message - The message that was signed.
   * @param args.signature - The signature to verify, which must be an instance of AnySignature.
   * @returns A boolean indicating whether the signature is valid for the given message.
   * @group Implementation
   * @category Serialization
   */
  verifySignature(args: { message: HexInput; signature: AnySignature }): boolean {
    const { message, signature } = args;
    if (this.variant === AnyPublicKeyVariant.Keyless || this.variant === AnyPublicKeyVariant.FederatedKeyless) {
      throw new Error("Use verifySignatureAsync to verify Keyless signatures");
    }
    return this.publicKey.verifySignature({
      message,
      signature: signature.signature,
    });
  }

  /**
   * Verifies the provided signature against the given message.
   * This function helps ensure the integrity and authenticity of the message by confirming that the signature is valid.
   *
   * @param args - The arguments for signature verification.
   * @param args.aptosConfig - The configuration object for connecting to the Aptos network
   * @param args.message - The message that was signed.
   * @param args.signature - The signature to verify, which must be an instance of AnySignature.
   * @returns A boolean indicating whether the signature is valid for the given message.
   * @group Implementation
   * @category Serialization
   */
  async verifySignatureAsync(args: {
    aptosConfig: AptosConfig;
    message: HexInput;
    signature: Signature;
    options?: { throwErrorWithReason?: boolean };
  }): Promise<boolean> {
    if (!(args.signature instanceof AnySignature)) {
      if (args.options?.throwErrorWithReason) {
        throw new Error("Signature must be an instance of AnySignature");
      }
      return false;
    }
    return await this.publicKey.verifySignatureAsync({
      ...args,
      signature: args.signature.signature,
    });
  }

  /**
   * Generates an authentication key from the current instance's byte representation.
   * This function is essential for creating a unique identifier for authentication purposes.
   *
   * @returns {AuthenticationKey} The generated authentication key.
   * @group Implementation
   * @category Serialization
   */
  authKey(): AuthenticationKey {
    return AuthenticationKey.fromSchemeAndBytes({
      scheme: AuthenticationKeyScheme.SingleKey,
      input: this.toUint8Array(),
    });
  }

  /**
   * Get the signature in bytes (Uint8Array).
   *
   * This function is a warning that it will soon return the underlying signature bytes directly.
   * Use AnySignature.bcsToBytes() instead.
   *
   * @returns Uint8Array representation of the signature.
   * @group Implementation
   * @category Serialization
   */
  toUint8Array(): Uint8Array {
    return this.bcsToBytes();
  }

  // endregion

  // region Serializable

  /**
   * Serializes the current object using the provided serializer.
   * This function helps in converting the object into a format suitable for transmission or storage.
   *
   * @param serializer - The serializer instance used to perform the serialization.
   * @group Implementation
   * @category Serialization
   */
  serialize(serializer: Serializer): void {
    serializer.serializeU32AsUleb128(this.variant);
    this.publicKey.serialize(serializer);
  }

  /**
   * Deserializes an AnySignature from the provided deserializer.
   * This function helps in reconstructing the AnySignature object from its serialized form, allowing for further processing or validation.
   *
   * @param deserializer - The deserializer instance used to read the serialized data.
   * @group Implementation
   * @category Serialization
   */
  static deserialize(deserializer: Deserializer): AnyPublicKey {
    const variantIndex = deserializer.deserializeUleb128AsU32();
    let publicKey: PublicKey;
    switch (variantIndex) {
      case AnyPublicKeyVariant.Ed25519:
        publicKey = Ed25519PublicKey.deserialize(deserializer);
        break;
      case AnyPublicKeyVariant.Secp256k1:
        publicKey = Secp256k1PublicKey.deserialize(deserializer);
        break;
      case AnyPublicKeyVariant.Secp256r1:
        publicKey = Secp256r1PublicKey.deserialize(deserializer);
        break;
      default: {
        // Check registered variant deserializers (e.g., keyless, federated keyless)
        const registeredDeserializer = getPublicKeyDeserializer(variantIndex);
        if (registeredDeserializer) {
          publicKey = registeredDeserializer(deserializer);
          break;
        }
        throw new Error(
          `Unknown variant index for AnyPublicKey: ${variantIndex}. ` +
            "If this is a keyless key, ensure keyless support is imported.",
        );
      }
    }
    return new AnyPublicKey(publicKey);
  }
  // endregion

  /**
   * Determines if the provided public key is an instance of AnyPublicKey.
   *
   * @param publicKey - The public key to check.
   * @deprecated Use `instanceof AnyPublicKey` instead.
   * @group Implementation
   * @category Serialization
   */
  static isPublicKey(publicKey: AccountPublicKey): publicKey is AnyPublicKey {
    return publicKey instanceof AnyPublicKey;
  }

  /**
   * Determines if the current public key is an instance of Ed25519PublicKey.
   *
   * @deprecated use `publicKey instanceof Ed25519PublicKey` instead.
   * @group Implementation
   * @category Serialization
   */
  isEd25519(): boolean {
    return this.publicKey instanceof Ed25519PublicKey;
  }

  /**
   * Checks if the public key is an instance of Secp256k1PublicKey.
   *
   * @deprecated use `publicKey instanceof Secp256k1PublicKey` instead.
   * @group Implementation
   * @category Serialization
   */
  isSecp256k1PublicKey(): boolean {
    return this.publicKey instanceof Secp256k1PublicKey;
  }

  /**
   * Determines if the provided publicKey is an instance of a valid PublicKey object.
   *
   * @param publicKey - The publicKey to be checked for validity.
   * @param publicKey.publicKey - The actual publicKey object that needs to be validated.
   * @returns True if the signature is a valid instance; otherwise, false.
   * @group Implementation
   * @category Serialization
   */
  static isInstance(publicKey: PublicKey): publicKey is AnyPublicKey {
    return "publicKey" in publicKey && "variant" in publicKey;
  }
}

/**
 * Represents a signature that utilizes the SingleKey authentication scheme.
 * This class is designed to encapsulate various types of signatures, which can
 * only be generated by a `SingleKeySigner` due to the shared authentication mechanism.
 *
 * @extends Signature
 * @group Implementation
 * @category Serialization
 */
export class AnySignature extends Signature {
  public readonly signature: Signature;

  /**
   * Index of the underlying enum variant
   * @group Implementation
   * @category Serialization
   */
  private readonly variant: AnySignatureVariant;

  // region Constructors

  constructor(signature: Signature) {
    super();
    this.signature = signature;

    if (signature instanceof Ed25519Signature) {
      this.variant = AnySignatureVariant.Ed25519;
    } else if (signature instanceof Secp256k1Signature) {
      this.variant = AnySignatureVariant.Secp256k1;
    } else if (signature instanceof WebAuthnSignature) {
      this.variant = AnySignatureVariant.WebAuthn;
    } else {
      // Check registered variants (e.g., keyless)
      const registeredVariant = detectSignatureVariant(signature);
      if (registeredVariant !== undefined) {
        this.variant = registeredVariant;
      } else {
        throw new Error("Unsupported signature type");
      }
    }
  }

  // endregion

  // region AccountSignature

  toUint8Array(): Uint8Array {
    // TODO: keep this warning around for a bit, and eventually change this to return `this.signature.toUint8Array()`.
    console.warn(
      "[Aptos SDK] Calls to AnySignature.toUint8Array() will soon return the underlying signature bytes. " +
        "Use AnySignature.bcsToBytes() instead.",
    );
    return this.bcsToBytes();
  }

  // endregion

  // region Serializable

  serialize(serializer: Serializer): void {
    serializer.serializeU32AsUleb128(this.variant);
    this.signature.serialize(serializer);
  }

  static deserialize(deserializer: Deserializer): AnySignature {
    const variantIndex = deserializer.deserializeUleb128AsU32();
    let signature: Signature;
    switch (variantIndex) {
      case AnySignatureVariant.Ed25519:
        signature = Ed25519Signature.deserialize(deserializer);
        break;
      case AnySignatureVariant.Secp256k1:
        signature = Secp256k1Signature.deserialize(deserializer);
        break;
      case AnySignatureVariant.WebAuthn:
        signature = WebAuthnSignature.deserialize(deserializer);
        break;
      default: {
        // Check registered variant deserializers (e.g., keyless)
        const registeredDeserializer = getSignatureDeserializer(variantIndex);
        if (registeredDeserializer) {
          signature = registeredDeserializer(deserializer);
          break;
        }
        throw new Error(
          `Unknown variant index for AnySignature: ${variantIndex}. ` +
            "If this is a keyless signature, ensure keyless support is imported.",
        );
      }
    }
    return new AnySignature(signature);
  }

  // endregion

  static isInstance(signature: Signature): signature is AnySignature {
    return (
      "signature" in signature &&
      typeof signature.signature === "object" &&
      signature.signature !== null &&
      "toUint8Array" in signature.signature
    );
  }
}
