import {SerialPort} from 'serialport' // Import the SerialPort library for serial communication.
import debug from "debug"; // Import the debug library for logging.
import {ArduinoProxyPayload} from "./ArduinoProxyPayload"; // Import the payload class definition.
// Assuming Utils class exists and has generateRandomString method.
// import {Utils} from "./Utils";
import {ReadlineParser} from "@serialport/parser-readline";
import {Utils} from "./Utils"; // Import the ReadlineParser for parsing incoming data.

// Initialize debug logger for this module.
const log = debug("itender:arduinoProxy");

/**
 * Static class to manage the serial connection and communication with the Arduino proxy.
 */
export class ArduinoProxy {

    // Static member to hold the SerialPort instance.
    private static serialPort: SerialPort | null = null;
    // Static dictionary to store promise resolve/reject functions for pending requests, keyed by message ID.
    private static callbacks: Record<string, { resolve: Function, reject: Function }> = {};
    // Static member defining the encoding for serial communication.
    private static encoding: BufferEncoding = "utf-8"; // Use BufferEncoding type

    /**
     * Establishes a serial connection to the Arduino.
     * Searches for an Arduino device by manufacturer string.
     * @returns A promise that resolves when the connection is established, or rejects on error.
     */
    public static connect(): Promise<void> {
        return new Promise<void>(async (resolve, reject) => {
            log("Connecting to arduino...");
            // List available serial ports.
            let list = await SerialPort.list();

            // Find a port whose manufacturer string starts with "Arduino".
            let arduino = list.find((ele) => {
                if (!ele.manufacturer) return false;
                return ele.manufacturer.startsWith("Arduino");
            });

            // If no Arduino is found, reject the promise.
            if (!arduino) {
                log("No arduino found!");
                return reject("No arduino found. Is it plugged in?");
            }

            // Create a new SerialPort instance with the found path and baud rate.
            // The baud rate must match the Serial.begin() speed on the Arduino.
            this.serialPort = new SerialPort({
                path: arduino.path,
                baudRate: 9600,
                autoOpen: false, // Do not open immediately.
            });

            log(`Opening serial port ${arduino.path}...`);
            // Open the serial port.
            this.serialPort.open((err: Error | null | undefined) => {
                if (err) {
                    // Handle error during port opening.
                    log("Error whilst connecting to proxy (open serial-connection)");
                    log(err.name + "\n" + err.message + "\n" + err.stack);
                    this.serialPort = null; // Ensure serialPort is null on error
                    return reject("Could not open serial connection to arduino");
                }

                log("Serial port opened successfully.");

                // Create a ReadlineParser to parse incoming data line by line.
                // The Arduino uses Serial.println(), which sends \r\n.
                // Parsing by '\n' is common, or '\r' followed by trim() also works.
                // Sticking with '\r' as in the original code, relying on trim().
                const parser = this.serialPort!.pipe(new ReadlineParser({delimiter: '\r'}));

                // Attach a data listener to the parser.
                parser.on('data', (data) => this.onData(data));

                // Resolve the promise indicating successful connection.
                resolve();
            });

            // Add error handling for the serial port itself after opening
            this.serialPort.on('error', (err) => {
                log("SerialPort error:", err.message);
                // You might want to handle this more gracefully, e.g., attempt reconnection
                // or notify active requests of failure.
                // For simplicity here, we just log the error.
            });

            this.serialPort.on('close', () => {
                log("Serial port closed.");
                // Handle port closure - potentially clean up callbacks or attempt reconnection
            });
        });
    }

    /**
     * Disconnects from the Arduino by closing the serial port.
     */
    public static disconnect(): void {
        if (this.serialPort && this.serialPort.isOpen) {
            log("Closing serial port...");
            this.serialPort.close((err) => {
                if (err) {
                    log("Error closing serial port:", err.message);
                } else {
                    log("Serial port closed.");
                }
                this.serialPort = null;
                // Optionally reject any pending callbacks here as the connection is closed.
                for (const id in this.callbacks) {
                    this.callbacks[id].reject("Connection closed");
                    delete this.callbacks[id];
                }
            });
        } else {
            log("Serial port is not open, no need to disconnect.");
        }
    }

    /**
     * Sends a request payload to the Arduino and waits for a response.
     * @param request The ArduinoProxyPayload object containing the request details.
     * @param timeout The maximum time (in milliseconds) to wait for a response. Defaults to 1000ms.
     * @returns A promise that resolves with the response payload or rejects on timeout or error.
     */
    public static sendRequest(request: ArduinoProxyPayload, timeout: number = 1000): Promise<ArduinoProxyPayload> {
        return new Promise<ArduinoProxyPayload>(async (resolve, reject) => {
            // Pre-check if connection is lost and attempt to reconnect if necessary.
            // Note: This simple reconnect might not be sufficient for all error cases.
            if (!this.serialPort || !this.serialPort.isOpen) {
                log("Serial port not open, attempting to reconnect before sending request...");
                try {
                    await this.connect();
                    log("Reconnected successfully.");
                } catch (e) {
                    log("Failed to reconnect:", e);
                    return reject("Connection not available to send request");
                }
            }

            // Generate a unique ID for this request.
            // Assumes Utils.generateRandomString exists and returns a string.
            let id = Utils.generateRandomString(8); // Requires Utils class with this method.

            // Assign the generated ID to the request payload.
            request.id = id;
            // Store the resolve/reject functions for this request ID.
            this.callbacks[id] = {resolve: resolve, reject: reject};

            // Format the payload into the JSON structure expected by the Arduino.
            let formatted = {
                id: request.id,
                type: request.type,
                data: request.data // Use the data getter which returns the original data object.
            }

            log("Sending...", formatted);
            // Serialize the JSON object to a string and write it to the serial port.
            // Add a newline character at the end, which the Arduino's ReadlineParser expects (implicitly via trim).
            this.serialPort!.write(JSON.stringify(formatted) + '\n', ArduinoProxy.encoding, (err: Error | null | undefined) => {
                if (err) {
                    // Handle error during writing.
                    log(`I/O error on request ${id}:`, err.message);
                    // Reject the promise and clean up the callback.
                    if (this.callbacks[id]) {
                        this.callbacks[id].reject(`I/O error on request ${id}: ${err.message}`);
                        delete this.callbacks[id];
                    }
                } else {
                    log(`Request ${id} written to serial port.`);
                }
            });

            // Set a timeout for the response.
            const timeoutId = setTimeout(() => {
                log(`Timeout on request ${id}`);
                // If the callback still exists after the timeout, reject the promise.
                if (this.callbacks[id]) {
                    this.callbacks[id].reject(`Timeout on request ${id}`);
                    delete this.callbacks[id];
                }
            }, timeout);

            // Modify the resolve function to clear the timeout when a response is received.
            const originalResolve = this.callbacks[id].resolve;
            this.callbacks[id].resolve = (response: ArduinoProxyPayload) => {
                clearTimeout(timeoutId);
                originalResolve(response);
            };

            // Modify the reject function to clear the timeout when an error occurs.
            const originalReject = this.callbacks[id].reject;
            this.callbacks[id].reject = (error: any) => {
                clearTimeout(timeoutId);
                originalReject(error);
            };
        });
    }

    /**
     * Handles incoming data from the serial port.
     * Parses the data as JSON and resolves/rejects the corresponding promise based on the message ID.
     * @param data The raw data received from the serial port (string or buffer).
     */
    private static onData(data: string | Buffer): void {
        // Convert data to string and remove leading/trailing whitespace (including the newline).
        const dataString = data.toString().trim();
        log("Received raw:", dataString);

        try {
            // Parse the incoming data string as a JSON object.
            const json = JSON.parse(dataString) as ArduinoProxyPayload;
            log("Received parsed:", json);

            // Check if there is a pending request with the same ID.
            if (this.callbacks[json.id]) {
                // Resolve the promise for the corresponding request.
                this.callbacks[json.id].resolve(json);
                // Remove the callback as the request is fulfilled.
                delete this.callbacks[json.id];
                log("Answered request " + json.id);
            } else {
                // Log a warning if a response is received for which no callback is waiting.
                log("WARNING - Received a response from Arduino with ID " + json.id + " but no matching callback was found.");
                // This could happen if the Arduino sends unsolicited messages or if a timeout occurred
                // on the Node.js side just before the response was received.
            }
        } catch (e) {
            // Handle JSON parsing errors.
            log("ERROR - Failed to parse incoming data as JSON:", e);
            log("Invalid data:", dataString);
            // You might want to notify all pending requests of a communication error,
            // or implement a more sophisticated error recovery mechanism.
        }
    }
}