/**
 * @module node-opcua-transport
 */
import { EventEmitter } from "node:events";
import { assert } from "node-opcua-assert";

import { decodeStatusCode, decodeString, decodeUInt32 } from "node-opcua-basic-types";
import { BinaryStream } from "node-opcua-binary-stream";

import { readMessageHeader, SequenceHeader } from "node-opcua-chunkmanager";
import { hexDump, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
import { type MessageHeader, PacketAssembler, type PacketInfo } from "node-opcua-packet-assembler";
import type { StatusCode } from "node-opcua-status-code";
import { get_clock_tick } from "node-opcua-utils";
import { StatusCodes2 } from "./status_codes";

const doPerfMonitoring = process.env.NODEOPCUADEBUG && process.env.NODEOPCUADEBUG.indexOf("PERF") >= 0;

const errorLog = make_errorLog("MessageBuilder");
const _debugLog = make_debugLog("MessageBuilder");
const warningLog = make_warningLog("MessageBuilder");

export function readRawMessageHeader(data: Buffer): PacketInfo {
    const messageHeader = readMessageHeader(new BinaryStream(data));
    return {
        extra: "",
        length: messageHeader.length,
        messageHeader
    };
}

export interface MessageBuilderBaseOptions {
    signatureLength?: number;
    maxMessageSize?: number;
    maxChunkCount?: number;
    maxChunkSize?: number;
}

export interface MessageBuilderBaseEvents {
    /**
     * notify the observers that a new message is being built
     * @event startChunk
     */
    startChunk: [info: PacketInfo, data: Buffer];
    /**
     * notify the observers that new message chunk has been received
     * @event chunk
     */
    chunk: [chunk: Buffer];
    /**
     * notify the observers that an error has occurred
     * @event error
     */
    error: [err: Error, statusCode: StatusCode, requestId: number | null];
    /**
     * notify the observers that a full message has been received
     * @event full_message_body
     */
    full_message_body: [fullMessageBody: Buffer];
    /**
     * notify the observers that a request has been abandoned
     * @event abandon
     */
    abandon: [requestId: number];
}
/**
 *
 */
export class MessageBuilderBase extends EventEmitter {
    public static defaultMaxChunkCount = 1000;
    public static defaultMaxMessageSize = 1024 * 64 * 1024; // 64Mo
    public static defaultMaxChunkSize = 1024 * 8;

    public readonly signatureLength: number;
    public readonly maxMessageSize: number;
    public readonly maxChunkCount: number;
    public readonly maxChunkSize: number;

    public readonly options: MessageBuilderBaseOptions;
    public channelId: number;
    public totalMessageSize: number;
    public sequenceHeader: SequenceHeader | null;

    public _tick0: number;
    public _tick1: number;

    protected id: string;

    protected totalBodySize: number;
    protected messageChunks: Buffer[];
    protected messageHeader?: MessageHeader;

    readonly #_packetAssembler: PacketAssembler;
    #_securityDefeated: boolean;
    #_hasReceivedError: boolean;
    #blocks: Buffer[];
    readonly #_expectedChannelId: number;
    #offsetBodyStart: number;

    constructor(options?: MessageBuilderBaseOptions) {
        super();

        this.id = "";

        this._tick0 = 0;
        this._tick1 = 0;
        this.#_hasReceivedError = false;
        this.#blocks = [];
        this.messageChunks = [];
        this.#_expectedChannelId = 0;

        options = options || {
            maxMessageSize: 0,
            maxChunkCount: 0,
            maxChunkSize: 0
        };

        this.signatureLength = options.signatureLength || 0;

        this.maxMessageSize = options.maxMessageSize || MessageBuilderBase.defaultMaxMessageSize;
        this.maxChunkCount = options.maxChunkCount || MessageBuilderBase.defaultMaxChunkCount;
        this.maxChunkSize = options.maxChunkSize || MessageBuilderBase.defaultMaxChunkSize;

        this.options = options;

        this.#_packetAssembler = new PacketAssembler({
            minimumSizeInBytes: 8,
            maxChunkSize: this.maxChunkSize,
            readChunkFunc: readRawMessageHeader
        });

        this.#_packetAssembler.on("chunk", (messageChunk) => this.#_feed_messageChunk(messageChunk));

        this.#_packetAssembler.on("startChunk", (info, data) => {
            if (doPerfMonitoring) {
                // record tick 0: when the first data is received
                this._tick0 = get_clock_tick();
            }
            this.emit("startChunk", info, data);
        });

        this.#_packetAssembler.on("error", (err) => {
            warningLog("packet assembler ", err.message);
            this._report_error(StatusCodes2.BadTcpMessageTooLarge, `packet assembler: ${err.message}`);
        });

        this.#_securityDefeated = false;
        this.totalBodySize = 0;
        this.totalMessageSize = 0;
        this.channelId = 0;
        this.#offsetBodyStart = 0;
        this.sequenceHeader = null;
        this.#_init_new();
    }

    public dispose(): void {
        this.removeAllListeners();
    }

    /**
     * Feed message builder with some data

     * @param data
     */
    public feed(data: Buffer): void {
        if (!this.#_securityDefeated && !this.#_hasReceivedError) {
            this.#_packetAssembler.feed(data);
        }
    }

    protected _decodeMessageBody(_fullMessageBody: Buffer): boolean {
        return true;
    }

    protected _read_headers(binaryStream: BinaryStream): boolean {
        try {
            this.messageHeader = readMessageHeader(binaryStream);
            // assert(binaryStream.length === 8, "expecting message header to be 8 bytes");
            this.channelId = binaryStream.readUInt32();
            // assert(binaryStream.length === 12);

            // verifying secure ChannelId
            if (this.#_expectedChannelId && this.channelId !== this.#_expectedChannelId) {
                return this._report_error(StatusCodes2.BadTcpSecureChannelUnknown, "Invalid secure channel Id");
            }
            return true;
        } catch (err) {
            return this._report_error(StatusCodes2.BadTcpInternalError, `_read_headers error ${err instanceof Error ? err.message : String(err)}`);
        }
    }

    protected _report_abandon(_channelId: number, _tokenId: number, sequenceHeader: SequenceHeader): false {
        // the server has not been able to send a complete message and has abandoned the request
        // the connection can probably continue
        this.#_hasReceivedError = false; ///
        this.emit("abandon", sequenceHeader.requestId);
        return false;
    }

    protected _report_error(statusCode: StatusCode, errorMessage: string): false {
        this.#_hasReceivedError = true;
        errorLog("Error  ", this.id, errorMessage);
        // xx errorLog(new Error());
        this.emit("error", new Error(errorMessage), statusCode, this.sequenceHeader?.requestId || null);
        return false;
    }

    #_init_new() {
        this.#_securityDefeated = false;
        this.#_hasReceivedError = false;
        this.totalBodySize = 0;
        this.totalMessageSize = 0;
        this.#blocks = [];
        this.messageChunks = [];
    }

    /**
     * append a message chunk

     * @param chunk
     * @private
     */
    #_append(chunk: Buffer): boolean {
        if (this.#_hasReceivedError) {
            // the message builder is in error mode and further message chunks should be discarded.
            return false;
        }

        if (this.messageChunks.length + 1 > this.maxChunkCount) {
            return this._report_error(StatusCodes2.BadTcpMessageTooLarge, `max chunk count exceeded: ${this.maxChunkCount}`);
        }

        this.messageChunks.push(chunk);
        this.totalMessageSize += chunk.length;

        if (this.totalMessageSize > this.maxMessageSize) {
            return this._report_error(
                StatusCodes2.BadTcpMessageTooLarge,
                `max message size exceeded: ${this.maxMessageSize} : total message size ${this.totalMessageSize}`
            );
        }

        const binaryStream = new BinaryStream(chunk);

        if (!this._read_headers(binaryStream)) {
            return false; // error already reported
        }

        assert(binaryStream.length >= 12);

        // verify message chunk length
        if (this.messageHeader?.length !== chunk.length) {
            // tslint:disable:max-line-length
            return this._report_error(
                StatusCodes2.BadTcpInternalError,
                `Invalid messageChunk size: the provided chunk is ${chunk.length} bytes long but header specifies ${
                    this.messageHeader?.length
                }`
            );
        }

        // the start of the message body block
        const offsetBodyStart = binaryStream.length;

        // the end of the message body block
        const offsetBodyEnd = binaryStream.buffer.length;

        this.totalBodySize += offsetBodyEnd - offsetBodyStart;
        this.#offsetBodyStart = offsetBodyStart;

        // add message body to a queue
        // We use subarray here to avoid copy.
        // This assumes PacketAssembler manages the buffer lifecycle appropriately.
        const sharedBuffer = chunk.subarray(this.#offsetBodyStart, offsetBodyEnd);
        this.#blocks.push(sharedBuffer);

        return true;
    }

    #_feed_messageChunk(chunk: Buffer): boolean {
        assert(chunk);
        const messageHeader = readMessageHeader(new BinaryStream(chunk));
        this.emit("chunk", chunk);

        if (messageHeader.isFinal === "F") {
            if (messageHeader.msgType === "ERR") {
                const binaryStream = new BinaryStream(chunk);
                binaryStream.length = 8;
                const errorCode = decodeStatusCode(binaryStream);
                const message = decodeString(binaryStream);
                this._report_error(errorCode, message || "Error message not specified");
                return true;
            } else {
                this.#_append(chunk);
                // last message
                if (this.#_hasReceivedError) {
                    return false;
                }

                const fullMessageBody: Buffer = this.#blocks.length === 1 ? this.#blocks[0] : Buffer.concat(this.#blocks);

                if (doPerfMonitoring) {
                    // record tick 1: when a complete message has been received ( all chunks assembled)
                    this._tick1 = get_clock_tick();
                }
                this.emit("full_message_body", fullMessageBody);

                const messageOk = this._decodeMessageBody(fullMessageBody);
                // be ready for next block
                this.#_init_new();
                return messageOk;
            }
        } else if (messageHeader.isFinal === "A") {
            try {
                // only valid for MSG, according to spec
                const stream = new BinaryStream(chunk);
                readMessageHeader(stream);
                assert(stream.length === 8);
                // instead of
                //   const securityHeader = new SymmetricAlgorithmSecurityHeader();
                //   securityHeader.decode(stream);

                const channelId = stream.readUInt32();
                const tokenId = decodeUInt32(stream);

                const sequenceHeader = new SequenceHeader();
                sequenceHeader.decode(stream);

                return this._report_abandon(channelId, tokenId, sequenceHeader);
            } catch (err) {
                const errMessage = err instanceof Error ? err.message : String(err);
                warningLog(hexDump(chunk));
                warningLog("Cannot interpret message chunk: ", errMessage);
                return this._report_error(
                    StatusCodes2.BadTcpInternalError,
                    `Error decoding message header ${errMessage}`
                );
            }
        } else if (messageHeader.isFinal === "C") {
            return this.#_append(chunk);
        }
        return false;
    }
}
