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

/**
 *
 * A module containing a grab bag of support for core network I/O functionality, including sockets, TLS, DNS, logging,
 * error handling, streams, and connection -> thread mapping.
 *
 * Categories include:
 * - Network: socket configuration
 * - TLS: tls configuration
 * - Logging: logging controls and configuration
 * - IO: everything else
 *
 * @packageDocumentation
 * @module io
 * @mergeTarget
 */

import crt_native from './binding';
import { NativeResource } from "./native_resource";
import { setLogLevel, LogLevel, TlsVersion, SocketType, SocketDomain } from '../common/io';
import { Readable } from 'stream';
// Do not re-export the logging functions in common; they are package-private
export { setLogLevel, LogLevel, TlsVersion, SocketType, SocketDomain } from '../common/io';
import { CrtError } from './error';

/**
 * Convert a native error code into a human-readable string
 * @param error_code - An error code returned from a native API call, or delivered
 * via callback.
 * @returns Long-form description of the error
 * @see CrtError
 *
 * nodejs only.
 *
 * @category System
 */
export function error_code_to_string(error_code: number): string {
    return crt_native.error_code_to_string(error_code);
}

/**
 * Convert a native error code into a human-readable identifier
 * @param error_code - An error code returned from a native API call, or delivered
 * via callback.
 * @return error name as a string
 * @see CrtError
 *
 * nodejs only.
 *
 * @category System
 */
export function error_code_to_name(error_code: number): string {
    return crt_native.error_code_to_name(error_code);
}

/**
 * Enables logging of the native AWS CRT libraries.
 * @param level - The logging level to filter to. It is not possible to log less than WARN.
 *
 * nodejs only.
 * @category Logging
 */
export function enable_logging(level: LogLevel) {
    crt_native.io_logging_enable(level);
    setLogLevel(level);
}

/**
 * Returns true if ALPN is available on this platform natively
 * @return true if ALPN is supported natively, false otherwise
 *
 * nodejs only.
 * @category TLS
*/
export function is_alpn_available(): boolean {
    return crt_native.is_alpn_available();
}

/**
 * Wraps a ```Readable``` for reading by native code, used to stream
 *  data into the AWS CRT libraries.
 *
 * nodejs only.
 * @category IO
 */
export class InputStream extends NativeResource {
    constructor(private source: Readable) {
        super(crt_native.io_input_stream_new(16 * 1024));
        this.source.on('data', (data) => {
            data = Buffer.isBuffer(data) ? data : Buffer.from(data.toString());
            crt_native.io_input_stream_append(this.native_handle(), data);
        });
        this.source.on('end', () => {
            crt_native.io_input_stream_append(this.native_handle(), undefined);
        })
    }
}

/**
 * Represents native resources required to bootstrap a client connection
 * Things like a host resolver, event loop group, etc. There should only need
 * to be 1 of these per application, in most cases.
 *
 * nodejs only.
 * @category IO
 */
export class ClientBootstrap extends NativeResource {
    constructor() {
        super(crt_native.io_client_bootstrap_new());
    }
}

/**
 * Standard Berkeley socket style options.
 *
 * nodejs only.
 * @category Network
*/
export class SocketOptions extends NativeResource {
    constructor(
        type = SocketType.STREAM,
        domain = SocketDomain.IPV6,
        connect_timeout_ms = 5000,
        keepalive = false,
        keep_alive_interval_sec = 0,
        keep_alive_timeout_sec = 0,
        keep_alive_max_failed_probes = 0) {
        super(crt_native.io_socket_options_new(
            type,
            domain,
            connect_timeout_ms,
            keep_alive_interval_sec,
            keep_alive_timeout_sec,
            keep_alive_max_failed_probes,
            keepalive
        ));
    }
}

/**
 * Interface used to hold the options for creating a PKCS#12 connection in the builder.
 *
 * Note: Only supported on MacOS devices.
 *
 * NodeJS only
 * @category TLS
 */
export interface Pkcs12Options {
    /** Path to the PKCS#12 file */
    pkcs12_file: string;

    /** The password for the PKCS#12 file */
    pkcs12_password : string;
}

/**
 * Each TlsCipherPreference represents an ordered list of TLS Ciphers to use when negotiating a TLS Connection. At
 * present, the ability to configure arbitrary orderings of TLS Ciphers is not allowed, and only a curated list of
 * vetted TlsCipherPref's are exposed.
 */
export enum TlsCipherPreference {

    /**
     * The underlying platform's default TLS Cipher Preference ordering. This is usually the best option, as it will be
     * automatically updated as the underlying OS or platform changes, and will always be supported on all platforms.
     */
    Default = 0,

    /**
     * A TLS Cipher Preference ordering that supports TLS 1.0 through TLS 1.3, and has Kyber Round 3 as its highest
     * priority post-quantum key exchange algorithm. PQ algorithms in this preference list will always be used in hybrid
     * mode, and will be combined with a classical ECDHE key exchange that is performed in addition to the PQ key
     * exchange. This preference makes a best-effort to negotiate a PQ algorithm, but if the peer does not support any
     * PQ algorithms the TLS connection will fall back to a single classical algorithm for key exchange (such as ECDHE
     * or RSA).
     * NIST has announced that they plan to eventually standardize Kyber. However, the NIST standardization process might
     * introduce minor changes that could cause the final Kyber standard to differ from the Kyber Round 3 implementation
     * available in this preference list.
     */
    PQ_TLSv1_0_2021_05 = 6,

    /**
     * Recommended default policy with post-quantum algorithm support. This policy may change over time.
     */
    PQ_Default = 8,

    /**
     * A TLS Cipher Preference ordering that supports TLS 1.2 through TLS 1.3, and does not include CBC cipher suites.
     * It is FIPS-complaint.
     */
    TLSv1_2_2025_07 = 9
}

/**
 * Returns true if the supplied TlsCipherPreference is supported on the current platform, false otherwise.
 *
 * @param tls_cipher_preference - cipher preference to check support for
 *
 * nodejs only.
 * @category TLS
 */
export function tls_cipher_preference_is_supported(tls_cipher_preference: TlsCipherPreference) : boolean {
    return crt_native.io_tls_cipher_preference_is_supported(tls_cipher_preference);
}

/**
 * Options for creating a {@link ClientTlsContext} or {@link ServerTlsContext}.
 *
 * nodejs only.
 * @category TLS
 */
export class TlsContextOptions {
    /** Minimum version of TLS to support. Uses OS/system default if unspecified. */
    public min_tls_version: TlsVersion = TlsVersion.Default;
    /** Path to a single file with all trust anchors in it, in PEM format */
    public ca_filepath?: string;
    /** Path to directory containing trust anchors. Only used on Unix-style systems. */
    public ca_dirpath?: string;
    /** String with all trust anchors in it, in PEM format */
    public certificate_authority?: string;
    /** List of ALPN protocols to be used on platforms which support ALPN */
    public alpn_list: string[] = [];
    /** Path to certificate, in PEM format */
    public certificate_filepath?: string;
    /** Certificate, in PEM format */
    public certificate?: string;
    /** Path to private key, in PEM format */
    public private_key_filepath?: string;
    /** Private key, in PEM format */
    public private_key?: string;
    /** Path to certificate, in PKCS#12 format. Currently, only supported on OSX */
    public pkcs12_filepath?: string;
    /** Password for PKCS#12. Currently, only supported on OSX. */
    public pkcs12_password?: string;
    /** PKCS#11 options. Currently, only supported on Unix */
    public pkcs11_options?: TlsContextOptions.Pkcs11Options;
    /** Path to certificate in a Windows cert store. Windows only. */
    public windows_cert_store_path?: string;
    /** TLS cipher preferences; Linux only */
    public tls_cipher_preference?: TlsCipherPreference;

    /**
     * In client mode, this turns off x.509 validation. Don't do this unless you are testing.
     * It is much better to just override the default trust store and pass the self-signed
     * certificate as the ca_file argument.
     *
     * In server mode (ServerTlsContext), this defaults to false. If you want to enforce mutual TLS on the server,
     * set this to true.
     */
    public verify_peer: boolean = true;

    /**
     * Overrides the default system trust store.
     * @param ca_dirpath - Only used on Unix-style systems where all trust anchors are
     * stored in a directory (e.g. /etc/ssl/certs).
     * @param ca_filepath - Single file containing all trust CAs, in PEM format
     */
    override_default_trust_store_from_path(ca_dirpath?: string, ca_filepath?: string) {
        this.ca_dirpath = ca_dirpath;
        this.ca_filepath = ca_filepath;
    }

    /**
     * Overrides the default system trust store.
     * @param certificate_authority - String containing all trust CAs, in PEM format
     */
    override_default_trust_store(certificate_authority: string) {
        this.certificate_authority = certificate_authority;
    }

    /**
     * Create options configured for mutual TLS in client mode,
     * with client certificate and private key provided as in-memory strings.
     * @param certificate - Client certificate file contents, in PEM format
     * @param private_key - Client private key file contents, in PEM format
     *
     * @returns newly configured TlsContextOptions object
     */
    static create_client_with_mtls(certificate: string, private_key: string): TlsContextOptions {
        let opt = new TlsContextOptions();
        opt.certificate = certificate;
        opt.private_key = private_key;
        opt.verify_peer = true;
        return opt;
    }

    /**
     * Create options configured for mutual TLS in client mode,
     * with client certificate and private key provided via filepath.
     * @param certificate_filepath - Path to client certificate, in PEM format
     * @param private_key_filepath - Path to private key, in PEM format
     *
     * @returns newly configured TlsContextOptions object
     */
    static create_client_with_mtls_from_path(certificate_filepath: string, private_key_filepath: string): TlsContextOptions {
        let opt = new TlsContextOptions();
        opt.certificate_filepath = certificate_filepath;
        opt.private_key_filepath = private_key_filepath;
        opt.verify_peer = true;
        return opt;
    }

    /**
     * Create options for mutual TLS in client mode,
     * with client certificate and private key bundled in a single PKCS#12 file.
     * @param pkcs12_filepath - Path to PKCS#12 file containing client certificate and private key.
     * @param pkcs12_password - PKCS#12 password
     *
     * @returns newly configured TlsContextOptions object
    */
    static create_client_with_mtls_pkcs12_from_path(pkcs12_filepath: string, pkcs12_password: string): TlsContextOptions {
        let opt = new TlsContextOptions();
        opt.pkcs12_filepath = pkcs12_filepath;
        opt.pkcs12_password = pkcs12_password;
        opt.verify_peer = true;
        return opt;
    }

    /**
     * @deprecated Renamed [[create_client_with_mtls_pkcs12_from_path]]
     */
    static create_client_with_mtls_pkcs_from_path(pkcs12_filepath: string, pkcs12_password: string): TlsContextOptions {
        return this.create_client_with_mtls_pkcs12_from_path(pkcs12_filepath, pkcs12_password);
    }

    /**
     * Create options configured for mutual TLS in client mode,
     * using a PKCS#11 library for private key operations.
     *
     * NOTE: This configuration only works on Unix devices.
     *
     * @param options - PKCS#11 options
     *
     * @returns newly configured TlsContextOptions object
     */
    static create_client_with_mtls_pkcs11(options: TlsContextOptions.Pkcs11Options): TlsContextOptions {
        let opt = new TlsContextOptions();
        opt.pkcs11_options = options;
        opt.verify_peer = true;
        return opt;
    }

    /**
     * Create options configured for mutual TLS in client mode,
     * using a certificate in a Windows certificate store.
     *
     * NOTE: Windows only.
     *
     * @param certificate_path - Path to certificate in a Windows certificate store.
     *      The path must use backslashes and end with the certificate's thumbprint.
     *      Example: `CurrentUser\MY\A11F8A9B5DF5B98BA3508FBCA575D09570E0D2C6`
     */
    static create_client_with_mtls_windows_cert_store_path(certificate_path: string): TlsContextOptions {
        let opt = new TlsContextOptions();
        opt.windows_cert_store_path = certificate_path;
        opt.verify_peer = true;
        return opt;
    }

    /**
     * Creates TLS context with peer verification disabled, along with a certificate and private key
     * @param certificate_filepath - Path to certificate, in PEM format
     * @param private_key_filepath - Path to private key, in PEM format
     *
     * @returns newly configured TlsContextOptions object
     */
    static create_server_with_mtls_from_path(certificate_filepath: string, private_key_filepath: string): TlsContextOptions {
        let opt = new TlsContextOptions();
        opt.certificate_filepath = certificate_filepath;
        opt.private_key_filepath = private_key_filepath;
        opt.verify_peer = false;
        return opt;
    }

    /**
     * Creates TLS context with peer verification disabled, along with a certificate and private key
     * in PKCS#12 format
     * @param pkcs12_filepath - Path to certificate, in PKCS#12 format
     * @param pkcs12_password - PKCS#12 Password
     *
     * @returns newly configured TlsContextOptions object
     */
    static create_server_with_mtls_pkcs_from_path(pkcs12_filepath: string, pkcs12_password: string): TlsContextOptions {
        let opt = new TlsContextOptions();
        opt.pkcs12_filepath = pkcs12_filepath;
        opt.pkcs12_password = pkcs12_password;
        opt.verify_peer = false;
        return opt;
    }
}

export namespace TlsContextOptions {

    /**
     * Options for TLS using a PKCS#11 library for private key operations.
     *
     * Unix only. nodejs only.
     *
     * @see [[TlsContextOptions.create_client_with_mtls_pkcs11]]
     */
    export type Pkcs11Options = {
        /**
         * Use this PKCS#11 library.
         */
        pkcs11_lib: Pkcs11Lib,

        /**
         * Use this PIN to log the user into the PKCS#11 token. Pass `null`
         * to log into a token with a "protected authentication path".
         */
        user_pin: null | string,

        /**
         * Specify the slot ID containing a PKCS#11 token. If not specified, the token
         * will be chosen based on other criteria (such as [[token_label]]).
         */
        slot_id?: number,

        /**
         * Specify the label of the PKCS#11 token to use. If not specified, the token
         * will be chosen based on other criteria (such as [[slot_id]]).
         */
        token_label?: string,

        /**
         * Specify the label of the private key object on the PKCS#11 token. If not
         * specified, the key will be chosen based on other criteria (such as being the
         * only available private key on the token).
         */
        private_key_object_label?: string,

        /**
         * Use this X.509 certificate (file on disk).
         * The certificate must be PEM-formatted.
         * The certificate may be specified by other means instead
         * (ex: [[cert_file_contents]])
         */
        cert_file_path?: string,

        /**
         * Use this X.509 certificate (contents in memory).
         * The certificate must be PEM-formatted.
         * The certificate may be specified by other means instead
         * (ex: [[cert_file_path]])
         */
        cert_file_contents?: string,
    }
}

/**
 * Abstract base TLS context used for client/server TLS communications over sockets.
 *
 * @see ClientTlsContext
 * @see ServerTlsContext
 *
 * nodejs only.
 * @category TLS
 */
export abstract class TlsContext extends NativeResource {
    constructor(ctx_opt: TlsContextOptions) {
        if (ctx_opt == null || ctx_opt == undefined) {
            throw new CrtError("TlsContext constructor: ctx_opt not defined");
        }
        super(crt_native.io_tls_ctx_new(
            ctx_opt.min_tls_version,
            ctx_opt.ca_filepath,
            ctx_opt.ca_dirpath,
            ctx_opt.certificate_authority,
            (ctx_opt.alpn_list && ctx_opt.alpn_list.length > 0) ? ctx_opt.alpn_list.join(';') : undefined,
            ctx_opt.certificate_filepath,
            ctx_opt.certificate,
            ctx_opt.private_key_filepath,
            ctx_opt.private_key,
            ctx_opt.pkcs12_filepath,
            ctx_opt.pkcs12_password,
            ctx_opt.pkcs11_options,
            ctx_opt.windows_cert_store_path,
            ctx_opt.tls_cipher_preference,
            ctx_opt.verify_peer));
    }
}

/**
 * TLS context used for client TLS communications over sockets. If no
 * options are supplied, the context will default to enabling peer verification
 * only.
 *
 * nodejs only.
 * @category TLS
 */
export class ClientTlsContext extends TlsContext {
    constructor(ctx_opt?: TlsContextOptions) {
        if (!ctx_opt) {
            ctx_opt = new TlsContextOptions()
            ctx_opt.verify_peer = true;
        }
        super(ctx_opt);
    }
}

/**
 * TLS context used for server TLS communications over sockets. If no
 * options are supplied, the context will default to disabling peer verification
 * only.
 *
 * nodejs only.
 * @category TLS
 */
export class ServerTlsContext extends TlsContext {
    constructor(ctx_opt?: TlsContextOptions) {
        if (!ctx_opt) {
            ctx_opt = new TlsContextOptions();
            ctx_opt.verify_peer = false;
        }
        super(ctx_opt);
    }
}

/**
 * TLS options that are unique to a given connection using a shared TlsContext.
 *
 * nodejs only.
 * @category TLS
 */
export class TlsConnectionOptions extends NativeResource {
    constructor(readonly tls_ctx: TlsContext, readonly server_name?: string, readonly alpn_list: string[] = []) {
        if (tls_ctx == null || tls_ctx == undefined) {
            throw new CrtError("TlsConnectionOptions constructor: tls_ctx not defined");
        }
        super(crt_native.io_tls_connection_options_new(
            tls_ctx.native_handle(),
            server_name,
            (alpn_list && alpn_list.length > 0) ? alpn_list.join(';') : undefined
        ));
    }
}

/**
 * Handle to a loaded PKCS#11 library.
 *
 * For most use cases, a single instance of Pkcs11Lib should be used
 * for the lifetime of your application.
 *
 * nodejs only.
 * @category TLS
 */
export class Pkcs11Lib extends NativeResource {

    /**
     * @param path - Path to PKCS#11 library.
     * @param behavior - Specifies how `C_Initialize()` and `C_Finalize()`
     *                   will be called on the PKCS#11 library.
     */
    constructor(path: string, behavior: Pkcs11Lib.InitializeFinalizeBehavior = Pkcs11Lib.InitializeFinalizeBehavior.DEFAULT) {
        super(crt_native.io_pkcs11_lib_new(path, behavior));
    }

    /**
     * Release the PKCS#11 library immediately, without waiting for the GC.
     */
    close() {
        crt_native.io_pkcs11_lib_close(this.native_handle());
    }
}

export namespace Pkcs11Lib {

    /**
     * Controls `C_Initialize()` and `C_Finalize()` are called on the PKCS#11 library.
     */
    export enum InitializeFinalizeBehavior {
        /**
         * Default behavior that accommodates most use cases.
         *
         * `C_Initialize()` is called on creation, and "already-initialized"
         * errors are ignored. `C_Finalize()` is never called, just in case
         * another part of your application is still using the PKCS#11 library.
         */
        DEFAULT = 0,

        /**
         * Skip calling `C_Initialize()` and `C_Finalize()`.
         *
         * Use this if your application has already initialized the PKCS#11 library,
         * and you do not want `C_Initialize()` called again.
         */
        OMIT = 1,

        /**
         * `C_Initialize()` is called on creation and `C_Finalize()` is called on cleanup.
         *
         * If `C_Initialize()` reports that's it's already initialized, this is
         * treated as an error. Use this if you need perfect cleanup (ex: running
         * valgrind with --leak-check).
         */
        STRICT = 2,
    }
}
