const dgram = require('dgram');
const os = require('os');
const fs = require('fs');
const path = require('path');

const PacketEnum = require('./packet_enum');
const PacketTypeEnum = require('./packet_type_enum');
const ParamDef = require('./param_def');
const DataOutputX = require('../io/data-outputx');
const TraceContextManager = require('../trace/trace-context-manager');
const PackageCtrHelper = require('../../lib/control/packagectr-helper');
const ActiveTransaction = require('../counter/task/activetransaction');
const GCAction = require('../system/gc-action');
const DataOuputX = require('../../lib/io/data-outputx');
const Logger = require('../logger');

class UdpSession {
    constructor() {
        this.socket = null;
        this.bufferArr = [];
        this.threadLock = false; // Simple lock simulation
        this.readTimeout = 5000; // 5 seconds
        this.serverHost = PacketEnum.SERVER;
        this.serverPort = PacketEnum.PORT;
        this.isConnected = false; // connected-UDP 여부

        // LLM Go Agent 전용 소켓
        this.llmSocket = null;
        this.llmPort = 0;
        this.llmConnected = false;
    }

    /**
     * Initialize UDP socket
     */
    udp(config) {
        const self = this;
        const conf = config || {};

        // Close existing socket if open
        if (self.socket) {
            try {
                self.socket.close();
            } catch (e) {
                Logger.printError("WHATAP-UDP-002", "Error closing UDP socket", e, false);
            }
        }

        // Create UDP socket
        self.socket = dgram.createSocket('udp4');

        // Check if Unix socket is configured
        const whatapHome = process.env.WHATAP_HOME;

        if (conf.unix_socket_enabled === true && whatapHome) {
            // Unix socket not directly supported in Node.js dgram
            // We'll use the regular UDP socket with the configured server and port
            Logger.print("WHATAP-UDP-003", "Unix socket not supported in Node.js, using UDP instead", false);
        }

        // Configure server info
        self.serverHost = conf.net_udp_host || PacketEnum.SERVER;
        self.serverPort = parseInt(conf.net_udp_port || PacketEnum.PORT);

        // console.log(`UDP initialized with server ${self.serverHost}:${self.serverPort}`);

        // "connected UDP" 로 전환: lsof에서 peer가 보이도록
        self.socket.connect(self.serverPort, self.serverHost, () => {
            self.isConnected = true;
            // 참고: UDP라 실제 연결 성사 여부 확인 절차는 없음 (peer 고정만 됨)
            // 필요시 ephemeral 로컬 포트를 고정하고 싶으면 bind 후 connect 실행
            // self.socket.bind(0, '127.0.0.1', () => self.socket.connect(...));
        });

        // Set up error handling
        self.socket.on('error', function (err) {
            Logger.printError("WHATAP-UDP-001", "UDP socket error", err, false);
        });
        self.socket.on('close', () => { self.isConnected = false; });

        self.socket.on('message', function (msg, rinfo) {
            self.handleMessage(msg, rinfo);
        });

        return self.socket;
    }

    /**
     * Handle incoming UDP messages
     */
    handleMessage(msg, rinfo) {
        const self = this;
        try {
            const received = msg.toString();
            const parts = received.split(',');

            if (parts.length >= 2) {
                const paramId = parseInt(parts[0]);
                const request = parts[1];
                const extra = parts.length > 2 ? parts[2] : '';

                self.handle(paramId, request, extra);
            }
        } catch (e) {
            Logger.printError("WHATAP-UDP-004", "Error handling UDP message", e, false);
        }
    }

    /**
     * Handle received requests
     */
    handle(paramId, request, extra) {
        const self = this;
        if(!paramId)
            return;
        
        try {
            let threadId = 0;
            let pid = 0;

            if (extra) {
                const extraData = extra.replace(' ', ', ').split(', ');
                if (extraData.length > 1) {
                    threadId = parseInt(extraData[1]);
                    // In Node.js there's no direct thread ID, so using PID
                    pid = process.pid;
                }
            }

            let data = '';
            let datas = [String(paramId), request];

            if (paramId === ParamDef.SYSTEM_GC){
                const gcAction = new GCAction();
                const p = gcAction.execSystemGC();

                const bout = new DataOuputX();
                bout.writePack(p, null);
                const packbytes = bout.toByteArray();

                self.send_relaypack(packbytes)
            } else if (paramId === ParamDef.MODULE_DEPENDENCY) {
                const dependencies = PackageCtrHelper.getDependencies();
                // result format: "axios x.x.x, whatap x.x.x, etc x,x,x"
                const result = Object.entries(dependencies)
                    .map(([name, version]) => `${name} ${version}`)
                    .join(', ');
                datas.push(result);
                self.send_packet(PacketTypeEnum.TX_PARAM, null, datas)
            } else if (paramId === ParamDef.GET_ACTIVE_STATS) {
                const stats = ActiveTransaction.getActiveStats();
                let statsStr = stats.join(',');

                // Append LLM meter count for Go Agent: "method,sql,httpc,dbc,socket|LLM:count"
                try {
                    const llmSender = require('../llm/llm-sender');
                    const llmCount = llmSender.getLlmMeterCountAndReset();
                    if (llmCount > 0) {
                        statsStr += '|LLM:' + llmCount;
                    }
                } catch (e) {}

                datas = [statsStr];
                self.send_packet(PacketTypeEnum.ACTIVE_STATS, null, datas);
                // LLM meter용: LLM Go agent(--llm, LLMMode=true)의 TaskLLM이
                // MeterLLM bucket을 소비하려면 같은 ACTIVE_STATS를 llmSocket에도 보내야 함.
                try {
                    if (self.llmSocket) {
                        self._sendActiveStatsToLlm(statsStr);
                    } else if (conf.getProperty('llm_enabled', false)) {
                        // lazy init
                        self.udp_llm();
                        if (self.llmSocket) self._sendActiveStatsToLlm(statsStr);
                    }
                } catch (e) {}
            }
            return;
        } catch (e) {
            Logger.printError("WHATAP-UDP-005", "Error in handle method", e, false);
        }
    }

    /**
     * Send a packet with trace data
     * @param {number} packet_type - Type of packet from PacketTypeEnum
     * @param {object} ctx - Trace context
     * @param {array} datas - Array of data strings to send
     */
    send_packet(packet_type, ctx, datas = []) {
        const self = this;

        try {
            // Calculate total buffer size
            let totalSize = 0;
            for (let i = 0; i < self.bufferArr.length; i++) {
                totalSize += self.bufferArr[i].length;
            }

            // Add estimated size of new data
            let newDataSize = PacketEnum.PACKET_BODY_REQUIRED_SIZE + 1;
            for (let i = 0; i < datas.length; i++) {
                const data = String(datas[i]);
                newDataSize += Buffer.byteLength(data, 'utf8') + 2; // +2 for UTF encoding length
            }

            if (totalSize + newDataSize > PacketEnum.PACKET_BUFFER_SIZE) {
                self.send(packet_type, ctx);
            }

            if (!datas || datas.length === 0) {
                return;
            }

            // Create packet
            const dout = new DataOutputX();
            var startHeaderBufferSize = dout.size();

            // Write header
            dout.writeByte(packet_type);
            dout.writeInt(PacketEnum.PACKET_VERSION);
            dout.writeInt(0); // Placeholder for length

            const startBodyBufferSize = dout.size();

            // Write body if context exists
            if (ctx) {
                dout.writeLong(ctx.id);
                dout.writeLong(ctx.start_time);
                dout.writeInt(ctx.elapsed);
                dout.writeLong(Math.floor(Date.now() / 1000));
                dout.writeLong(0);
                dout.writeInt(process.pid);
            }

            // Write data with length limit check
            let diff = PacketEnum.PACKET_BUFFER_SIZE - totalSize - (PacketEnum.PACKET_BODY_REQUIRED_SIZE + 1);

            for (let i = 0; i < datas.length; i++) {
                let data = String(datas[i]).slice(0, PacketEnum.DATA_SIZE_LIMIT);
                diff -= (Buffer.byteLength(data, 'utf8') + 2); // Account for UTF encoding overhead

                if (diff < 0) {
                    data = ' ';
                    Logger.print("WHATAP-UDP-006", "UDP message too long, truncated", false);
                }

                dout.writeUTF(data);
            }

            // Update length in header
            dout.writeToPos(
                startHeaderBufferSize + PacketEnum.PACKET_HEADER_LEN_POS,
                dout.size() - startBodyBufferSize
            );

            // Add to buffer array (simulating thread lock)
            if (!self.threadLock) {
                self.threadLock = true;
                self.bufferArr.push(dout.toByteArray());
                self.threadLock = false;

                // Flush in certain conditions
                if (!ctx || packet_type === PacketTypeEnum.TX_START || packet_type === PacketTypeEnum.TX_END) {
                    self.send(packet_type, ctx);
                }
            }
        } catch (e) {
            Logger.printError("WHATAP-UDP-007", "Error sending packet", e, false);
        }
    }

    /**
     * Send the buffered packets
     */
    send(packet_type, ctx) {
        const self = this;

        if (!self.socket || self.bufferArr.length === 0) {
            return;
        }

        try {
            // Combine all buffers
            const sendbuf = Buffer.concat(self.bufferArr);

            // Send via UDP
            const cb = (err) => {
                if (err) {
                    Logger.printError("WHATAP-UDP-008", "Failed to send UDP packet", err, false);
                    // Consider reinitializing UDP connection on error
                    self.udp();
                }
            };

            if (self.isConnected) {
                // connected UDP: 목적지 인자 없이 전송
                self.socket.send(sendbuf, cb);
            } else {
                // fallback (미연결 상태)
                self.socket.send(sendbuf, 0, sendbuf.length, self.serverPort, self.serverHost, cb);
            }
        } catch (e) {
            Logger.printError("WHATAP-UDP-009", "Error in send method", e, false);
        } finally {
            // Clear buffer array
            self.bufferArr = [];

            // Handle context cleanup for TX_END
            if (packet_type === PacketTypeEnum.TX_END && ctx && ctx.id) {
                if (ctx.id) {
                    TraceContextManager.end(ctx.id)
                }
            }
        }
    }

    /**
     * ACTIVE_STATS 응답을 LLM Go agent(llmSocket)에도 동일 포맷으로 송신.
     * LLMMode=true agent의 TaskLLM이 MeterLLM.Add를 호출하려면 같은 packet이 필요.
     */
    _sendActiveStatsToLlm(statsStr) {
        const self = this;
        if (!self.llmSocket) return;
        try {
            // ACTIVE_STATS 패킷 바디 포맷 (send_packet과 동일 구조)
            const dout = new DataOutputX();
            dout.writeByte(PacketTypeEnum.ACTIVE_STATS);
            dout.writeInt(PacketEnum.PACKET_VERSION);
            dout.writeInt(0); // length placeholder
            const bodyStart = dout.size();
            // ctx 없음 → body 필드 생략 (send_packet의 ctx null 분기와 동일)
            const data = String(statsStr).slice(0, PacketEnum.DATA_SIZE_LIMIT);
            dout.writeUTF(data);
            // length 덮어쓰기
            dout.writeToPos(
                PacketEnum.PACKET_HEADER_LEN_POS,
                dout.size() - bodyStart
            );
            const buf = dout.toByteArray();
            self.llmSocket.send(buf, 0, buf.length, self.llmPort, self.serverHost, function(err) {
                if (err) Logger.printError('WHATAP-LLM-UDP-006', 'LLM ACTIVE_STATS send failed', err, false);
            });
        } catch (e) {
            Logger.printError('WHATAP-LLM-UDP-007', 'LLM ACTIVE_STATS encode failed', e, false);
        }
    }

    /**
     * Initialize LLM-dedicated UDP socket for LLM Go Agent
     */
    udp_llm(config) {
        const self = this;
        const conf = config || require('../conf/configure');

        let llmPort = parseInt(conf.getProperty ? conf.getProperty('llm_net_udp_port', 0) : (conf.llm_net_udp_port || 0));
        if (!llmPort) {
            llmPort = self.serverPort + 100;
        }
        self.llmPort = llmPort;

        if (self.llmSocket) {
            try { self.llmSocket.close(); } catch (e) {}
        }

        try {
            self.llmSocket = dgram.createSocket('udp4');
            // UDP는 connect 불필요. connect()를 호출하면 connecting 상태에서 첫 패킷이 EISCONN으로 drop됨.
            // 항상 unconnected send 사용.
            self.llmConnected = false;
            self.llmSocket.on('error', (err) => {
                Logger.printError("WHATAP-LLM-UDP-002", "LLM UDP socket error", err, false);
            });
            self.llmSocket.on('close', () => {
                self.llmConnected = false;
            });
        } catch (e) {
            self.llmSocket = null;
        }
    }

    /**
     * Send LLM relay pack to LLM Go Agent socket
     */
    send_llm_relaypack(packbytes) {
        const self = this;

        if (!packbytes) return;

        // lazy init
        if (!self.llmSocket) {
            self.udp_llm();
        }
        if (!self.llmSocket) {
            return;
        }

        try {
            const packet_type = PacketTypeEnum.RELAY_PACK;
            const dout = new DataOutputX();
            dout.writeByte(packet_type);
            dout.writeInt(PacketEnum.PACKET_VERSION);
            dout.writeIntBytes(packbytes);

            const buf = dout.toByteArray();
            if (self.llmConnected) {
                self.llmSocket.send(buf, function(err) {
                    if (err) Logger.printError("WHATAP-LLM-UDP-004", "LLM send (connected) failed", err, false);
                });
            } else {
                self.llmSocket.send(buf, 0, buf.length, self.llmPort, self.serverHost, function(err) {
                    if (err) Logger.printError("WHATAP-LLM-UDP-005", "LLM send (unconnected) failed", err, false);
                });
            }
        } catch (e) {
            Logger.printError("WHATAP-LLM-UDP-003", "Error sending LLM relay pack", e, false);
        }
    }

    /**
     * Send relay pack data
     */
    send_relaypack(packbytes) {
        const self = this;
        const packet_type = PacketTypeEnum.RELAY_PACK;

        try {
            // Check buffer size and flush if needed
            let totalSize = 0;
            for (let i = 0; i < self.bufferArr.length; i++) {
                totalSize += self.bufferArr[i].length;
            }

            if (totalSize + PacketEnum.PACKET_BODY_REQUIRED_SIZE + 1 > PacketEnum.PACKET_BUFFER_SIZE) {
                self.send(packet_type, null);
            }

            if (!packbytes) {
                return;
            }

            // Create packet
            const dout = new DataOutputX();

            // Write header
            dout.writeByte(packet_type);
            dout.writeInt(PacketEnum.PACKET_VERSION);
            dout.writeIntBytes(packbytes);

            // Add to buffer
            if (!self.threadLock) {
                self.threadLock = true;
                self.bufferArr.push(dout.toByteArray());
                self.threadLock = false;
            }
        } catch (e) {
            Logger.printError("WHATAP-UDP-010", "Error sending relay pack", e, false);
        }
    }
}

// Export a singleton instance
module.exports = new UdpSession();