import {
    assertKeyExporterIsAvailable,
    assertSigningCapabilityIsAvailable,
    assertVerificationCapabilityIsAvailable,
} from '@solana/assertions';
import { Encoder, ReadonlyUint8Array, toArrayBuffer } from '@solana/codecs-core';
import { getBase58Encoder } from '@solana/codecs-strings';
import {
    SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH,
    SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
    SOLANA_ERROR__KEYS__VERIFICATION_REQUIRES_EXTRACTABLE_PUBLIC_KEY,
    SolanaError,
} from '@solana/errors';
import { Brand, EncodedString } from '@solana/nominal-types';

import { ED25519_ALGORITHM_IDENTIFIER } from './algorithm';
import { isSmallOrderPointEncoding } from './small-order';

/**
 * A 64-byte Ed25519 signature as a base58-encoded string.
 */
export type Signature = Brand<EncodedString<string, 'base58'>, 'Signature'>;
/**
 * A 64-byte Ed25519 signature.
 *
 * Whenever you need to verify that a particular signature is, in fact, the one that would have been
 * produced by signing some known bytes using the private key associated with some known public key,
 * use the {@link verifySignature} function in this package.
 */
export type SignatureBytes = Brand<Uint8Array, 'SignatureBytes'>;

let base58Encoder: Encoder<string> | undefined;

/**
 * Asserts that an arbitrary string is a base58-encoded Ed25519 signature.
 *
 * Useful when you receive a string from user input or an untrusted network API that you expect to
 * represent an Ed25519 signature (eg. of a transaction).
 *
 * @example
 * ```ts
 * import { assertIsSignature } from '@solana/keys';
 *
 * // Imagine a function that asserts whether a user-supplied signature is valid or not.
 * function handleSubmit() {
 *     // We know only that what the user typed conforms to the `string` type.
 *     const signature: string = signatureInput.value;
 *     try {
 *         // If this type assertion function doesn't throw, then
 *         // Typescript will upcast `signature` to `Signature`.
 *         assertIsSignature(signature);
 *         // At this point, `signature` is a `Signature` that can be used with the RPC.
 *         const {
 *             value: [status],
 *         } = await rpc.getSignatureStatuses([signature]).send();
 *     } catch (e) {
 *         // `signature` turned out not to be a base58-encoded signature
 *     }
 * }
 * ```
 */
export function assertIsSignature(putativeSignature: string): asserts putativeSignature is Signature {
    if (!base58Encoder) base58Encoder = getBase58Encoder();
    // Fast-path; see if the input string is of an acceptable length.
    if (
        // Lowest value (64 bytes of zeroes)
        putativeSignature.length < 64 ||
        // Highest value (64 bytes of 255)
        putativeSignature.length > 88
    ) {
        throw new SolanaError(SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE, {
            actualLength: putativeSignature.length,
        });
    }
    // Slow-path; actually attempt to decode the input string.
    const bytes = base58Encoder.encode(putativeSignature);
    assertIsSignatureBytes(bytes);
}

/**
 * Asserts that an arbitrary `ReadonlyUint8Array` is an Ed25519 signature.
 *
 * Useful when you receive a `ReadonlyUint8Array` from an external interface (like the browser wallets' `signMessage` API) that you expect to
 * represent an Ed25519 signature.
 *
 * @example
 * ```ts
 * import { assertIsSignatureBytes } from '@solana/keys';
 *
 * // Imagine a function that verifies a signature.
 * function verifySignature() {
 *     // We know only that the input conforms to the `ReadonlyUint8Array` type.
 *     const signatureBytes: ReadonlyUint8Array = signatureBytesInput;
 *     try {
 *         // If this type assertion function doesn't throw, then
 *         // Typescript will upcast `signatureBytes` to `SignatureBytes`.
 *         assertIsSignatureBytes(signatureBytes);
 *         // At this point, `signatureBytes` is a `SignatureBytes` that can be used with `verifySignature`.
 *         if (!(await verifySignature(publicKey, signatureBytes, data))) {
 *             throw new Error('The data were *not* signed by the private key associated with `publicKey`');
 *         }
 *     } catch (e) {
 *         // `signatureBytes` turned out not to be a 64-byte Ed25519 signature
 *     }
 * }
 * ```
 */
export function assertIsSignatureBytes(
    putativeSignatureBytes: ReadonlyUint8Array,
): asserts putativeSignatureBytes is SignatureBytes {
    const numBytes = putativeSignatureBytes.byteLength;
    if (numBytes !== 64) {
        throw new SolanaError(SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH, {
            actualLength: numBytes,
        });
    }
}

/**
 * A type guard that accepts a string as input. It will both return `true` if the string conforms to
 * the {@link Signature} type and will refine the type for use in your program.
 *
 * @example
 * ```ts
 * import { isSignature } from '@solana/keys';
 *
 * if (isSignature(signature)) {
 *     // At this point, `signature` has been refined to a
 *     // `Signature` that can be used with the RPC.
 *     const {
 *         value: [status],
 *     } = await rpc.getSignatureStatuses([signature]).send();
 *     setSignatureStatus(status);
 * } else {
 *     setError(`${signature} is not a transaction signature`);
 * }
 * ```
 */
export function isSignature(putativeSignature: string): putativeSignature is Signature {
    if (!base58Encoder) base58Encoder = getBase58Encoder();

    // Fast-path; see if the input string is of an acceptable length.
    if (
        // Lowest value (64 bytes of zeroes)
        putativeSignature.length < 64 ||
        // Highest value (64 bytes of 255)
        putativeSignature.length > 88
    ) {
        return false;
    }
    // Slow-path; actually attempt to decode the input string.
    const bytes = base58Encoder.encode(putativeSignature);
    return isSignatureBytes(bytes);
}

/**
 * A type guard that accepts a `ReadonlyUint8Array` as input. It will both return `true` if the `ReadonlyUint8Array` conforms to
 * the {@link SignatureBytes} type and will refine the type for use in your program.
 *
 * @example
 * ```ts
 * import { isSignatureBytes } from '@solana/keys';
 *
 * if (isSignatureBytes(signatureBytes)) {
 *     // At this point, `signatureBytes` has been refined to a
 *     // `SignatureBytes` that can be used with `verifySignature`.
 *     if (!(await verifySignature(publicKey, signatureBytes, data))) {
 *         throw new Error('The data were *not* signed by the private key associated with `publicKey`');
 *     }
 * } else {
 *     setError(`${signatureBytes} is not a 64-byte Ed25519 signature`);
 * }
 * ```
 */
export function isSignatureBytes(putativeSignatureBytes: ReadonlyUint8Array): putativeSignatureBytes is SignatureBytes {
    return putativeSignatureBytes.byteLength === 64;
}

/**
 * Given a private [`CryptoKey`](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey) and a
 * `Uint8Array` of bytes, this method will return the 64-byte Ed25519 signature of that data as a
 * `Uint8Array`.
 *
 * @example
 * ```ts
 * import { signBytes } from '@solana/keys';
 *
 * const data = new Uint8Array([1, 2, 3]);
 * const signature = await signBytes(privateKey, data);
 * ```
 */
export async function signBytes(key: CryptoKey, data: ReadonlyUint8Array): Promise<SignatureBytes> {
    assertSigningCapabilityIsAvailable();
    const signedData = await crypto.subtle.sign(ED25519_ALGORITHM_IDENTIFIER, key, toArrayBuffer(data));
    return new Uint8Array(signedData) as SignatureBytes;
}

/**
 * This helper combines _asserting_ that a string is an Ed25519 signature with _coercing_ it to the
 * {@link Signature} type. It's best used with untrusted input.
 *
 * @example
 * ```ts
 * import { signature } from '@solana/keys';
 *
 * const signature = signature(userSuppliedSignature);
 * const {
 *     value: [status],
 * } = await rpc.getSignatureStatuses([signature]).send();
 * ```
 */
export function signature(putativeSignature: string): Signature {
    assertIsSignature(putativeSignature);
    return putativeSignature;
}

/**
 * This helper combines _asserting_ that a `ReadonlyUint8Array` is an Ed25519 signature with _coercing_ it to the
 * {@link SignatureBytes} type. It's best used with untrusted input.
 *
 * @example
 * ```ts
 * import { signatureBytes } from '@solana/keys';
 *
 * const signature = signatureBytes(userSuppliedSignatureBytes);
 * if (!(await verifySignature(publicKey, signature, data))) {
 *     throw new Error('The data were *not* signed by the private key associated with `publicKey`');
 * }
 * ```
 */
export function signatureBytes(putativeSignatureBytes: ReadonlyUint8Array): SignatureBytes {
    assertIsSignatureBytes(putativeSignatureBytes);
    return putativeSignatureBytes;
}

/**
 * Given a public [`CryptoKey`](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey), some
 * {@link SignatureBytes}, and a `Uint8Array` of data, this method will return `true` if the
 * signature was produced by signing the data using the private key associated with the public key,
 * and `false` otherwise.
 *
 * Verification is strict: signatures whose `R` component is a point of small order are rejected, as
 * are public keys of small order (for which trivially forged signatures would otherwise verify
 * against any data). This matches the behavior of the Solana runtime, and holds regardless of
 * whether the underlying WebCrypto implementation performs these checks itself.
 *
 * Checking the public key requires reading its bytes, so the key must be an extractable public key.
 * The public keys produced by this library (eg. by {@link generateKeyPair},
 * {@link getPublicKeyFromPrivateKey}, and `getPublicKeyFromAddress()`) always are. If you import a
 * public key yourself using `crypto.subtle.importKey()`, set `extractable` to `true`.
 *
 * @throws {@link SOLANA_ERROR__KEYS__VERIFICATION_REQUIRES_EXTRACTABLE_PUBLIC_KEY} when supplied a
 * key that is not a public key, or a public key that is not extractable.
 *
 * @example
 * ```ts
 * import { verifySignature } from '@solana/keys';
 *
 * const data = new Uint8Array([1, 2, 3]);
 * if (!(await verifySignature(publicKey, signature, data))) {
 *     throw new Error('The data were *not* signed by the private key associated with `publicKey`');
 * }
 * ```
 */
export async function verifySignature(
    key: CryptoKey,
    signature: SignatureBytes,
    data: ReadonlyUint8Array,
): Promise<boolean> {
    assertVerificationCapabilityIsAvailable();
    assertKeyExporterIsAvailable();
    if (key.type !== 'public' || !key.extractable) {
        throw new SolanaError(SOLANA_ERROR__KEYS__VERIFICATION_REQUIRES_EXTRACTABLE_PUBLIC_KEY);
    }
    const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', key));
    if (isSmallOrderPointEncoding(publicKeyBytes)) {
        return false;
    }
    if (isSmallOrderPointEncoding(signature.subarray(0, 32))) {
        return false;
    }
    return await crypto.subtle.verify(ED25519_ALGORITHM_IDENTIFIER, key, toArrayBuffer(signature), toArrayBuffer(data));
}
