/**
 * Commissioning Manager
 *
 * Handles passcode generation, discriminator generation, credential persistence,
 * QR code rendering, commissioning event listeners, and commissioning file updates.
 */
import { randomBytes } from 'node:crypto';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { ManualPairingCodeCodec, QrCode, QrPairingCodeCodec } from '@matter/types/schema';
import { Logger } from '../../logger.js';
import { DEFAULT_PRODUCT_ID, DEFAULT_VENDOR_ID, MAX_PASSCODE_ATTEMPTS } from './ServerConfig.js';
const log = Logger.withPrefix('Matter/Server');
export class CommissioningManager {
    passcode = 0;
    discriminator = 0;
    vendorId;
    productId;
    commissioningInfo = {};
    // Stored references so commissioning Observable listeners can be removed in
    // teardownCommissioningEventListeners(). matter.js Observables require the
    // exact same observer reference passed to .off() that was passed to .on().
    // Bound at registration time so each CommissioningDeps closure stays consistent.
    onFabricsChanged = null;
    onCommissioned = null;
    onDecommissioned = null;
    constructor() {
        this.vendorId = DEFAULT_VENDOR_ID;
        this.productId = DEFAULT_PRODUCT_ID;
    }
    /**
     * Generate a secure random passcode
     * According to Matter spec, passcode must be:
     * - 8 digits (00000001 to 99999998)
     * - Not in the invalid list
     * - Not sequential or repeating patterns
     */
    generateSecurePasscode() {
        let passcode;
        const maxAttempts = MAX_PASSCODE_ATTEMPTS;
        let attempts = 0;
        const invalidPasscodes = [
            0,
            11111111,
            22222222,
            33333333,
            44444444,
            55555555,
            66666666,
            77777777,
            88888888,
            99999999,
            12345678,
            87654321,
        ];
        do {
            // Use cryptographically secure random number generation with rejection sampling
            const maxRange = 99999998;
            const maxUint32 = 0xFFFFFFFF;
            const limit = Math.floor(maxUint32 / maxRange) * maxRange;
            let randomValue;
            do {
                randomValue = randomBytes(4).readUInt32BE(0);
            } while (randomValue >= limit);
            // Generate a value between 1 and 99999998 without modulo bias
            passcode = (randomValue % maxRange) + 1;
            attempts++;
            if (attempts > maxAttempts) {
                throw new Error('Failed to generate secure passcode after maximum attempts');
            }
        } while (invalidPasscodes.includes(passcode)
            || !this.isValidPasscode(passcode));
        return passcode;
    }
    /**
     * Validate a passcode according to Matter specifications
     */
    isValidPasscode(passcode) {
        // Must be between 1 and 99999998
        if (passcode < 1 || passcode > 99999998) {
            return false;
        }
        // Convert to 8-digit string
        const passcodeStr = passcode.toString().padStart(8, '0');
        // Check for sequential patterns (12345678, 23456789, etc.)
        let isSequential = true;
        for (let i = 1; i < passcodeStr.length; i++) {
            if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) + 1) {
                isSequential = false;
                break;
            }
        }
        if (isSequential) {
            return false;
        }
        // Check for reverse sequential (87654321, 76543210, etc.)
        let isReverseSequential = true;
        for (let i = 1; i < passcodeStr.length; i++) {
            if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) - 1) {
                isReverseSequential = false;
                break;
            }
        }
        if (isReverseSequential) {
            return false;
        }
        // Check for too many repeating digits (more than 3 of same digit)
        const digitCounts = new Map();
        for (const digit of passcodeStr) {
            digitCounts.set(digit, (digitCounts.get(digit) || 0) + 1);
            const count = digitCounts.get(digit);
            if (count !== undefined && count > 3) {
                return false;
            }
        }
        return true;
    }
    /**
     * Generate a random discriminator
     * According to Matter spec, discriminator must be:
     * - 12 bits (0-4095)
     * - Should be random for security
     */
    generateRandomDiscriminator() {
        // Generate cryptographically secure random 12-bit discriminator (0-4095)
        const discriminator = randomBytes(2).readUInt16BE(0) & 0x0FFF; // Mask to 12 bits
        // Validate discriminator range
        if (discriminator < 0 || discriminator > 4095) {
            throw new Error(`Invalid discriminator generated: ${discriminator}`);
        }
        return discriminator;
    }
    /**
     * Load or generate commissioning credentials (passcode and discriminator)
     * Reads/writes a simple credentials.json file in the bridge storage directory.
     */
    async loadOrGenerateCredentials(matterStoragePath) {
        const credentialsPath = join(matterStoragePath, 'credentials.json');
        try {
            const { readFile } = await import('node:fs/promises');
            const data = JSON.parse(await readFile(credentialsPath, 'utf-8'));
            if (data.passcode && data.discriminator) {
                log.info('Loading existing commissioning credentials from storage');
                this.passcode = data.passcode;
                this.discriminator = data.discriminator;
                return;
            }
        }
        catch {
            // File doesn't exist or is invalid - generate new credentials
        }
        // Generate new credentials and store them
        log.info('Generating new commissioning credentials');
        this.passcode = this.generateSecurePasscode();
        this.discriminator = this.generateRandomDiscriminator();
        // Store for future use
        await writeFile(credentialsPath, JSON.stringify({ passcode: this.passcode, discriminator: this.discriminator }, null, 2), 'utf-8');
        log.info('Commissioning credentials saved to storage');
    }
    /**
     * Generate and display commissioning information
     */
    async generateCommissioningInfo(deps) {
        const passcode = this.passcode.toString().padStart(8, '0');
        const discriminator = this.discriminator;
        const vendorId = this.vendorId;
        const productId = this.productId;
        // Use Matter.js library to generate pairing codes properly
        const manualCode = ManualPairingCodeCodec.encode({
            discriminator,
            passcode: this.passcode,
        });
        // Format as XXXX-XXX-XXXX for display
        const manualPairingCode = `${manualCode.slice(0, 4)}-${manualCode.slice(4, 7)}-${manualCode.slice(7, 11)}`;
        log.info(`Encoding QR code with: passcode=${this.passcode}, discriminator=${discriminator}, vendorId=${vendorId}, productId=${productId}`);
        const qrCodePayload = QrPairingCodeCodec.encode([{
                version: 0,
                vendorId,
                productId,
                flowType: 0, // Standard commissioning flow
                discoveryCapabilities: 4, // OnNetwork=4
                discriminator,
                passcode: this.passcode,
            }]);
        log.info(`Generated QR code: ${qrCodePayload}`);
        log.info(`Generated manual code: ${manualPairingCode}`);
        // Store commissioning info
        this.commissioningInfo = {
            qrCode: qrCodePayload,
            manualPairingCode,
        };
        // Save commissioning info to disk for UI access
        try {
            if (!deps.matterStoragePath) {
                throw new Error('Matter storage path not initialized');
            }
            const commissioningFilePath = join(deps.matterStoragePath, 'commissioning.json');
            const commissioningData = {
                qrCode: qrCodePayload,
                manualPairingCode,
                serialNumber: deps.serialNumber,
                passcode: this.passcode,
                discriminator: this.discriminator,
                commissioned: deps.fabricManager.isCommissioned(),
            };
            await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8');
            log.debug(`Saved commissioning info to ${commissioningFilePath}`);
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            log.warn(`Failed to save commissioning info to disk: ${errorMessage}`);
        }
        // Display commissioning information
        log.info(`${'='.repeat(60)}`);
        log.info('📱 MATTER COMMISSIONING INFORMATION');
        log.info('='.repeat(60));
        log.info(`Manual Pairing Code: ${manualPairingCode}`);
        log.info(`Passcode: ${passcode}`);
        log.info(`Discriminator: ${discriminator}`);
        log.info('QR Code for commissioning:');
        // Generate and display QR code in terminal using matter.js native QR renderer
        const qrCodeString = QrCode.get(qrCodePayload);
        log.info(`\n${qrCodeString}`);
        log.info(`${'='.repeat(60)}`);
    }
    /**
     * Set up Matter.js commissioning event listeners
     */
    setupCommissioningEventListeners(deps) {
        if (!deps.serverNode) {
            log.warn('Cannot set up commissioning event listeners - serverNode not initialized');
            return;
        }
        if (this.onFabricsChanged || this.onCommissioned || this.onDecommissioned) {
            log.debug('Commissioning event listeners already registered, skipping');
            return;
        }
        log.debug('Setting up commissioning event listeners');
        try {
            // Listen for fabric changes (add/remove/update)
            this.onFabricsChanged = (fabricIndex, action) => {
                log.info(`Fabric ${action}: index ${fabricIndex}`);
                // Compute commissioning state once and reuse for both the file update
                // and the IPC emit, then push the snapshot into updateCommissioningFile
                // so it doesn't redo the same fabric reads.
                const snapshot = deps.fabricManager.getCommissioningSnapshot();
                this.updateCommissioningFile(deps, snapshot).catch((error) => {
                    log.warn('Failed to update commissioning file after fabric change:', error);
                });
                deps.emitter.emit('commissioning-status-changed', snapshot.commissioned, snapshot.fabricCount);
            };
            deps.serverNode.events.commissioning.fabricsChanged.on(this.onFabricsChanged);
            // Listen for commissioning (first fabric added)
            this.onCommissioned = () => {
                log.info('Bridge commissioned');
                const snapshot = deps.fabricManager.getCommissioningSnapshot();
                this.updateCommissioningFile(deps, snapshot).catch((error) => {
                    log.warn('Failed to update commissioning file after commissioning:', error);
                });
                deps.emitter.emit('commissioning-status-changed', true, snapshot.fabricCount);
            };
            deps.serverNode.events.commissioning.commissioned.on(this.onCommissioned);
            // Listen for decommissioning (last fabric removed)
            this.onDecommissioned = () => {
                log.info('Bridge decommissioned');
                this.updateCommissioningFile(deps).catch((error) => {
                    log.warn('Failed to update commissioning file after decommissioning:', error);
                });
                deps.emitter.emit('commissioning-status-changed', false, 0);
            };
            deps.serverNode.events.commissioning.decommissioned.on(this.onDecommissioned);
            log.debug('Commissioning event listeners registered successfully');
        }
        catch (error) {
            log.error('Failed to set up commissioning event listeners:', error);
            // Roll back any partial registration so a retry can succeed
            this.teardownCommissioningEventListeners(deps.serverNode);
        }
    }
    /**
     * Remove Matter.js commissioning event listeners.
     *
     * Called from ServerLifecycle.cleanup() to release the closures that capture
     * deps (serverNode, fabricManager, emitter, matterStoragePath) and `this`.
     * Without this, the matter.js Observable retains the observer across stop()
     * cycles, holding the entire CommissioningDeps graph from GC.
     */
    teardownCommissioningEventListeners(serverNode) {
        if (!serverNode) {
            this.onFabricsChanged = null;
            this.onCommissioned = null;
            this.onDecommissioned = null;
            return;
        }
        try {
            if (this.onFabricsChanged) {
                serverNode.events.commissioning.fabricsChanged.off(this.onFabricsChanged);
            }
            if (this.onCommissioned) {
                serverNode.events.commissioning.commissioned.off(this.onCommissioned);
            }
            if (this.onDecommissioned) {
                serverNode.events.commissioning.decommissioned.off(this.onDecommissioned);
            }
            log.debug('Commissioning event listeners removed');
        }
        catch (error) {
            log.debug('Error removing commissioning event listeners:', error);
        }
        finally {
            this.onFabricsChanged = null;
            this.onCommissioned = null;
            this.onDecommissioned = null;
        }
    }
    /**
     * Update commissioning info file when commissioning state changes.
     *
     * Pass a precomputed `snapshot` to avoid redundant fabric reads — each of
     * isCommissioned(), getCommissionedFabricCount(), and getFabricInfo() may
     * scan the matter storage directory synchronously, and they're called in
     * tight succession from the commissioning event handlers.
     */
    async updateCommissioningFile(deps, snapshot) {
        try {
            if (!deps.matterStoragePath) {
                return;
            }
            const { commissioned, fabricCount, fabrics } = snapshot ?? deps.fabricManager.getCommissioningSnapshot();
            const commissioningFilePath = join(deps.matterStoragePath, 'commissioning.json');
            const commissioningData = {
                qrCode: this.commissioningInfo.qrCode,
                manualPairingCode: this.commissioningInfo.manualPairingCode,
                serialNumber: deps.serialNumber,
                passcode: this.passcode,
                discriminator: this.discriminator,
                commissioned,
                fabricCount,
                fabrics,
            };
            await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8');
            log.debug('Updated commissioning info file');
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            log.debug(`Failed to update commissioning info file: ${errorMessage}`);
        }
    }
}
//# sourceMappingURL=CommissioningManager.js.map