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

import { GroupKeyManagementServer } from "#behaviors/group-key-management";
import { IdentifyBehavior } from "#behaviors/identify";
import { ScenesManagementServer } from "#behaviors/scenes-management";
import { Endpoint } from "#endpoint/Endpoint.js";
import { RootEndpoint } from "#endpoints/root";
import { InternalError, Logger } from "@matter/general";
import { assertRemoteActor, Fabric } from "@matter/protocol";
import { Status, StatusResponseError } from "@matter/types";
import { Groups } from "@matter/types/clusters/groups";
import { GroupsBehavior } from "./GroupsBehavior.js";

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

/**
 * Extend the Groups model to relax constraints on command request fields.  This prevents the interaction layer from
 * rejecting with ConstraintError so the behavior can validate and return proper Status responses.
 */
const { commands } = Groups.schema;
const addGroup = commands.require("AddGroup");
const addGroupIfIdentifying = commands.require("AddGroupIfIdentifying");
const addGroupResponse = commands.require("AddGroupResponse");
const viewGroupResponse = commands.require("ViewGroupResponse");
const removeGroupResponse = commands.require("RemoveGroupResponse");

const GroupsSchema = Groups.schema.extend(
    undefined,
    addGroup.extend(
        undefined,
        addGroup.fields.extend("GroupId", { constraint: "none" }),
        addGroup.fields.extend("GroupName", { constraint: "none" }),
    ),
    addGroupIfIdentifying.extend(
        undefined,
        addGroupIfIdentifying.fields.extend("GroupId", { constraint: "none" }),
        addGroupIfIdentifying.fields.extend("GroupName", { constraint: "none" }),
    ),
    addGroupResponse.extend(undefined, addGroupResponse.fields.extend("GroupId", { constraint: "none" })),
    viewGroupResponse.extend(
        undefined,
        viewGroupResponse.fields.extend("GroupId", { constraint: "none" }),
        viewGroupResponse.fields.extend("GroupName", { constraint: "none" }),
    ),
    removeGroupResponse.extend(undefined, removeGroupResponse.fields.extend("GroupId", { constraint: "none" })),
);

// We enable group names by default
const GroupsBase = GroupsBehavior.for(Groups, GroupsSchema).with(Groups.Feature.GroupNames);

/**
 * This is the default server implementation of {@link GroupsBehavior}.
 */
export class GroupsServer extends GroupsBase {
    override initialize() {
        this.state.nameSupport.groupNames = this.features.groupNames;
    }

    // We need to search the root here ourselves because we cannot include ServerNode because else we generate a
    // circular dependency
    get #rootEndpoint(): Endpoint<RootEndpoint> {
        const rootEndpoint = this.endpoint.ownerOfType(RootEndpoint);
        if (rootEndpoint === undefined) {
            throw new InternalError("RootEndpoint not found");
        }
        return rootEndpoint;
    }

    async #actOnGroupKeyManagement<T>(
        act: (fabric: Fabric, groupKeyManagement: GroupKeyManagementServer) => T,
    ): Promise<T> {
        assertRemoteActor(this.context);
        const agent = this.#rootEndpoint.agentFor(this.context);
        const gkm = agent.get(GroupKeyManagementServer);
        await agent.context.transaction.addResources(gkm);
        await agent.context.transaction.begin();
        return act(this.context.session.associatedFabric, gkm);
    }

    override async addGroup({ groupId, groupName }: Groups.AddGroupRequest): Promise<Groups.AddGroupResponse> {
        assertRemoteActor(this.context);
        const fabric = this.context.session.associatedFabric;

        if (groupId < 1) {
            return { status: Status.ConstraintError, groupId };
        }
        if (groupName.length > 16) {
            return { status: Status.ConstraintError, groupId };
        }

        if (!fabric.groups.groupKeyIdMap.has(groupId)) {
            return { status: Status.UnsupportedAccess, groupId };
        }

        const endpointNumber = this.endpoint.number;

        try {
            await this.#actOnGroupKeyManagement((fabric, gkm) =>
                gkm.addEndpointForGroup(fabric, groupId, endpointNumber, groupName),
            );
        } catch (error) {
            logger.debug("Could not add group", error);
            StatusResponseError.accept(error);
            return { status: error.code, groupId };
        }

        return { status: Status.Success, groupId };
    }

    override viewGroup({ groupId }: Groups.ViewGroupRequest): Groups.ViewGroupResponse {
        assertRemoteActor(this.context);
        const fabric = this.context.session.associatedFabric;

        if (groupId < 1) {
            return { status: Status.ConstraintError, groupId, groupName: "" };
        }

        const fabricIndex = fabric.fabricIndex;
        const endpointNumber = this.endpoint.number;

        const { groupTable } = this.#rootEndpoint.stateOf(GroupKeyManagementServer);
        const groupEntry = groupTable.find(entry => entry.groupId === groupId && entry.fabricIndex === fabricIndex);
        if (groupEntry === undefined || !groupEntry.endpoints.includes(endpointNumber)) {
            return { status: Status.NotFound, groupId, groupName: "" };
        }
        return { status: Status.Success, groupId, groupName: groupEntry.groupName ?? "" };
    }

    override async getGroupMembership({
        groupList,
    }: Groups.GetGroupMembershipRequest): Promise<Groups.GetGroupMembershipResponse> {
        assertRemoteActor(this.context);
        const fabric = this.context.session.associatedFabric;
        const fabricIndex = fabric.fabricIndex;
        const endpointNumber = this.endpoint.number;

        const { groupTable } = this.#rootEndpoint.stateOf(GroupKeyManagementServer);
        const endpointGroups = groupTable.filter(
            entry => entry.endpoints.includes(endpointNumber) && entry.fabricIndex === fabricIndex,
        );
        const fabricGroupsList = endpointGroups.map(entry => entry.groupId);

        if (groupList.length === 0) {
            return { capacity: 0xfe - fabricGroupsList.length, groupList: fabricGroupsList };
        }

        const filteredGroupsList = groupList.filter(groupId => fabricGroupsList.includes(groupId));
        return { capacity: 0xfe - fabricGroupsList.length, groupList: filteredGroupsList };
    }

    override async removeGroup({ groupId }: Groups.RemoveGroupRequest): Promise<Groups.RemoveGroupResponse> {
        if (groupId < 1) {
            return { status: Status.ConstraintError, groupId };
        }

        try {
            assertRemoteActor(this.context);
            if (
                await this.#actOnGroupKeyManagement((fabric, gkm) =>
                    gkm.removeEndpoint(fabric, this.endpoint.number, groupId),
                )
            ) {
                if (this.agent.has(ScenesManagementServer)) {
                    this.agent
                        .get(ScenesManagementServer)
                        .removeScenesForGroupOnFabric(this.context.session.associatedFabric.fabricIndex, groupId);
                }
                return { status: Status.Success, groupId };
            }
            return { status: Status.NotFound, groupId };
        } catch (error) {
            StatusResponseError.accept(error);
            return { status: error.code, groupId };
        }
    }

    override async removeAllGroups() {
        try {
            assertRemoteActor(this.context);
            await this.#actOnGroupKeyManagement((fabric, gkm) => gkm.removeEndpoint(fabric, this.endpoint.number));
            if (this.agent.has(ScenesManagementServer)) {
                this.agent
                    .get(ScenesManagementServer)
                    .removeScenesForAllGroupsForFabric(this.context.session.associatedFabric.fabricIndex);
            }
        } catch (error) {
            StatusResponseError.accept(error);
            throw error;
        }
    }

    override async addGroupIfIdentifying({ groupId, groupName }: Groups.AddGroupIfIdentifyingRequest) {
        if (this.endpoint.stateOf(IdentifyBehavior).identifyTime > 0) {
            // We identify ourselves currently
            const { status } = await this.addGroup({ groupId, groupName });
            if (status !== Status.Success) {
                throw new StatusResponseError(`Failed to add group ${groupId}`, status);
            }
        }
    }
}
