/**
 * Tapo Client Module
 * 
 * Handles Tapo device discovery and polling for integration with Shelly server.
 * Implements the KLAP protocol for authentication and encrypted communication.
 */

import dgram from 'dgram';
import crypto from 'crypto';
import http from 'http';
import net from 'net';
import { Buffer } from 'buffer';

// Tapo discovery port
const TAPO_DISCOVERY_PORT = 20002;

/**
 * CRC32 implementation for discovery packets
 */
function crc32(data) {
    let crc = 0xFFFFFFFF;
    const table = getCrc32Table();
    for (let i = 0; i < data.length; i++) {
        crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xFF];
    }
    return (crc ^ 0xFFFFFFFF) >>> 0;
}

let crc32Table = null;
function getCrc32Table() {
    if (crc32Table) return crc32Table;
    crc32Table = new Uint32Array(256);
    for (let i = 0; i < 256; i++) {
        let c = i;
        for (let j = 0; j < 8; j++) {
            c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
        }
        crc32Table[i] = c;
    }
    return crc32Table;
}

/**
 * Generate RSA key pair for discovery
 */
function generateRsaKeyPair() {
    const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
        modulusLength: 1024,
        publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
        privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
    });
    return { publicKey, privateKey };
}

/**
 * Generate discovery packet using AES discovery protocol
 */
function generateDiscoveryPacket() {
    const { publicKey } = generateRsaKeyPair();
    const keyPayload = JSON.stringify({ params: { rsa_key: publicKey } });
    const keyPayloadBytes = Buffer.from(keyPayload, 'utf8');

    const secret = crypto.randomBytes(4);
    const deviceSerial = secret.readUInt32BE(0);

    const header = Buffer.alloc(16);
    let offset = 0;
    header.writeUInt8(2, offset++);           // version
    header.writeUInt8(0, offset++);           // msg_type
    header.writeUInt16BE(1, offset);          // op_code
    offset += 2;
    header.writeUInt16BE(keyPayloadBytes.length, offset);  // msg_size
    offset += 2;
    header.writeUInt8(17, offset++);          // flags
    header.writeUInt8(0, offset++);           // padding
    header.writeUInt32BE(deviceSerial, offset); // device_serial
    offset += 4;
    header.writeInt32BE(0x5A6B7C8D, offset);  // initial_crc (placeholder)

    const query = Buffer.concat([header, keyPayloadBytes]);
    const crcValue = crc32(query);
    query.writeUInt32BE(crcValue, 12);

    return query;
}

/**
 * KLAP Cipher for encrypted communication
 */
class KlapCipher {
    constructor(localSeed, remoteSeed, authHash) {
        const localHash = Buffer.concat([localSeed, remoteSeed, authHash]);

        this.key = this._keyDerive(localHash);
        const { iv, seq } = this._ivDerive(localHash);
        this.iv = iv;
        this.seq = seq;
        this.sig = this._sigDerive(localHash);
    }

    static sha1(data) {
        return crypto.createHash('sha1').update(data).digest();
    }

    static sha256(data) {
        return crypto.createHash('sha256').update(data).digest();
    }

    _keyDerive(localHash) {
        const data = Buffer.concat([Buffer.from('lsk'), localHash]);
        return KlapCipher.sha256(data).subarray(0, 16);
    }

    _ivDerive(localHash) {
        const data = Buffer.concat([Buffer.from('iv'), localHash]);
        const hash = KlapCipher.sha256(data);
        const iv = hash.subarray(0, 12);
        const seq = hash.readInt32BE(hash.length - 4);
        return { iv, seq };
    }

    _sigDerive(localHash) {
        const data = Buffer.concat([Buffer.from('ldk'), localHash]);
        return KlapCipher.sha256(data).subarray(0, 28);
    }

    _ivSeq(seq) {
        const seqBuf = Buffer.alloc(4);
        seqBuf.writeInt32BE(seq);
        return Buffer.concat([this.iv, seqBuf]);
    }

    encrypt(data) {
        this.seq++;
        const ivSeq = this._ivSeq(this.seq);

        const cipher = crypto.createCipheriv('aes-128-cbc', this.key, ivSeq);
        const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);

        const sigData = Buffer.concat([
            this.sig,
            Buffer.from([(this.seq >> 24) & 0xff, (this.seq >> 16) & 0xff, (this.seq >> 8) & 0xff, this.seq & 0xff]),
            encrypted
        ]);
        const signature = KlapCipher.sha256(sigData);

        return {
            payload: Buffer.concat([signature, encrypted]),
            seq: this.seq
        };
    }

    decrypt(seq, data) {
        const ivSeq = this._ivSeq(seq);
        const ciphertext = data.subarray(32); // Skip 32-byte signature

        const decipher = crypto.createDecipheriv('aes-128-cbc', this.key, ivSeq);
        const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);

        return decrypted.toString('utf8');
    }
}

/**
 * Tapo Device Client
 */
class TapoDevice {
    constructor(ip, username, password) {
        this.ip = ip;
        this.username = username;
        this.password = password;
        this.baseUrl = `http://${ip}/app`;
        this.cookie = null;
        this.cipher = null;
        this.deviceInfo = null;
    }

    // HTTP request helper using native http module (fetch has issues with binary bodies)
    async _request(path, body, cookie = null) {
        return new Promise((resolve, reject) => {
            const options = {
                hostname: this.ip,
                port: 80,
                path: `/app${path}`,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/octet-stream',
                    'Content-Length': body.length
                },
                timeout: 5000
            };

            if (cookie) {
                options.headers['Cookie'] = cookie;
            }

            const req = http.request(options, (res) => {
                const chunks = [];
                res.on('data', chunk => chunks.push(chunk));
                res.on('end', () => {
                    resolve({
                        status: res.statusCode,
                        headers: res.headers,
                        body: Buffer.concat(chunks)
                    });
                });
            });

            req.on('error', reject);
            req.on('timeout', () => {
                req.destroy();
                reject(new Error(`Request timeout to ${this.ip}`));
            });

            req.write(body);
            req.end();
        });
    }

    async handshake() {
        // IMPORTANT: Tapo requires email to be lowercase for the hash
        const normalizedUsername = this.username.toLowerCase().trim();

        const authHash = KlapCipher.sha256(Buffer.concat([
            KlapCipher.sha1(Buffer.from(normalizedUsername)),
            KlapCipher.sha1(Buffer.from(this.password))
        ]));

        const localSeed = crypto.randomBytes(16);

        // Handshake1 - send local seed, receive remote seed + server hash
        const hs1Response = await this._request('/handshake1', localSeed);

        if (hs1Response.status !== 200) {
            if (hs1Response.status === 403) {
                throw new Error('Forbidden - Enable Third-Party Compatibility in Tapo app');
            }
            throw new Error(`Handshake1 failed: ${hs1Response.status}`);
        }

        // Get session cookie
        const cookies = hs1Response.headers['set-cookie'];
        if (cookies) {
            const cookieStr = Array.isArray(cookies) ? cookies[0] : cookies;
            const match = cookieStr.match(/TP_SESSIONID=([^;]+)/);
            if (match) {
                this.cookie = `TP_SESSIONID=${match[1]}`;
            }
        }

        const hs1Body = hs1Response.body;

        if (hs1Body.length < 48) {
            throw new Error(`Handshake1 response too short: ${hs1Body.length} bytes`);
        }

        const remoteSeed = hs1Body.subarray(0, 16);
        const serverHash = hs1Body.subarray(16, 48);

        // Verify server hash
        const localHash = KlapCipher.sha256(Buffer.concat([localSeed, remoteSeed, authHash]));
        if (!localHash.equals(serverHash)) {
            throw new Error('Authentication failed - check credentials');
        }

        // Handshake2 - send our hash to confirm
        const hs2Payload = KlapCipher.sha256(Buffer.concat([remoteSeed, localSeed, authHash]));
        const hs2Response = await this._request('/handshake2', hs2Payload, this.cookie);

        if (hs2Response.status !== 200) {
            throw new Error(`Handshake2 failed: ${hs2Response.status}`);
        }

        this.cipher = new KlapCipher(localSeed, remoteSeed, authHash);
    }

    async request(method, params = {}) {
        if (!this.cipher) {
            await this.handshake();
        }

        const requestData = JSON.stringify({ method, params });
        const { payload, seq } = this.cipher.encrypt(requestData);

        try {
            const response = await this._request(`/request?seq=${seq}`, payload, this.cookie);

            if (response.status === 401 || response.status === 403) {
                // Session expired, try re-handshake
                this.cipher = null;
                await this.handshake();
                return this.request(method, params);
            }

            if (response.status !== 200) {
                throw new Error(`Request failed: ${response.status}`);
            }

            const decrypted = this.cipher.decrypt(seq, response.body);
            return JSON.parse(decrypted);
        } catch (e) {
            throw e;
        }
    }

    async getDeviceInfo() {
        const response = await this.request('get_device_info');
        if (response.error_code === 0) {
            this.deviceInfo = response.result;
            return response.result;
        }
        throw new Error(`get_device_info failed: ${response.error_code}`);
    }

    async getChildDeviceList() {
        const response = await this.request('get_child_device_list');
        if (response.error_code === 0) {
            return response.result.child_device_list || [];
        }
        throw new Error(`get_child_device_list failed: ${response.error_code}`);
    }

    async getCurrentPower() {
        const response = await this.request('get_current_power');
        if (response.error_code === 0) {
            return response.result;
        }
        throw new Error(`get_current_power failed: ${response.error_code}`);
    }

    async getEnergyUsage() {
        const response = await this.request('get_energy_usage');
        if (response.error_code === 0) {
            return response.result;
        }
        throw new Error(`get_energy_usage failed: ${response.error_code}`);
    }

    async turnOn() {
        const response = await this.request('set_device_info', { device_on: true });
        return response.error_code === 0;
    }

    async turnOff() {
        const response = await this.request('set_device_info', { device_on: false });
        return response.error_code === 0;
    }
}

/**
 * Decode base64 nickname to readable string
 */
function decodeNickname(base64) {
    if (!base64) return null;
    try {
        return Buffer.from(base64, 'base64').toString('utf8');
    } catch (e) {
        return base64;
    }
}

/**
 * Normalize MAC address format (remove -, :, and uppercase)
 */
function normalizeMac(mac) {
    if (!mac) return null;
    return mac.replace(/[-:]/g, '').toUpperCase();
}

/**
 * Tapo Manager - handles discovery and polling
 */
class TapoManager {
    constructor(username, password, options = {}) {
        this.username = username;
        this.password = password;
        this.discoveryInterval = options.discoveryInterval || 5 * 60 * 1000;  // 5 minutes
        this.pollInterval = options.pollInterval || 10 * 1000;  // 10 seconds
        this.broadcastAddr = options.broadcastAddr || '255.255.255.255';
        this.discoveryTimeout = options.discoveryTimeout || 10;  // 10 seconds

        this.devices = new Map();  // IP -> device info
        this.deviceClients = new Map();  // IP -> TapoDevice instance
        this.childDevices = new Map();  // deviceId -> { hub, child info }

        this.discoveryTimer = null;
        this.pollTimer = null;

        // Callbacks - updated signatures to include more info
        this.onDeviceDiscovered = null;  // callback(deviceInfo) - includes mac, nickname, model
        this.onDeviceStateChange = null;  // callback(mac, component, field, type, value, deviceInfo)
        this.onChildDeviceDiscovered = null;  // callback(hubInfo, childInfo) - includes mac, nickname
    }

    async start() {
        console.log('[Tapo] Starting Tapo manager...');

        // Initial discovery
        await this.discoverDevices();

        // Start discovery interval
        this.discoveryTimer = setInterval(() => {
            this.discoverDevices().catch(e => console.error('[Tapo] Discovery error:', e.message));
        }, this.discoveryInterval);

        // Start polling interval
        this.pollTimer = setInterval(() => {
            this.pollDevices().catch(e => console.error('[Tapo] Polling error:', e.message));
        }, this.pollInterval);

        console.log(`[Tapo] Manager started. Discovery every ${this.discoveryInterval / 1000}s, polling every ${this.pollInterval / 1000}s`);
    }

    stop() {
        if (this.discoveryTimer) {
            clearInterval(this.discoveryTimer);
            this.discoveryTimer = null;
        }
        if (this.pollTimer) {
            clearInterval(this.pollTimer);
            this.pollTimer = null;
        }
        console.log('[Tapo] Manager stopped');
    }

    async discoverDevices() {
        console.log('[Tapo] Starting device discovery...');

        return new Promise((resolve) => {
            const foundDevices = new Map();
            const socket = dgram.createSocket('udp4');

            socket.on('error', (err) => {
                console.error('[Tapo] Discovery socket error:', err.message);
                socket.close();
                resolve(foundDevices);
            });

            socket.on('message', (msg, rinfo) => {
                if (!foundDevices.has(rinfo.address) && msg.length > 16) {
                    try {
                        const jsonStr = msg.subarray(16).toString('utf8');
                        const jsonEnd = jsonStr.lastIndexOf('}') + 1;
                        if (jsonEnd > 0) {
                            const data = JSON.parse(jsonStr.slice(0, jsonEnd));
                            if (data.result) {
                                foundDevices.set(rinfo.address, {
                                    ip: rinfo.address,
                                    deviceId: data.result.device_id,
                                    deviceType: data.result.device_type,
                                    deviceModel: data.result.device_model,
                                    owner: data.result.owner,
                                    encryptScheme: data.result.mgt_encrypt_schm
                                });
                                console.log(`[Tapo] Discovered: ${rinfo.address} - ${data.result.device_model} (${data.result.device_type})`);
                            }
                        }
                    } catch (e) {
                        // Ignore parse errors
                    }
                }
            });

            socket.bind(() => {
                socket.setBroadcast(true);

                const sendDiscovery = () => {
                    try {
                        const packet = generateDiscoveryPacket();
                        socket.send(packet, 0, packet.length, TAPO_DISCOVERY_PORT, this.broadcastAddr);
                    } catch (e) {
                        console.error('[Tapo] Error generating discovery packet:', e.message);
                    }
                };

                sendDiscovery();
                const interval = setInterval(sendDiscovery, 3000);

                setTimeout(async () => {
                    clearInterval(interval);
                    socket.close();

                    // Process discovered devices
                    for (const [ip, info] of foundDevices) {
                        if (!this.devices.has(ip)) {
                            this.devices.set(ip, info);
                            if (this.onDeviceDiscovered) {
                                this.onDeviceDiscovered(info);
                            }
                        }

                        // Create client for supported device types
                        const deviceType = info.deviceType || '';
                        if ((deviceType.includes('TAPOPLUG') || deviceType.includes('TAPOHUB')) &&
                            !this.deviceClients.has(ip)) {
                            const client = new TapoDevice(ip, this.username, this.password);
                            this.deviceClients.set(ip, client);
                        }
                    }

                    // Discover H100 hub children
                    await this.discoverHubChildren();

                    console.log(`[Tapo] Discovery complete. ${this.devices.size} devices, ${this.childDevices.size} child devices`);
                    resolve(foundDevices);
                }, this.discoveryTimeout * 1000);
            });
        });
    }

    async discoverHubChildren() {
        for (const [ip, info] of this.devices) {
            if (info.deviceType && info.deviceType.includes('TAPOHUB')) {
                const client = this.deviceClients.get(ip);
                if (client) {
                    try {
                        const children = await client.getChildDeviceList();
                        for (const child of children) {
                            const childKey = child.device_id;
                            if (!this.childDevices.has(childKey)) {
                                this.childDevices.set(childKey, {
                                    hubIp: ip,
                                    hubDeviceId: info.deviceId,
                                    ...child
                                });
                                console.log(`[Tapo] Hub child discovered: ${child.nickname || child.device_id} (${child.model})`);
                                if (this.onChildDeviceDiscovered) {
                                    this.onChildDeviceDiscovered(info, child);
                                }
                            } else {
                                // Update existing child info
                                this.childDevices.set(childKey, {
                                    ...this.childDevices.get(childKey),
                                    ...child
                                });
                            }
                        }
                    } catch (e) {
                        console.error(`[Tapo] Error getting children from hub ${ip}:`, e.message);
                    }
                }
            }
        }
    }

    async pollDevices() {
        const pollPromises = [];

        for (const [ip, info] of this.devices) {
            const deviceType = info.deviceType || '';

            // Poll P100/P115 plugs
            if (deviceType.includes('TAPOPLUG')) {
                pollPromises.push(this._pollPlug(ip, info));
            }

            // Poll H100 hub children
            if (deviceType.includes('TAPOHUB')) {
                pollPromises.push(this._pollHub(ip, info));
            }

            // Poll cameras for online status (they may use different protocol)
            if (deviceType.includes('IPCAMERA')) {
                pollPromises.push(this._pollCamera(ip, info));
            }
        }

        await Promise.allSettled(pollPromises);
    }

    async _pollCamera(ip, info) {
        // Cameras don't use KLAP typically, but we can check TCP connectivity
        // to see if they're online
        return new Promise((resolve) => {
            const socket = new net.Socket();

            socket.setTimeout(3000);

            socket.on('connect', async () => {
                socket.destroy();

                // Camera is reachable - try to get device info if possible
                let client = this.deviceClients.get(ip);
                if (!client) {
                    client = new TapoDevice(ip, this.username, this.password);
                    this.deviceClients.set(ip, client);
                }

                try {
                    const deviceInfo = await client.getDeviceInfo();
                    const mac = normalizeMac(deviceInfo.mac) || info.deviceId;
                    const nickname = decodeNickname(deviceInfo.nickname);

                    info.mac = mac;
                    info.nickname = nickname;
                    info.model = deviceInfo.model;
                    info.online = true;
                    info.lastSeen = new Date().toISOString();

                    if (this.onDeviceStateChange) {
                        this.onDeviceStateChange(mac, 'system', 'online', 'boolean', true, deviceInfo);
                    }
                } catch (e) {
                    // KLAP failed but TCP connected - camera is online but uses different protocol
                    // Use discovery deviceId as fallback
                    const mac = info.mac || info.deviceId;

                    if (info.online !== true) {
                        info.online = true;
                        if (this.onDeviceStateChange) {
                            this.onDeviceStateChange(mac, 'system', 'online', 'boolean', true, null);
                        }
                    }
                }
                resolve();
            });

            socket.on('timeout', () => {
                socket.destroy();
                const mac = info.mac || info.deviceId;
                if (info.online !== false) {
                    info.online = false;
                    if (this.onDeviceStateChange) {
                        this.onDeviceStateChange(mac, 'system', 'online', 'boolean', false, null);
                    }
                }
                resolve();
            });

            socket.on('error', () => {
                socket.destroy();
                const mac = info.mac || info.deviceId;
                if (info.online !== false) {
                    info.online = false;
                    if (this.onDeviceStateChange) {
                        this.onDeviceStateChange(mac, 'system', 'online', 'boolean', false, null);
                    }
                }
                resolve();
            });

            // Try port 554 (RTSP) as cameras typically expose this
            socket.connect(554, ip);
        });
    }

    async _pollPlug(ip, info) {
        let client = this.deviceClients.get(ip);
        if (!client) {
            client = new TapoDevice(ip, this.username, this.password);
            this.deviceClients.set(ip, client);
        }

        try {
            const deviceInfo = await client.getDeviceInfo();

            // Use real MAC from device (normalized) - this is the key change
            const mac = normalizeMac(deviceInfo.mac) || info.deviceId;
            const nickname = decodeNickname(deviceInfo.nickname);

            // Update stored info with real MAC and nickname
            info.mac = mac;
            info.nickname = nickname;
            info.model = deviceInfo.model;
            info.lastState = deviceInfo;
            info.lastSeen = new Date().toISOString();
            info.online = true;

            // Emit state changes using real MAC
            if (this.onDeviceStateChange) {
                // Online status
                this.onDeviceStateChange(mac, 'system', 'online', 'boolean', true, deviceInfo);

                // Switch state
                if (typeof deviceInfo.device_on !== 'undefined') {
                    this.onDeviceStateChange(mac, 'switch:0', 'output', 'boolean', deviceInfo.device_on, deviceInfo);
                }

                // On time (how long has it been on)
                // if (typeof deviceInfo.on_time !== 'undefined') {
                //    this.onDeviceStateChange(mac, 'switch:0', 'on_time', 'range', deviceInfo.on_time, deviceInfo);
                // }

                // Signal strength
                if (typeof deviceInfo.rssi !== 'undefined') {
                    this.onDeviceStateChange(mac, 'wifi', 'rssi', 'range', deviceInfo.rssi, deviceInfo);
                }

                // Overheated status (safety)
                if (typeof deviceInfo.overheated !== 'undefined') {
                    this.onDeviceStateChange(mac, 'system', 'overheated', 'boolean', deviceInfo.overheated, deviceInfo);
                }

                // P115 specific: power protection status
                if (typeof deviceInfo.power_protection_status !== 'undefined') {
                    this.onDeviceStateChange(mac, 'power', 'protection_status', 'enum', deviceInfo.power_protection_status, deviceInfo);
                }

                // Power Monitoring for P110/P115
                if (info.model && (info.model.includes('P110') || info.model.includes('P115'))) {
                    try {
                        const powerData = await client.getCurrentPower();
                        if (powerData && typeof powerData.current_power !== 'undefined') {
                            // current_power is in mW, convert to W for Shelly compatibility usually or keep as is?
                            // Shelly usually reports W. Tapo P110 returns current_power in mW.
                            const powerWatts = powerData.current_power / 1000.0;
                            this.onDeviceStateChange(mac, 'power:0', 'apower', 'range', powerWatts, deviceInfo);
                        }

                        const energyData = await client.getEnergyUsage();
                        if (energyData && typeof energyData.today_energy !== 'undefined') {
                            // today_energy is in Wh
                            this.onDeviceStateChange(mac, 'power:0', 'aenergy', 'range', energyData.today_energy, deviceInfo);
                        }
                    } catch (err) {
                        // Ignore power poll errors, don't fail the whole poll
                        console.error(`[Tapo] Error polling power for ${mac}:`, err.message);
                    }
                }
            }

        } catch (e) {
            console.error(`[Tapo] Error polling plug ${ip}:`, e.message);

            // Use stored MAC for offline notification, or fallback to deviceId
            const mac = info.mac || info.deviceId;

            // Mark as offline if previously online
            if (info.online !== false) {
                info.online = false;
                if (this.onDeviceStateChange) {
                    this.onDeviceStateChange(mac, 'system', 'online', 'boolean', false, null);
                }
            }
        }
    }

    async _pollHub(ip, info) {
        let client = this.deviceClients.get(ip);
        if (!client) {
            client = new TapoDevice(ip, this.username, this.password);
            this.deviceClients.set(ip, client);
        }

        try {
            // Get hub device info
            const hubDeviceInfo = await client.getDeviceInfo();

            // Use real MAC from device
            const hubMac = normalizeMac(hubDeviceInfo.mac) || info.deviceId;
            const hubNickname = decodeNickname(hubDeviceInfo.nickname);

            // Update stored hub info
            info.mac = hubMac;
            info.nickname = hubNickname;
            info.model = hubDeviceInfo.model;
            info.online = true;
            info.lastSeen = new Date().toISOString();

            if (this.onDeviceStateChange) {
                this.onDeviceStateChange(hubMac, 'system', 'online', 'boolean', true, hubDeviceInfo);

                // Hub alarm state
                if (typeof hubDeviceInfo.in_alarm !== 'undefined') {
                    this.onDeviceStateChange(hubMac, 'alarm', 'active', 'boolean', hubDeviceInfo.in_alarm, hubDeviceInfo);
                }

                // Signal strength
                if (typeof hubDeviceInfo.rssi !== 'undefined') {
                    this.onDeviceStateChange(hubMac, 'wifi', 'rssi', 'range', hubDeviceInfo.rssi, hubDeviceInfo);
                }
            }

            // Get and process child devices
            const children = await client.getChildDeviceList();
            for (const child of children) {
                // Use real MAC from child device
                const childMac = normalizeMac(child.mac) || child.device_id;
                const childNickname = decodeNickname(child.nickname);

                // Update stored child info with real MAC and nickname
                if (this.childDevices.has(child.device_id)) {
                    const stored = this.childDevices.get(child.device_id);
                    this.childDevices.set(child.device_id, {
                        ...stored,
                        ...child,
                        mac: childMac,
                        nickname: childNickname
                    });
                }

                // Emit child device state changes using real MAC
                if (this.onDeviceStateChange) {
                    // Online status
                    this.onDeviceStateChange(childMac, 'system', 'online', 'boolean', child.status === 'online', child);

                    // T100 motion sensor
                    if (child.model && child.model.startsWith('T100')) {
                        this.onDeviceStateChange(childMac, 'sensor:0', 'motion', 'boolean', child.detected || false, child);
                    }

                    // T110 door/window sensor
                    if (child.model && child.model.startsWith('T110')) {
                        this.onDeviceStateChange(childMac, 'sensor:0', 'open', 'boolean', child.open || false, child);
                    }

                    // T300 water leak sensor
                    if (child.model && child.model.startsWith('T300')) {
                        this.onDeviceStateChange(childMac, 'sensor:0', 'water_leak', 'boolean', child.water_leak_status === 'water_leak', child);
                        this.onDeviceStateChange(childMac, 'alarm', 'active', 'boolean', child.in_alarm || false, child);
                    }

                    // T31X temperature/humidity sensor
                    if (child.model && (child.model.startsWith('T310') || child.model.startsWith('T315'))) {
                        if (typeof child.current_temperature !== 'undefined') {
                            this.onDeviceStateChange(childMac, 'sensor:0', 'temperature', 'range', child.current_temperature, child);
                        }
                        if (typeof child.current_humidity !== 'undefined') {
                            this.onDeviceStateChange(childMac, 'sensor:0', 'humidity', 'range', child.current_humidity, child);
                        }
                    }

                    // Battery level for all battery-powered sensors
                    if (typeof child.at_low_battery !== 'undefined') {
                        this.onDeviceStateChange(childMac, 'battery', 'low', 'boolean', child.at_low_battery, child);
                    }

                    // Signal strength
                    if (typeof child.rssi !== 'undefined') {
                        this.onDeviceStateChange(childMac, 'rf', 'rssi', 'range', child.rssi, child);
                    }
                }
            }

        } catch (e) {
            console.error(`[Tapo] Error polling hub ${ip}:`, e.message);

            const hubMac = info.mac || info.deviceId;
            if (info.online !== false) {
                info.online = false;
                if (this.onDeviceStateChange) {
                    this.onDeviceStateChange(hubMac, 'system', 'online', 'boolean', false, null);
                }
            }
        }
    }

    getDevice(deviceId) {
        for (const [ip, info] of this.devices) {
            if (info.deviceId === deviceId) {
                return { ip, ...info };
            }
        }
        return null;
    }

    getChildDevice(deviceId) {
        return this.childDevices.get(deviceId) || null;
    }

    getAllDevices() {
        return Array.from(this.devices.values());
    }

    getAllChildDevices() {
        return Array.from(this.childDevices.values());
    }

    async setDeviceState(deviceId, on) {
        for (const [ip, info] of this.devices) {
            if (info.deviceId === deviceId) {
                const client = this.deviceClients.get(ip);
                if (client) {
                    return on ? await client.turnOn() : await client.turnOff();
                }
            }
        }
        return false;
    }
}

export { TapoManager, TapoDevice, generateDiscoveryPacket };
