/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * Important note: This file is part of the legacy matter-node (internal) API and should not be used anymore directly!
 * Please use the new API classes!
 * @deprecated
 */

import { ClusterClient } from "#cluster/client/ClusterClient.js";
import {
    InteractionClientProvider,
    NodeDiscoveryType,
    PeerConnectionOptions,
} from "#cluster/client/InteractionClient.js";
import type { NodeCommissioningOptions } from "#CommissioningController.js";
import { ControllerStore, ControllerStoreInterface } from "#ControllerStore.js";
import { DeviceInformationData } from "#device/DeviceInformation.js";
import {
    BasicMultiplex,
    Bytes,
    causedBy,
    ChannelType,
    ClassExtends,
    ConnectionlessTransportSet,
    Construction,
    Crypto,
    Diagnostic,
    Environment,
    ImplementationError,
    InternalError,
    isDeepEqual,
    isObject,
    Logger,
    MatterError,
    Minutes,
    MockStorageService,
    ObserverGroup,
    ServerAddress,
    ServerAddressUdp,
    StorageManager,
    StorageService,
    SupportedStorageTypes,
    Time,
    UnexpectedDataError,
} from "@matter/general";
import {
    ClientNode,
    ClientNodePhysicalProperties,
    CommissioningClient,
    ControllerBehavior,
    Endpoint,
    NetworkClient,
    NodePhysicalProperties,
    Peers,
    RemoteDescriptor,
    ServerNode,
    ServerNodeStore,
} from "@matter/node";
import { BasicInformationClient } from "@matter/node/behaviors/basic-information";
import { OtaProviderEndpoint } from "@matter/node/endpoints/ota-provider";
import {
    CertificateAuthority,
    CommissioningError,
    ControllerCommissioner,
    ControllerCommissioningFlow,
    DiscoveryData,
    Fabric,
    FabricAuthority,
    FabricManager,
    PeerAddress,
    PeerDescriptor,
    PeerSet,
    PhysicalDeviceProperties,
    RetransmissionLimitReachedError,
    ScannerSet,
    SecureSession,
    SessionManager,
} from "@matter/protocol";
import {
    CaseAuthenticatedTag,
    DiscoveryCapabilitiesBitmap,
    EndpointNumber,
    EventNumber,
    FabricId,
    FabricIndex,
    NodeId,
    TypeFromPartialBitSchema,
    VendorId,
} from "@matter/types";
import { BasicInformation } from "@matter/types/clusters/basic-information";
import { GeneralCommissioning } from "@matter/types/clusters/general-commissioning";

export type CommissionedNodeDetails = {
    operationalServerAddress?: ServerAddressUdp;
    discoveryData?: DiscoveryData;
    deviceData?: DeviceInformationData;
};

const DEFAULT_FABRIC_INDEX = FabricIndex(1);

const logger = Logger.get("MatterController");

// Operational peer extended with basic information as required for conversion to CommissionedNodeDetails
type CommissionedPeer = PeerDescriptor & { deviceData?: DeviceInformationData };

// Backward-compatible persistence record for nodes
type StoredOperationalPeer = [NodeId, CommissionedNodeDetails];

export type PairedNodeDetails = {
    nodeId: NodeId;
    operationalAddress?: string;
    advertisedName?: string;
    discoveryData?: RemoteDescriptor;
    deviceData: {
        basicInformation?: Record<string, SupportedStorageTypes>;
        deviceMeta?: PhysicalDeviceProperties;
    };
};

export class MatterController {
    public static async create(options: {
        id: string;
        rootCertificateAuthority?: CertificateAuthority;
        rootFabric?: Fabric;
        adminVendorId?: VendorId;
        adminFabricIndex?: FabricIndex;
        caseAuthenticatedTags?: CaseAuthenticatedTag[];
        rootNodeId?: NodeId;
        adminFabricId?: FabricId;
        adminFabricLabel: string;
        ble?: boolean;
        ipv4?: boolean;
        listeningAddressIpv4?: string;
        listeningAddressIpv6?: string;
        localPort?: number;
        environment: Environment;
        enableOtaProvider?: boolean;
        basicInformation?: Partial<Omit<BasicInformation.Attributes, "vendorId">>;
    }): Promise<MatterController> {
        const {
            rootFabric,
            rootCertificateAuthority,
            adminFabricIndex = FabricIndex(DEFAULT_FABRIC_INDEX),
            environment,
        } = options;

        if (adminFabricIndex !== FabricIndex(1)) {
            logger.warn(
                "Fabric Indices will be assigned automatically from now on. Specifying a custom Fabric Index is deprecated.",
            );
        }

        let controller: MatterController | undefined = undefined;
        let fabric: Fabric | undefined = rootFabric;

        const baseStorage: StorageManager = await options.environment.get(StorageService).open(options.id);

        try {
            // Storage data migration from legacy Controller to ServerNode based controller
            const oldStorage = baseStorage.createContext("credentials");
            const newStorage = baseStorage.createContext("certificates");
            const newFabricStorage = baseStorage.createContext("fabrics");

            const keys = await oldStorage.keys();

            const newFabrics = await newFabricStorage.get<Fabric.Config[]>("fabrics", []);
            if (keys.length !== 0) {
                for (const key of await oldStorage.keys()) {
                    if (key === "fabric") {
                        if (rootFabric !== undefined) {
                            logger.debug("Skipping fabric migration because a rootFabric was provided.");
                            continue;
                        }
                        const oldFabric = await oldStorage.get<Fabric.Config>("fabric");
                        if (
                            newFabrics.length &&
                            newFabrics.some(
                                fab =>
                                    fab.fabricIndex === oldFabric.fabricIndex &&
                                    Bytes.areEqual(fab.rootCert, oldFabric.rootCert),
                            )
                        ) {
                            logger.debug("Skipping fabric migration because a new storage already has matching fabric");
                            continue;
                        }
                        fabric = await Fabric.create(Environment.default.get(Crypto), oldFabric);
                    } else {
                        // Migrates Certificate Authority data to a new location
                        if (!(await newStorage.has(key))) {
                            await newStorage.set(key, await oldStorage.get(key));
                        }
                    }
                }
            }

            if (rootCertificateAuthority !== undefined) {
                environment.set(CertificateAuthority, rootCertificateAuthority);
            }
        } finally {
            try {
                await baseStorage.close();
            } catch (closeError) {
                logger.warn("Error closing base storage:", closeError);
            }
        }

        controller = new MatterController({
            ...options,
            fabric,
        });

        await controller.construction;
        return controller;
    }

    public static async createAsPaseCommissioner(options: {
        id: string;
        certificateAuthorityConfig?: CertificateAuthority.Configuration;
        rootCertificateAuthority?: CertificateAuthority;
        fabricConfig: Fabric.Config;
        adminFabricLabel: string;
        adminFabricId?: FabricId;
        ble?: boolean;
        ipv4?: boolean;
        listeningAddressIpv4?: string;
        listeningAddressIpv6?: string;
        localPort?: number;
        environment: Environment;
    }): Promise<MatterController> {
        const { certificateAuthorityConfig, rootCertificateAuthority, fabricConfig, environment } = options;
        const crypto = environment.get(Crypto);

        if (rootCertificateAuthority === undefined && certificateAuthorityConfig === undefined) {
            throw new ImplementationError("Either rootCertificateAuthority or certificateAuthorityConfig must be set.");
        }
        const ca = rootCertificateAuthority ?? (await CertificateAuthority.create(crypto, certificateAuthorityConfig!));
        environment.set(CertificateAuthority, ca);

        // Stored data are temporary anyway and no node will be connected, so just use an in-memory storage
        new MockStorageService(environment);

        const fabric = await Fabric.create(crypto, fabricConfig);
        if (!Bytes.areEqual(fabric.rootCert, ca.rootCert)) {
            throw new MatterError("Fabric CA certificate is not in sync with CA.");
        }

        // Check if we have a fabric stored in the storage, if yes initialize this one, else build a new one
        const controller = new MatterController({
            ...options,
            fabric,
        });
        await controller.construction;

        // Verify an appropriate network interface is available
        const netInterfaces = environment.get(ConnectionlessTransportSet);
        if (!netInterfaces.hasInterfaceFor(ChannelType.BLE)) {
            if (
                !environment.get(ScannerSet).hasScannerFor(ChannelType.UDP) ||
                !netInterfaces.hasInterfaceFor(ChannelType.UDP, "::")
            ) {
                throw new ImplementationError(
                    "Ble must be initialized to create a Sub Commissioner without an IP network!",
                );
            }
            logger.info("BLE is not enabled. Using only IP network for commissioning.");
        }

        return controller;
    }

    #construction: Construction<MatterController>;
    #node?: ServerNode;
    #peers?: PeerSet;
    #fabric?: Fabric;
    #clients?: InteractionClientProvider;
    #migratedPeerObservers = new ObserverGroup();
    #legacyPeerStore?: CommissionedNodeStore;

    get construction() {
        return this.#construction;
    }

    constructor(options: {
        id: string;
        fabric?: Fabric;
        ble?: boolean;
        adminFabricId?: FabricId;
        adminFabricLabel: string;
        adminVendorId?: VendorId;
        rootNodeId?: NodeId;
        caseAuthenticatedTags?: CaseAuthenticatedTag[];
        ipv4?: boolean;
        listeningAddressIpv4?: string;
        listeningAddressIpv6?: string;
        localPort?: number;
        environment: Environment;
        enableOtaProvider?: boolean;
        basicInformation?: Partial<Omit<BasicInformation.Attributes, "vendorId">>;
    }) {
        const crypto = options.environment.get(Crypto);
        const {
            ble = false,
            adminFabricLabel,
            adminFabricId = FabricId(crypto.randomBigInt(8)),
            adminVendorId,
            rootNodeId,
            caseAuthenticatedTags,
            ipv4 = true,
            listeningAddressIpv4,
            listeningAddressIpv6,
            localPort,
            environment,
            id,
            fabric,
            enableOtaProvider = false,
            basicInformation = {},
        } = options;

        this.#construction = Construction(this, async () => {
            // Now after all Legacy stuff is prepared, initialize the ServerNode
            this.#node = await ServerNode.create(ServerNode.RootEndpoint.with(ControllerBehavior), {
                environment,
                id,
                network: {
                    ble: false,
                    ipv4,
                    listeningAddressIpv4,
                    listeningAddressIpv6,
                    port: localPort,
                },
                basicInformation: {
                    ...basicInformation,
                    vendorId: adminVendorId,
                },
                controller: {
                    adminFabricLabel,
                    adminFabricId,
                    adminNodeId: rootNodeId,
                    caseAuthenticatedTags,
                    ble,
                },
                commissioning: {
                    enabled: false, // The node is never commissionable directly
                },
                subscriptions: {
                    persistenceEnabled: false, // Disable because that's a device feature
                },
            });

            if (enableOtaProvider) {
                await this.#enableOtaProvider();
            }

            const fabricManager = await this.#node.env.load(FabricManager);
            const fabricAuthority = await this.#node.env.load(FabricAuthority);
            if (fabric !== undefined) {
                if (!fabricManager.has(fabric.fabricIndex)) {
                    if (fabricAuthority.hasControlOf(fabric)) {
                        logger.info(
                            `Adding provided fabric with index ${fabric.fabricIndex} under the control of the Fabric Authority`,
                        );
                        fabricManager.addFabric(fabric);
                        await fabricManager.persistFabrics();
                    } else {
                        throw new ImplementationError(
                            `Provided fabric with index ${fabric.fabricIndex} is not under the control of the Fabric Authority`,
                        );
                    }
                }
            }
            this.#fabric = await fabricAuthority.defaultFabric(
                {
                    adminFabricLabel,
                    adminVendorId,
                    adminNodeId: rootNodeId,
                    adminFabricId,
                    caseAuthenticatedTags,
                },
                fabric === undefined, // When no fabric is provided, we rotate the NOC operational keypair on start.
                // This avoids long-lived operational keys for controllers that bootstrap their own fabric state,
                // which is a security best practice: each restart derives a fresh keypair for the same logical fabric.
                // Already-commissioned devices remain accessible because the NOC is reissued under the same CA and
                // fabric identifiers, so peers still recognize and trust the controller despite the new keypair.
            );
            if (fabric !== undefined) {
                if (
                    !fabricAuthority.fabrics.some(
                        controlledFabric =>
                            controlledFabric.fabricIndex === fabric.fabricIndex && fabricAuthority.hasControlOf(fabric),
                    )
                ) {
                    throw new ImplementationError(
                        `Fabric with index ${fabric.fabricIndex} is already present but not under the control of the Fabric Authority`,
                    );
                } else {
                    logger.info(
                        `Fabric with index ${fabric.fabricIndex} and matching keys is already present, initialized correctly.`,
                    );
                }
            }

            if (this.#fabric !== undefined) {
                await this.#migrateNodeData(this.#node, this.#fabric);
            }

            // TODO
            //await (await server.env.get(ServerNodeStore).storage
            //    .createContext("credentials")
            //    .clearAll(); // Clear old credentials storage

            this.#peers = this.#node.env.get(PeerSet);
        });
    }

    get peers() {
        this.construction.assert();

        return this.#peers!;
    }

    #enableOtaProvider() {
        if (!this.#node) {
            throw new ImplementationError("Node is not initialized yet");
        }
        if (this.#node.endpoints.has("ota-provider")) {
            return; // Already enabled
        }
        return this.#node.add(new Endpoint(OtaProviderEndpoint, { id: "ota-provider" }));
    }

    get ble() {
        return this.node.stateOf(ControllerBehavior).ble ?? false;
    }

    get fabric() {
        this.#construction.assert();
        if (this.#fabric === undefined) {
            throw new InternalError("Fabric is not initialized.");
        }
        return this.#fabric;
    }

    get nodeId() {
        return this.fabric.rootNodeId;
    }

    get caConfig() {
        return this.node.env.get(CertificateAuthority).config;
    }

    get fabricConfig() {
        return this.fabric.config;
    }

    get sessions() {
        return this.node.env.get(SessionManager);
    }

    getFabrics() {
        return [this.fabric];
    }

    collectScanners(
        discoveryCapabilities: TypeFromPartialBitSchema<typeof DiscoveryCapabilitiesBitmap> = { onIpNetwork: true },
    ) {
        this.#construction.assert();
        // Note we always scan via MDNS if available
        return this.node.env
            .get(ScannerSet)
            .filter(
                scanner =>
                    scanner.type === ChannelType.UDP || (discoveryCapabilities.ble && scanner.type === ChannelType.BLE),
            );
    }

    /**
     * Commission a device by its identifier and the Passcode. If a known address is provided this is tried first
     * before discovering devices in the network. If multiple addresses or devices are found, they are tried all after
     * each other. It returns the NodeId of the commissioned device.
     * If it throws a PairRetransmissionLimitReachedError that means that no found device responded to the pairing
     * request or the passode did not match to any discovered device/address.
     *
     * Use the connectNodeAfterCommissioning callback to implement an own logic to do the operative device discovery and
     * to complete the commissioning process.
     * Return true when the commissioning process is completed successfully, false on error.
     */
    async commission(
        options: NodeCommissioningOptions,
        customizations?: {
            completeCommissioningCallback?: (peerNodeId: NodeId, discoveryData?: DiscoveryData) => Promise<boolean>;
            commissioningFlowImpl?: ClassExtends<ControllerCommissioningFlow>;
        },
    ): Promise<NodeId> {
        const { completeCommissioningCallback, commissioningFlowImpl } = customizations ?? {};

        // Wrap the optional PASE-only callback into the finalizeCommissioning hook understood by CommissioningClient
        const finalizeCommissioning = completeCommissioningCallback
            ? async (peerAddress: PeerAddress, discoveryData?: DiscoveryData) => {
                  const result = await completeCommissioningCallback(peerAddress.nodeId, discoveryData);
                  if (!result) {
                      throw new RetransmissionLimitReachedError("Device could not be discovered");
                  }
              }
            : undefined;

        const caseAuthenticatedTags = options.caseAuthenticatedTags;
        const { knownAddress, timeout, discoveryCapabilities } = options.discovery;
        const commissionableDevice =
            "commissionableDevice" in options.discovery ? options.discovery.commissionableDevice : undefined;

        let clientNode: ClientNode;

        const addresses = commissionableDevice?.addresses ?? (knownAddress !== undefined ? [knownAddress] : undefined);
        if (addresses !== undefined) {
            // Pre-discovered device or known address — find/create the ClientNode and commission via CommissioningClient
            const descriptor: RemoteDescriptor = commissionableDevice ?? { addresses };
            clientNode = await this.node.peers.forDescriptor(descriptor);
            await clientNode.commission({
                fabric: this.fabric,
                passcode: options.passcode,
                commissioningFlowImpl,
                caseAuthenticatedTags,
                autoSubscribe: false,
                wifiNetwork: options.commissioning.wifiNetwork,
                threadNetwork: options.commissioning.threadNetwork,
                regulatoryLocation: options.commissioning.regulatoryLocation,
                regulatoryCountryCode: options.commissioning.regulatoryCountryCode,
                nodeId: options.commissioning.nodeId,
                finalizeCommissioning,
            });
        } else {
            // Pure discovery by identifier — use CommissioningDiscovery for full parallel PASE support
            const identifierData = "identifierData" in options.discovery ? options.discovery.identifierData : {};
            clientNode = await this.node.peers.commission({
                ...identifierData,
                ...options.commissioning,
                fabric: this.fabric,
                passcode: options.passcode,
                commissioningFlowImpl,
                caseAuthenticatedTags,
                autoSubscribe: false,
                timeout,
                discoveryCapabilities,
                finalizeCommissioning,
            });
        }

        const peerAddress = clientNode.peerAddress;
        if (peerAddress === undefined) {
            throw new ImplementationError("Commissioned node has no peer address");
        }

        await this.fabric.persist();
        return peerAddress.nodeId;
    }

    async disconnect(nodeId: NodeId) {
        const node = await this.node.peers.forAddress(this.fabric.addressOf(nodeId));
        return await node.disable();
    }

    async connectPaseChannel(options: NodeCommissioningOptions) {
        const { discovery, passcode } = options;
        const timeout = discovery.timeout;

        if ("commissionableDevice" in discovery) {
            // Pre-discovered device: addresses already known, skip discovery.
            let { addresses } = discovery.commissionableDevice;
            if (discovery.discoveryCapabilities?.ble !== true) {
                addresses = addresses.filter(a => a.type !== "ble");
            }
            const commissioner = this.node.env.get(ControllerCommissioner);
            const { paseSession } = await commissioner.establishPase({
                addresses,
                discoveryData: discovery.commissionableDevice,
                passcode,
                timeout,
            });
            logger.warn("PASE channel established", paseSession.via, paseSession.isSecure);
            return paseSession;
        }

        const identifierData = "identifierData" in discovery ? discovery.identifierData : {};

        // If we have a known address, try it first before falling back to full discovery.
        if (discovery.knownAddress !== undefined) {
            const commissioner = this.node.env.get(ControllerCommissioner);
            try {
                const { paseSession } = await commissioner.establishPase({
                    addresses: [discovery.knownAddress],
                    passcode,
                    timeout,
                });
                logger.warn("PASE channel established via known address", paseSession.via, paseSession.isSecure);
                return paseSession;
            } catch (error) {
                // Intentional: fall back to full discovery when the known address is stale, unreachable, or
                // points to a different device.  Re-throw on unexpected errors (e.g. config problems).
                if (!causedBy(error, UnexpectedDataError, RetransmissionLimitReachedError)) {
                    throw error;
                }
            }
        }

        // Pure discovery: use PaseDiscovery which manages scanner lifecycle automatically.
        const paseSession = await this.node.peers.pase({ ...identifierData, timeout, passcode });
        logger.warn("PASE channel established via discovery", paseSession.via, paseSession.isSecure);
        return paseSession;
    }

    async removeNode(nodeId: NodeId) {
        const peerAddress = this.fabric.addressOf(nodeId);
        const node = this.node.peers.get(peerAddress);
        const peer = this.node.env.get(PeerSet).get(peerAddress);
        await node?.delete();
        await peer?.delete();
    }

    /**
     * Method to complete the commissioning process to a node which was initialized with a PASE secure channel.
     */
    async completeCommissioning(peerNodeId: NodeId, discoveryData?: DiscoveryData) {
        this.#construction.assert();
        // Look for the device broadcast over MDNS and do CASE pairing
        const interactionClient = await this.connect(peerNodeId, {
            discoveryOptions: {
                discoveryType: NodeDiscoveryType.TimedDiscovery,
                timeout: Minutes(2),
                discoveryData,
            },
            allowUnknownPeer: true,
        }); // Wait maximum 120s to find the operational device for a commissioning process
        const generalCommissioningClusterClient = ClusterClient(
            GeneralCommissioning,
            EndpointNumber(0),
            interactionClient,
        );
        const { errorCode, debugText } = await generalCommissioningClusterClient.commissioningComplete({
            useExtendedFailSafeMessageResponseTimeout: true,
        });
        if (errorCode !== GeneralCommissioning.CommissioningError.Ok) {
            // We might have added data for an operational address that we need to cleanup
            await this.#peers?.get(this.fabric.addressOf(peerNodeId))?.delete();
            throw new CommissioningError(`Commission error on commissioningComplete: ${errorCode}, ${debugText}`);
        }

        await (
            await this.node.peers.forAddress(this.fabric.addressOf(peerNodeId))
        ).setStateOf(CommissioningClient, { commissionedAt: Time.nowMs });

        await this.fabric.persist();
    }

    isCommissioned() {
        return !!this.getCommissionedNodes().length;
    }

    getCommissionedNodes() {
        return this.node.peers
            .map(peer => peer.maybeStateOf(CommissioningClient)?.peerAddress?.nodeId)
            .filter(nodeId => nodeId !== undefined);
    }

    #commissionedNodeDetailsForNode(peer: ClientNode): PairedNodeDetails {
        const { peerAddress, addresses, deviceName } = peer.state.commissioning;
        return {
            nodeId: peerAddress!.nodeId,
            operationalAddress: Array.isArray(addresses) ? ServerAddress.urlFor(addresses[0]) : undefined,
            advertisedName: deviceName,
            discoveryData: RemoteDescriptor.fromLongForm(peer.state.commissioning),
            deviceData: {
                basicInformation: peer.maybeStateOf(BasicInformationClient),
                deviceMeta: NodePhysicalProperties(peer),
            },
        };
    }

    getCommissionedNodesDetails(): PairedNodeDetails[] {
        return this.node.peers
            .filter(peer => peer.maybeStateOf(CommissioningClient)?.peerAddress !== undefined)
            .map(peer => this.#commissionedNodeDetailsForNode(peer));
    }

    getCommissionedNodeDetails(nodeId: NodeId) {
        const address = this.fabric.addressOf(nodeId);
        const peer = this.node.peers.get(address);
        if (peer === undefined) {
            throw new Error(`Node ${nodeId} is not commissioned.`);
        }
        return this.#commissionedNodeDetailsForNode(peer);
    }

    /**
     * Connect to the device by opening a channel and creating a new CASE session if necessary.
     * Returns an InteractionClient on success.
     */
    async connect(peerNodeId: NodeId, options: MatterController.ConnectOptions) {
        const address = this.fabric.addressOf(peerNodeId);
        let node = this.node.peers.get(address);
        if (node === undefined) {
            if (!options.allowUnknownPeer) {
                throw new MatterError(`Node ${peerNodeId} is not commissioned on this controller.`);
            }
            node = await this.node.peers.forAddress(address);
        }
        if (
            options.caseAuthenticatedTags !== undefined &&
            !isDeepEqual(options.caseAuthenticatedTags, node.state.commissioning.caseAuthenticatedTags)
        ) {
            await node.setStateOf(CommissioningClient, { caseAuthenticatedTags: options.caseAuthenticatedTags });
        }
        await node.enable();
        return this.#clients!.connect(this.fabric.addressOf(peerNodeId), options);
    }

    createInteractionClient(peerNodeIdOrSession: NodeId | SecureSession, options: PeerConnectionOptions = {}) {
        if (peerNodeIdOrSession instanceof SecureSession) {
            return this.#clients!.interactionClientFor(peerNodeIdOrSession);
        }
        const address = this.fabric.addressOf(peerNodeIdOrSession);
        return this.#clients!.getNodeInteractionClient(address, options);
    }

    async start() {
        await this.node.start();
        this.#clients = new InteractionClientProvider(this.node);
        await this.node.act(agent => agent.load(ControllerBehavior));
    }

    async close() {
        this.#migratedPeerObservers.close();
        await this.#legacyPeerStore?.close();
        await this.#node?.close();
        this.#clients = undefined;
        await this.#construction.close();
    }

    getActiveSessionInformation() {
        return this.node.env.get(SessionManager).getActiveSessionInformation();
    }

    get node() {
        this.#construction.assert();
        return this.#node!;
    }

    async updateFabricLabel(label: string) {
        await this.fabric.setLabel(label);
    }

    async #migrateNodeData(server: ServerNode, fabric: Fabric) {
        const serverStore = server.env.get(ServerNodeStore);

        const baseStorage = serverStore.storage;

        // Note that we're assuming this is invoked prior to the node initializing peers
        const baseNodeStorage = baseStorage.createContext("nodes");

        // Initialize component to manage commissioned nodes storage in legacy storage format data migration needed
        const controllerStore = new ControllerStore(server.id, baseStorage);
        if (!(await controllerStore.nodesStorage.has("commissionedNodes"))) {
            logger.debug("No former commissioned nodes to migrate.");
            // No commissionedNodes key, so simply migrate nothing
            return;
        }

        const peerStore = new CommissionedNodeStore(controllerStore, fabric, server.peers);
        const peers = await peerStore.loadPeers();
        if (peers.length === 0) {
            logger.debug("No former commissioned nodes to migrate.");
            return;
        }
        this.#legacyPeerStore = peerStore;

        const migratedPeers = new Set<string>();

        const newClientStores = serverStore.clientStores;
        for (const { address: peerAddress, discoveryData, deviceData, operationalAddress } of peers) {
            logger.debug(`Migrating data for commissioned node ${peerAddress.toString()}`);
            const clientNode = server.peers.get(peerAddress);
            if (clientNode !== undefined) {
                logger.debug(`Node ${peerAddress.nodeId} seems already migrated, skipping.`);
                if (clientNode.stateOf(NetworkClient).autoSubscribe) {
                    logger.debug(` Disabling auto subscribe on node ${peerAddress.nodeId}`);
                    await clientNode.setStateOf(NetworkClient, { autoSubscribe: false });
                }
                continue;
            }

            const id = newClientStores.allocateId(); // Manually allocate next id to allow data migration before we add the node
            logger.debug(`Allocated client node store id ${id} for node ${peerAddress.toString()}`);
            migratedPeers.add(id);

            logger.debug(
                ` Migrating stored data for node ${peerAddress.toString()}: node-${peerAddress.nodeId.toString()}`,
            );
            const oldDataStore = await controllerStore.clientNodeStore(peerAddress.nodeId.toString());
            const maxEventNumber = await oldDataStore.get<EventNumber>("__maxEventNumber__", EventNumber(0));

            const peerStorage = baseNodeStorage.createContext(id);
            const endpointStorage = peerStorage.createContext("endpoints");
            const oldEndpoints = await oldDataStore.contexts();
            if (oldEndpoints.length === 0) {
                logger.info(`No endpoint data to migrate for node ${peerAddress.toString()}`);
            }
            for (const ep of oldEndpoints) {
                logger.debug(`  Migrating data for endpoint ${ep} of node ${peerAddress.toString()}`);
                const oldEndpointStorage = oldDataStore.createContext(ep);
                const newEndpointStorage = endpointStorage.createContext(ep);
                for (const cluster of await oldEndpointStorage.contexts()) {
                    logger.debug(
                        `    Migrating data for cluster ${cluster} of endpoint ${ep} of node ${peerAddress.toString()}`,
                    );
                    const oldClusterStorage = oldEndpointStorage.createContext(cluster);
                    const newClusterStorage = newEndpointStorage.createContext(cluster);
                    for (const key of await oldClusterStorage.keys()) {
                        const value = await oldClusterStorage.get(key);
                        if (key === "__version__") {
                            await newClusterStorage.set(key, value);
                        } else if (isObject(value) && "value" in value) {
                            // Old storage contained "value" and "attributeName", just store value now
                            await newClusterStorage.set(key, value.value);
                        }
                    }
                }
            }

            const commissioning = RemoteDescriptor.toLongForm({
                ...(discoveryData ? deviceData : {}),
                addresses: operationalAddress ? [operationalAddress] : [],
            });
            logger.debug(
                `Initialize node store for migrated node ${peerAddress.toString()}`,
                Diagnostic.dict({ maxEventNumber, ...commissioning }),
            );
            const node = await server.peers.forAddress(peerAddress, { id });
            await node.set({
                commissioning,
                network: {
                    maxEventNumber,
                    autoSubscribe: false,
                },
            });

            if ((await oldDataStore.contexts()).length) {
                //logger.info(`Deleting old storage for node ${peerAddress.nodeId}`);
                //await oldDataStore.clearAll(); // TODO
            }
        }

        //await controllerStore.nodesStorage.delete("commissionedNodes"); // TODO

        this.#migratedPeerObservers.on(server.peers.clusterInstalled(BasicInformationClient), peer => {
            if (!migratedPeers.has(peer.id)) {
                logger.info(`Migrating commissioned node ${peer.id} to old format`);
                migratedPeers.add(peer.id);
                peerStore.save();
            }
        });
        this.#migratedPeerObservers.on(server.peers.deleted, async peer => {
            migratedPeers.delete(peer.id);
            logger.info(`Deleted commissioned node ${peer.id} from old format`);
            peerStore.save(peer.id);
        });

        logger.info("Commissioned nodes migration completed.");
        await controllerStore.close();
    }
}

export namespace MatterController {
    export interface ConnectOptions extends PeerConnectionOptions {
        allowUnknownPeer?: boolean;
    }
}

/**
 * Only used for Node data migration
 */
class CommissionedNodeStore {
    #peers: Peers;
    #controllerStore: ControllerStoreInterface;
    #fabric: Fabric;
    #saves = new BasicMultiplex();

    constructor(controllerStore: ControllerStoreInterface, fabric: Fabric, peers: Peers) {
        this.#controllerStore = controllerStore;
        this.#fabric = fabric;
        this.#peers = peers;
    }

    async loadPeers() {
        if (!(await this.#controllerStore.nodesStorage.has("commissionedNodes"))) {
            return [];
        }

        const commissionedNodes = (await this.#controllerStore.nodesStorage.get(
            "commissionedNodes",
        )) as StoredOperationalPeer[];

        const nodes = new Array<CommissionedPeer>();

        for (const [nodeId, { operationalServerAddress, discoveryData, deviceData }] of commissionedNodes) {
            const address = this.#fabric.addressOf(nodeId);
            nodes.push({
                address,
                operationalAddress: operationalServerAddress,
                discoveryData,
                deviceData,
            } satisfies CommissionedPeer);
        }
        return nodes;
    }

    save(ignorePeer?: string) {
        this.#saves.add(
            this.#controllerStore.nodesStorage.set(
                "commissionedNodes",
                this.#peers
                    .map(peer => {
                        if ((ignorePeer !== undefined && peer.id === ignorePeer) || !peer.lifecycle.isCommissioned) {
                            return undefined;
                        }
                        const commissioningState = peer.maybeStateOf(CommissioningClient);
                        const address = commissioningState?.peerAddress;
                        const operationalServerAddress = commissioningState?.addresses?.[0];
                        const discoveryData =
                            commissioningState !== undefined
                                ? RemoteDescriptor.fromLongForm(commissioningState)
                                : undefined;
                        const deviceData = {
                            meta: ClientNodePhysicalProperties(peer),
                            basicInformation: peer.maybeStateOf(BasicInformationClient),
                        };

                        if (address === undefined) {
                            return;
                        }
                        return [
                            address.nodeId,
                            {
                                operationalServerAddress:
                                    operationalServerAddress !== undefined && operationalServerAddress.type === "udp"
                                        ? (ServerAddress(operationalServerAddress) as ServerAddressUdp)
                                        : undefined,
                                discoveryData,
                                deviceData,
                            },
                        ] satisfies StoredOperationalPeer;
                    })
                    .filter(details => details !== undefined) as SupportedStorageTypes,
            ),
        );
    }

    async close() {
        await this.#saves;
    }
}
