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

import type { RemoteActorContext } from "#behavior/context/server/RemoteActorContext.js";
import { Duration, InternalError, Logger, Seconds, Time, Timer, Worker } from "@matter/general";
import {
    assertRemoteActor,
    DeviceCommissioner,
    FailsafeContext,
    hasRemoteActor,
    PaseServer,
    SessionManager,
} from "@matter/protocol";
import {
    MINIMUM_COMMISSIONING_TIMEOUT,
    PAKE_PASSCODE_VERIFIER_LENGTH,
    STANDARD_COMMISSIONING_TIMEOUT,
    Status,
    StatusResponseError,
} from "@matter/types";
import { AdministratorCommissioning } from "@matter/types/clusters/administrator-commissioning";
import { AdministratorCommissioningBehavior } from "./AdministratorCommissioningBehavior.js";

/**
 * Extend the AdministratorCommissioning model to relax constraints on OpenCommissioningWindow fields.  This prevents
 * the interaction layer from rejecting with ConstraintError so the behavior can validate and return PakeParameterError.
 */
const openCommissioningWindow = AdministratorCommissioning.schema.commands.require("OpenCommissioningWindow");

const AdministratorCommissioningSchema = AdministratorCommissioning.schema.extend(
    undefined,
    openCommissioningWindow.extend(
        undefined,
        openCommissioningWindow.fields.extend("PakePasscodeVerifier", { constraint: "none" }),
        openCommissioningWindow.fields.extend("Iterations", { constraint: "none" }),
        openCommissioningWindow.fields.extend("Salt", { constraint: "none" }),
    ),
);

const AdministratorCommissioningBase = AdministratorCommissioningBehavior.for(
    AdministratorCommissioning,
    AdministratorCommissioningSchema,
);

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

/**
 * This is the default server implementation of AdministratorCommissioningBehavior.
 *
 * This implementation includes all features of AdministratorCommissioning.Cluster. You should use
 * AdministratorCommissioningServer.with to specialize the class for the features your implementation supports.
 */
export class AdministratorCommissioningServer extends AdministratorCommissioningBase {
    declare internal: AdministratorCommissioningServer.Internal;
    declare readonly state: AdministratorCommissioningServer.State;

    static override lockOnInvoke = false;

    /**
     * This method opens an Enhanced Commissioning Window (a dynamic passcode is used which was provided by the caller).
     */
    override async openCommissioningWindow({
        pakePasscodeVerifier,
        discriminator,
        iterations,
        salt,
        commissioningTimeout,
    }: AdministratorCommissioning.OpenCommissioningWindowRequest) {
        // We monkey patched the Tlv definition above, so take care about correct error handling
        if (pakePasscodeVerifier.byteLength !== PAKE_PASSCODE_VERIFIER_LENGTH) {
            throw new AdministratorCommissioning.PakeParameterError("PAKE passcode verifier length is invalid");
        }
        if (iterations < 1000 || iterations > 100_000) {
            throw new AdministratorCommissioning.PakeParameterError("PAKE iterations invalid");
        }
        if (salt.byteLength < 16 || salt.byteLength > 32) {
            throw new AdministratorCommissioning.PakeParameterError("PAKE salt has invalid length.");
        }

        const commissioner = this.env.get(DeviceCommissioner);

        const timeout = Seconds(commissioningTimeout);

        this.#assertCommissioningWindowRequirements(timeout, commissioner);

        this.#initializeCommissioningWindow(
            timeout,
            AdministratorCommissioning.CommissioningWindowStatus.EnhancedWindowOpen,
        );

        await this.env.get(DeviceCommissioner).allowEnhancedCommissioning(
            discriminator,
            PaseServer.fromVerificationValue(this.env.get(SessionManager), pakePasscodeVerifier, {
                iterations,
                salt,
            }),
            this.callback(this.#endCommissioning),
        );
    }

    /** This method opens a Basic Commissioning Window. The default passcode is used. */
    async openBasicCommissioningWindow({
        commissioningTimeout,
    }: AdministratorCommissioning.OpenBasicCommissioningWindowRequest) {
        const commissioner = this.env.get(DeviceCommissioner);

        const timeout = Seconds(commissioningTimeout);

        this.#assertCommissioningWindowRequirements(timeout, commissioner);

        this.#initializeCommissioningWindow(
            timeout,
            AdministratorCommissioning.CommissioningWindowStatus.BasicWindowOpen,
        );

        await commissioner.allowBasicCommissioning(this.callback(this.#endCommissioning));
    }

    /**
     * This method is used to revoke a commissioning window.
     *
     * Spec 1.5.1 requires Step 1 cleanup to run regardless of window state before checking the window.
     */
    override async revokeCommissioning() {
        logger.debug("Revoking commissioning window");

        // Step 1: Regardless of window state — terminate PASE sessions and expire PASE-held fail-safe
        const paseSession = this.env.get(SessionManager).getPaseSession();
        if (paseSession) {
            await paseSession.initiateClose();
        }

        if (this.env.has(FailsafeContext)) {
            const failsafeContext = this.env.get(FailsafeContext);
            if (failsafeContext) {
                await failsafeContext.close((this.context as RemoteActorContext).exchange);
            }
        }

        // Step 2: If window was not open, return error
        if (this.internal.commissioningWindowTimeout === undefined) {
            throw new AdministratorCommissioning.WindowNotOpenError(
                "No commissioning window is opened that could be revoked.",
            );
        }

        // Step 3: Close the commissioning window
        await this.#closeCommissioningWindow();
    }

    /**
     * Called whenever a Commissioning/Announcement Window is opened by this cluster. This method starts the timer and
     * adjusts the needed attributes.
     */
    #initializeCommissioningWindow(
        commissioningTimeout: Duration,
        windowStatus: AdministratorCommissioning.CommissioningWindowStatus,
    ) {
        if (this.internal.commissioningWindowTimeout !== undefined) {
            // Should never happen, but let's make sure
            throw new InternalError("Commissioning window already initialized.");
        }
        const actor = hasRemoteActor(this.context) ? this.context.session.via : "local actor";
        logger.debug(`Commissioning window timer started for ${commissioningTimeout} seconds for ${actor}.`);
        this.internal.commissioningWindowTimeout = Time.getTimer(
            "Commissioning timeout",
            commissioningTimeout,
            this.callback(this.#commissioningTimeout),
        ).start();

        assertRemoteActor(this.context);

        const adminFabric = this.context.session.associatedFabric;

        this.state.windowStatus = windowStatus;
        this.state.adminFabricIndex = adminFabric.fabricIndex;
        this.state.adminVendorId = adminFabric.rootVendorId;

        const removeCallback = this.callback(this.#fabricRemovedCallback);

        this.internal.stopMonitoringFabricForRemoval = () => {
            adminFabric.deleting.off(removeCallback);
        };

        this.context.session.associatedFabric.deleting.on(removeCallback);
    }

    /**
     * This method validates if a commissioning window can be opened and throws various exceptions in case of failures.
     */
    #assertCommissioningWindowRequirements(commissioningTimeout: Duration, commissioner: DeviceCommissioner) {
        if (this.internal.commissioningWindowTimeout !== undefined) {
            throw new AdministratorCommissioning.BusyError("A commissioning window is already opened");
        }

        if (commissioningTimeout > this.internal.maximumCommissioningTimeout) {
            throw new StatusResponseError(
                `Commissioning timeout must not exceed ${this.internal.maximumCommissioningTimeout} seconds.`,
                Status.InvalidCommand,
            );
        }

        if (commissioningTimeout < this.internal.minimumCommissioningTimeout) {
            throw new StatusResponseError(
                `Commissioning timeout must not be lower then ${this.internal.minimumCommissioningTimeout} seconds.`,
                Status.InvalidCommand,
            );
        }

        if (commissioner.isFailsafeArmed) {
            throw new AdministratorCommissioning.BusyError("Failsafe timer armed, assume commissioning in progress.");
        }
    }

    /**
     * This method is used internally when the commissioning window timer expires or the commissioning was completed.
     */
    #endCommissioning() {
        logger.debug("Ending commissioning");
        if (this.internal.commissioningWindowTimeout !== undefined) {
            this.internal.commissioningWindowTimeout.stop();
            this.internal.commissioningWindowTimeout = undefined;
        }

        this.internal.stopMonitoringFabricForRemoval?.();
        this.state.adminFabricIndex = null;

        this.state.windowStatus = AdministratorCommissioning.CommissioningWindowStatus.WindowNotOpen;
        this.state.adminFabricIndex = null;
        this.state.adminVendorId = null;
    }

    /**
     * Closes the commissioning window per the matter specification.
     */
    async #closeCommissioningWindow() {
        using _closing = this.lifetime.join("closing commissioning window");
        await this.env.get(DeviceCommissioner).endCommissioning();
    }

    /**
     * Close commissioning window on timeout when there's nobody to await the resulting promise
     * */
    #commissioningTimeout() {
        this.env.runtime.add(
            Worker({
                name: "closing commissioning window",
                done: this.#closeCommissioningWindow(),
            }),
        );
    }

    /**
     * Invoked when fabric is removed.
     */
    #fabricRemovedCallback() {
        this.state.adminFabricIndex = null;
        this.internal.stopMonitoringFabricForRemoval?.();
    }

    /**
     * Clean up resources and stop the timer when the behavior is destroyed.
     */
    override [Symbol.asyncDispose]() {
        if (this.internal.commissioningWindowTimeout !== undefined) {
            this.internal.commissioningWindowTimeout.stop();
            this.internal.commissioningWindowTimeout = undefined;
        }
    }
}

export namespace AdministratorCommissioningServer {
    export class Internal {
        commissioningWindowTimeout?: Timer;
        stopMonitoringFabricForRemoval?: () => void;

        /**
         * Mandated by spec; should only be modified in testing.
         */
        minimumCommissioningTimeout = MINIMUM_COMMISSIONING_TIMEOUT;

        /**
         * Commissioning beyond the standard 15-minute window is "extended commissioning" and has limitations on
         * advertisement.  We default to the standard window.
         */
        maximumCommissioningTimeout = STANDARD_COMMISSIONING_TIMEOUT;
    }

    export class State extends AdministratorCommissioningBase.State {
        // Spec doesn't declare a default here so set manually
        override windowStatus = AdministratorCommissioning.CommissioningWindowStatus.WindowNotOpen;
    }
}
