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

import type { ClientGroup } from "#node/ClientGroup.js";
import type { ClientNode } from "#node/ClientNode.js";
import type { Node } from "#node/Node.js";
import {
    Construction,
    MatterAggregateError,
    MemoryStorageDriver,
    StorageContext,
    StorageManager,
} from "@matter/general";
import { ClientNodeStore } from "./ClientNodeStore.js";

/**
 * Prefix for incrementally assigned client IDs that are usually commissioned, so an increasing ID is used instead the
 * node id.
 */
const CLIENT_ID_PREFIX = "peer";

/**
 * Manages {@link ClientNodeStore}s for a {@link Node}.
 */
export class ClientNodeStores {
    #storage: StorageContext;
    #stores = {} as Record<string, ClientNodeStore>;
    #construction: Construction<ClientNodeStores>;
    #nextAutomaticId = 1;

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

    constructor(storage: StorageContext) {
        this.#storage = storage;
        this.#construction = Construction(this);
        this.#construction.start();
    }

    async [Construction.construct]() {
        const contexts = await this.#storage.contexts();

        for (const id of contexts) {
            // Keep the auto-allocation counter ahead of any persisted "peerN" id; custom ids are restored too.
            if (id.startsWith(CLIENT_ID_PREFIX)) {
                const num = Number.parseInt(id.slice(CLIENT_ID_PREFIX.length));
                if (Number.isFinite(num) && this.#nextAutomaticId <= num) {
                    this.#nextAutomaticId = num + 1;
                }
            }

            this.#createNodeStore(id, true);
        }

        await MatterAggregateError.allSettled(
            Object.values(this.#stores).map(store => store.construction.ready),
            "Error while initializing client stores",
        );
    }

    /**
     * Allocate a stable local ID for a peer.
     *
     * The ID may be preassigned or we will assign using an incrementing sequential number.  The number is reserved for
     * the life of this process or, if data is persisted, until erased.
     */
    allocateId() {
        this.#construction.assert();

        return `${CLIENT_ID_PREFIX}${this.#nextAutomaticId++}`;
    }

    /**
     * Get the store for a single {@link ClientNode} or peer Id.
     *
     * These stores are cached internally by Id.
     */
    storeForNode(nodeOrId: ClientNode | string): ClientNodeStore {
        this.#construction.assert();

        if (typeof nodeOrId !== "string") {
            nodeOrId = nodeOrId.id;
        }

        const store = this.#stores[nodeOrId];
        if (store) {
            return store;
        }

        return this.#createNodeStore(nodeOrId);
    }

    storeForGroup(node: ClientGroup): ClientNodeStore {
        this.#construction.assert();

        const store = this.#stores[node.id];
        if (store) {
            return store;
        }

        return this.#createGroupStore(node.id);
    }

    /**
     * List all nodes present.
     */
    get knownIds() {
        this.#construction.assert();

        return Object.keys(this.#stores);
    }

    async close() {
        await this.construction.close(async () => {
            const stores = Object.values(this.#stores);
            this.#stores = {};
            await MatterAggregateError.allSettled(
                stores.map(store => store.construction.close()),
                "Error while closing client stores",
            );
        });
    }

    /**
     * Group stores are always created with a memory backend as they are transient.
     */
    #createGroupStore(id: string) {
        const manager = new StorageManager(new MemoryStorageDriver());
        manager.initialize();
        const store = new ClientNodeStore(id, manager.createContext(id), false);
        store.construction.start();
        this.#stores[id] = store;
        return store;
    }

    #createNodeStore(id: string, isPreexisting = false) {
        const store = new ClientNodeStore(
            id,
            this.#storage.createContext(id),
            isPreexisting,
            () => delete this.#stores[id],
        );
        store.construction.start();
        this.#stores[id] = store;
        return store;
    }
}
