/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Behavior } from "#behavior/Behavior.js";
import { NodeActivity } from "#behavior/context/NodeActivity.js";
import { LocalActorContext } from "#behavior/context/server/LocalActorContext.js";
import { ProtocolService } from "#node/integration/ProtocolService.js";
import {
  camelize,
  Construction,
  describeList,
  Diagnostic,
  ImplementationError,
  Lifecycle,
  Logger,
  MaybePromise,
  Transaction
} from "@matter/general";
import { ClusterModel } from "@matter/model";
import { BehaviorInitializationError, EndpointBehaviorsError } from "../errors.js";
import { EndpointInitializer } from "./EndpointInitializer.js";
import { EndpointLifecycle } from "./EndpointLifecycle.js";
const logger = Logger.get("Behaviors");
class Behaviors {
  #endpoint;
  #supported;
  #backings = {};
  #events = {};
  #options;
  #protocol;
  #detachedObservers;
  /**
   * The {@link SupportedBehaviors} of the {@link Endpoint}.
   */
  get supported() {
    return this.#supported;
  }
  /**
   * Obtain the specific {@link Behavior.Type} used by the endpoint for implementation if the endpoint supports
   * {@link type}.
   */
  typeFor(type) {
    const supported = this.#supported[type.id];
    if (!supported) {
      return;
    }
    if (!supported.supports(type)) {
      return void 0;
    }
    return supported;
  }
  /**
   * The list of active behaviors.
   */
  get active() {
    return Object.values(this.#backings).map((backing) => backing.type);
  }
  get status() {
    const status = {};
    for (const key in this.#supported) {
      status[key] = this.#backings[key]?.status ?? Lifecycle.Status.Inactive;
    }
    return status;
  }
  get [Diagnostic.value]() {
    return Diagnostic.lifecycleList(this.status);
  }
  get detailedDiagnostic() {
    return Object.entries(this.#supported).map(([name, type]) => {
      const backing = this.#backings[name];
      const cluster = clusterOf(type);
      const result = [
        Diagnostic(backing?.status ?? Lifecycle.Status.Inactive, name),
        Diagnostic.dict({
          id: cluster?.id !== void 0 ? Diagnostic.hex(cluster.id) : void 0
        })
      ];
      if (!cluster) {
        return result;
      }
      const schema = type.schema;
      const elements = this.elementsOf(type);
      const elementDiagnostic = Array();
      if (schema instanceof ClusterModel) {
        const features = schema.supportedFeatures;
        if (features.size) {
          const titleMap = new Map(schema.features.map((f) => [f.name, f.title ?? f.name]));
          elementDiagnostic.push([
            Diagnostic.strong("features"),
            [...features].map((name2) => titleMap.get(name2) ?? name2)
          ]);
        }
      }
      if (elements.attributes.size) {
        const behaviorData = new Array();
        for (const attributeName of elements.attributes) {
          const attr = cluster.attributes?.[attributeName];
          if (attr) {
            behaviorData.push([
              attributeName,
              Diagnostic.dict({
                id: Diagnostic.hex(attr.id),
                val: backing?.stateView?.[attributeName],
                flags: Diagnostic.asFlags({
                  fabricScoped: attr.schema.fabricScoped
                })
              })
            ]);
          }
        }
        elementDiagnostic.push([Diagnostic.strong("attributes"), Diagnostic.list(behaviorData)]);
      }
      if (elements.commands.size) {
        elementDiagnostic.push([
          Diagnostic.strong("commands"),
          Diagnostic.list(
            [...elements.commands].map((name2) => {
              const command = cluster.commands?.[name2];
              if (!command) {
                return [name2];
              }
              const response = command.schema.responseModel;
              return [
                name2,
                Diagnostic.weak(
                  `(${Diagnostic.hex(command.id)}${response ? `/${Diagnostic.hex(response.id)}` : ""})`
                )
              ];
            })
          )
        ]);
      }
      if (elements.events.size) {
        elementDiagnostic.push(
          Diagnostic.strong("events"),
          Diagnostic.list([
            [...elements.events].map((name2) => {
              const event = cluster.events?.[name2];
              return event ? [name2, Diagnostic.weak(`(${Diagnostic.hex(event.id)})`)] : [name2];
            })
          ])
        );
      }
      if (elementDiagnostic.length) {
        result.push(Diagnostic.list(elementDiagnostic));
      }
      return result;
    });
  }
  constructor(endpoint, options) {
    const { type } = endpoint;
    if (typeof type?.behaviors !== "object") {
      throw new ImplementationError('EndpointType "behaviors" must be an array of Behavior.Type instances');
    }
    this.#endpoint = endpoint;
    this.#supported = type.behaviors;
    this.#options = options;
    for (const id in this.#supported) {
      const type2 = this.#supported[id];
      if (!(type2.prototype instanceof Behavior)) {
        throw new ImplementationError(`${endpoint}.${id}" is not a Behavior.Type`);
      }
      if (typeof type2.id !== "string") {
        throw new ImplementationError(`${endpoint}.${id} has no ID`);
      }
      this.#augmentEndpoint(type2);
    }
  }
  /**
   * Activate any behaviors designated for immediate activation.  Returns a promise iff any behaviors have ongoing
   * initialization.
   *
   * Throws an error if any behavior crashes, but we allow all behaviors to settle before throwing.  The goal is to
   * surface multiple configuration errors and prevent inconsistent state caused by partial initialization.
   */
  initialize() {
    if (!this.#endpoint.lifecycle.isInstalled) {
      throw new ImplementationError(`Cannot initialize behaviors because endpoint is not installed`);
    }
    const initializeBehaviors = (context) => {
      const agent = this.#endpoint.agentFor(context);
      for (const type of Object.values(this.supported)) {
        if (type.early) {
          this.activate(type, agent);
        }
      }
      let promise2 = Construction.all(
        {
          [Symbol.iterator]: () => {
            return Object.values(this.#backings)[Symbol.iterator]();
          }
        },
        (causes) => new EndpointBehaviorsError(causes)
      );
      const endpointInitializer = this.#endpoint.env.get(EndpointInitializer);
      if (promise2) {
        promise2 = promise2.then(() => endpointInitializer.behaviorsInitialized(agent));
      } else {
        promise2 = endpointInitializer.behaviorsInitialized(agent);
      }
      return promise2;
    };
    const activity = this.#endpoint.env.get(NodeActivity);
    let promise = LocalActorContext.act(`initialize<${this.#endpoint}>`, initializeBehaviors, {
      activity,
      lifetime: this.#endpoint.construction
    });
    const onReady = () => {
      this.#endpoint.lifecycle.change(EndpointLifecycle.Change.Ready);
    };
    if (promise) {
      promise = promise.then(onReady);
    } else {
      onReady();
    }
    return promise;
  }
  has(type) {
    if (typeof type === "string") {
      return !!this.#supported[type];
    }
    const myType = this.#supported[type.id];
    return myType === type || myType?.supports(type);
  }
  /**
   * Add behavior support dynamically at runtime.  Typically called via {@link Agent.require}.
   */
  require(type, options) {
    if (this.#supported[type.id]) {
      if (!this.has(type)) {
        throw new ImplementationError(
          `Cannot require ${this.#endpoint}.${type.id} because incompatible implementation already exists`
        );
      }
      return;
    }
    this.inject(type, options);
  }
  /**
   * Create a behavior synchronously.  Fails if the behavior is not fully initialized.
   */
  createSync(type, agent) {
    const behavior = this.createMaybeAsync(type, agent);
    if (MaybePromise.is(behavior)) {
      throw new ImplementationError(
        `Synchronous access to ${this.#endpoint}.${type.id} is impossible because it is still initializing`
      );
    }
    return behavior;
  }
  /**
   * Create a behavior asynchronously.  Waits for the behavior to complete initialization.
   */
  async createAsync(type, agent) {
    return MaybePromise.then(
      () => this.createMaybeAsync(type, agent),
      void 0,
      (e) => {
        const backing = this.#backings[type.id];
        if (!backing) {
          throw e;
        }
        backing.construction.assert(backing.toString());
      }
    );
  }
  /**
   * Create a behavior, possibly asynchronously.
   *
   * This method returns a {@link Promise} only if await is necessary so the behavior can be used immediately if
   * possible.
   */
  createMaybeAsync(type, agent) {
    this.activate(type, agent);
    const backing = this.#backings[type.id];
    const getBehavior = () => {
      if (agent.isLoaded(type)) {
        return agent.get(type);
      }
      return backing.createBehavior(agent, type);
    };
    if (backing.construction.status === Lifecycle.Status.Initializing) {
      return backing.construction.then(() => getBehavior()).catch(() => {
        backing.construction.assert(backing.toString());
        return getBehavior();
      });
    }
    backing.construction.assert(backing.toString());
    return getBehavior();
  }
  /**
   * Activate a behavior.
   *
   * Semantically identical to createAsync() but does not return a {@link Promise} or throw an error.
   *
   * Behaviors that fail initialization will be marked with crashed {@link status}.
   */
  activate(type, agent) {
    if (!this.#backings[type.id]) {
      this.#createBacking(type, agent);
    }
  }
  /**
   * Determine if a specified behavior is supported and active.
   */
  isActive(type) {
    if (typeof type === "string") {
      return this.#backings[type] !== void 0;
    }
    const backing = this.#backings[type.id];
    return !!backing && backing.type.supports(type);
  }
  /**
   * Destroy all behaviors that are initialized (have backings present).  The object may be reused after close.
   */
  async close() {
    const dispose = async (context) => {
      const agent = this.#endpoint.agentFor(context);
      let destroyNow = new Set(Object.keys(this.#backings));
      while (destroyNow.size) {
        for (const key in this.#backings) {
          const dependencies = this.#backings[key].type.dependencies;
          if (!dependencies) {
            continue;
          }
          for (const type of dependencies) {
            destroyNow.delete(type.id);
          }
          if (!destroyNow.size) {
            throw new ImplementationError("Cannot destroy behaviors due to circular dependency");
          }
        }
        for (const id of destroyNow) {
          const backing = this.#backings[id];
          this.#protocol?.deleteCluster(backing);
          await this.#backings[id].close(agent);
          delete this.#backings[id];
        }
        destroyNow = new Set(Object.keys(this.#backings));
      }
      for (const emitter of Object.values(this.#events)) {
        emitter[Symbol.dispose]?.();
      }
      const transaction = agent.context.transaction;
      if (transaction.status === Transaction.Status.Exclusive) {
        await transaction.commit();
      }
    };
    await LocalActorContext.act(
      `close<${this.#endpoint}>`,
      dispose,
      {
        lifetime: this.#endpoint.construction
        // Note - do not close in an activity because this can cause deadlock
        // activity: this.#endpoint.env.get(NodeActivity),
      }
    );
  }
  /**
   * Add support for an additional behavior.
   *
   * This should generally only be used prior to initialization.  It may cause subtle errors if incompatible types are
   * injected once the endpoint is initialized.
   */
  inject(type, options, notify = true) {
    if (typeof type.id !== "string") {
      throw new ImplementationError("Behavior type has no ID");
    }
    if (!/^[a-z]/.test(type.id)) {
      throw new ImplementationError(
        `Behavior ID "${type.id}" must start with a lowercase letter (for example "${camelize(type.id)}")`
      );
    }
    if (options) {
      this.#options[type.id] = options;
    }
    if (this.#supported === this.#endpoint.type.behaviors) {
      this.#supported = { ...this.#supported };
    }
    this.#supported[type.id] = type;
    this.#augmentEndpoint(type);
    if (notify) {
      this.#endpoint.lifecycle.change(EndpointLifecycle.Change.ServersChanged);
    }
    if (!this.#endpoint.lifecycle.isInstalled) {
      return;
    }
    const activeBacking = this.#backings[type.id];
    if (activeBacking) {
      activeBacking.type = type;
    } else if (type.early) {
      this.#activateLate(type);
    }
  }
  /**
   * Drop support for a behavior.
   *
   * This is intended for synchronization with peers and should not be used for servers as Matter does not allow an
   * endpoint to change its set of supported clusters.
   */
  drop(id) {
    const supported = this.#supported[id];
    if (!supported) {
      return;
    }
    const type = this.#supported[id];
    delete this.#supported[id];
    let promise;
    const backing = this.#backings[id];
    if (backing) {
      logger.info(`Removing ${backing} from active endpoint`);
      promise = backing.close();
      delete this.#backings[id];
    }
    this.#endpoint.lifecycle.change(EndpointLifecycle.Change.ServersChanged);
    const events = this.#events[id];
    if (events) {
      let detachedObservers;
      for (const key in events) {
        const observable = events[key];
        if (observable && "detachObservers" in observable) {
          const detached = observable.detachObservers();
          if (detached) {
            (detachedObservers ??= {})[key] = detached;
          }
        }
      }
      if (detachedObservers) {
        (this.#detachedObservers ??= {})[id] = detachedObservers;
      }
      delete this.#events[id];
    }
    delete this.#endpoint.state[id];
    if (type.schema.id !== void 0) {
      delete this.#endpoint.state[type.schema.id];
    }
    delete this.#endpoint.events[id];
    return promise;
  }
  /**
   * Ensure a set of behavior requirements are met.  Throws an error detailing missing requirements.
   */
  validateRequirements(requirements) {
    if (!requirements) {
      return;
    }
    const missing = Array();
    for (const requirement of Object.values(requirements)) {
      let name = camelize(requirement.name, true);
      if (this.#endpoint.behaviors.has(requirement)) {
        continue;
      }
      const cluster = clusterOf(requirement);
      if (cluster) {
        const other = this.#endpoint.behaviors.supported[requirement.id];
        if (clusterOf(other)?.id === cluster.id) {
          continue;
        }
        if (cluster.id !== void 0) {
          name = `${name} (${Diagnostic.hex(cluster.id)})`;
        }
      }
      missing.push(name);
    }
    if (missing.length) {
      throw new ImplementationError(
        `${this.#endpoint} is missing required behaviors: ${describeList("and", ...missing)}`
      );
    }
  }
  /**
   * Obtain default values for a behavior.  This is state values as present when the behavior is first initialized for
   * a new endpoint.
   */
  defaultsFor(type) {
    let defaults;
    const options = this.#options[type.id];
    if (options) {
      for (const key in type.defaults) {
        if (key in options) {
          if (!defaults) {
            defaults = {};
          }
          defaults[key] = options[key];
        }
      }
    }
    const { variableService } = this.#endpoint.env.get(EndpointInitializer);
    if (variableService) {
      const vars = variableService.forBehaviorInstance(this.#endpoint, type);
      if (vars !== void 0) {
        defaults = { ...defaults, ...type.supervisor.cast(vars) };
      }
    }
    return defaults;
  }
  /**
   * Retrieve the options for a behavior type provided to the endpoint.
   */
  optionsFor(type) {
    return this.#options[type.id];
  }
  /**
   * Access internal state for a {@link Behavior}.
   *
   * Internal state is not a stable API and not intended for consumption outside the behavior.  However, it is not
   * truly private and may be accessed by tightly coupled implementation.
   *
   * As this API is intended for use by "friendly" code, it does not perform the same initialization assertions as
   * does access to {@link Behavior.State} and {@link Behavior.Events}.
   */
  internalsOf(type) {
    const backing = this.#backingFor(type);
    return backing.getInternal();
  }
  versionOf(type) {
    if (typeof type === "string") {
      const backing2 = this.#backings[type];
      return backing2?.maybeDatasource?.version;
    }
    const backing = this.#backingFor(type);
    return backing.datasource.version;
  }
  /**
   * @throws ImplementationError if the behavior is not supported.
   */
  backingFor(type) {
    if (!this.has(type)) {
      throw new ImplementationError(`Endpoint ${this.#endpoint} does not support behavior ${type.id}`);
    }
    return this.#backingFor(type);
  }
  /**
   * Access elements supported by a behavior.
   */
  elementsOf(type) {
    if (!this.has(type)) {
      throw new ImplementationError(`Endpoint ${this.#endpoint} does not support behavior ${type.id}`);
    }
    const elements = this.#backingFor(type).elements;
    if (elements === void 0) {
      throw new ImplementationError(
        `Endpoint ${this.#endpoint} behavior ${type.id} elements accessed before initialization`
      );
    }
    return elements;
  }
  /**
   * Access the state view of a behavior if loaded.
   */
  maybeStateOf(behaviorId) {
    const backing = this.#backings[behaviorId];
    return backing?.maybeDatasource?.view;
  }
  [Symbol.iterator]() {
    return Object.values(this.#supported)[Symbol.iterator]();
  }
  #activateLate(type) {
    const result = LocalActorContext.act(
      "behavior-late-activation",
      (context) => {
        this.activate(type, this.#endpoint.agentFor(context));
        const backing = this.#backingFor(type);
        return backing.construction.ready;
      },
      { lifetime: this.#endpoint.construction }
    );
    if (MaybePromise.is(result)) {
      result.then(void 0, (error) => {
        const backing = this.#backings[type.id];
        if (error instanceof BehaviorInitializationError) {
          logger.error(error);
        } else if (backing) {
          logger.error(`Error initializing ${backing}:`, error);
        } else {
          logger.error(`Unexpected rejection initializing ${type.name}:`, error);
        }
      });
    }
  }
  /**
   * Create a read-only online view of a behavior.
   */
  createOnlineView(type) {
    return this.#backingFor(type).datasource;
  }
  /**
   * Get backing for a behavior.
   */
  #backingFor(type) {
    if (this.#endpoint.construction.status !== Lifecycle.Status.Initializing) {
      this.#endpoint.construction.assert(this.#endpoint.toString(), `behavior ${type.id}`);
    }
    let backing = this.#backings[type.id];
    if (!backing) {
      this.#activateLate(type);
      backing = this.#backings[type.id];
      if (backing === void 0) {
        throw new BehaviorInitializationError(`${this.#endpoint}.${type.id}`, "initialization failed");
      }
    }
    return backing;
  }
  #createBacking(type, agent) {
    const myType = this.#getBehaviorType(type);
    if (!myType) {
      throw new BehaviorInitializationError(`Initializing ${this.#endpoint}.${type.id}: Unsupported behavior`);
    }
    const backing = this.#endpoint.env.get(EndpointInitializer).createBacking(this.#endpoint, myType);
    this.#backings[backing.type.id] = backing;
    if (backing.type !== myType) {
      this.#supported[backing.type.id] = backing.type;
    }
    if (!this.#protocol) {
      this.#protocol = this.#endpoint.env.get(ProtocolService);
    }
    backing.initializeDataSource();
    backing.construction.start(agent);
    return backing;
  }
  #getBehaviorType(type) {
    const myType = this.#supported[type.id];
    if (myType === void 0) {
      return myType;
    }
    if (typeof myType !== "function" || !(myType.prototype instanceof Behavior)) {
      throw new ImplementationError(`Endpoint behavior "${type.id}" implementation is not a Behavior`);
    }
    return myType;
  }
  /**
   * Updates endpoint "state" and "events" properties to include properties for a supported behavior.
   */
  #augmentEndpoint(type) {
    const { id, Events: Events2 } = type;
    const get = () => {
      const status = this.#endpoint.construction.status;
      if (status === Lifecycle.Status.Destroying || status === Lifecycle.Status.Destroyed) {
        return void 0;
      }
      return this.#backingFor(type).stateView;
    };
    Object.defineProperty(this.#endpoint.state, id, { get, enumerable: true, configurable: true });
    if (type.schema.id !== void 0) {
      Object.defineProperty(this.#endpoint.state, type.schema.id, { get, configurable: true });
    }
    const detachedObservers = this.#detachedObservers?.[type.id];
    if (detachedObservers) {
      delete this.#detachedObservers[type.id];
      const newEvents = new Events2();
      for (const key in detachedObservers) {
        const newEvent = newEvents[key];
        if (newEvent && "attachObservers" in newEvent) {
          newEvent.attachObservers(detachedObservers);
        }
      }
      this.#events[id] = newEvents;
    }
    Object.defineProperty(this.#endpoint.events, id, {
      get: () => {
        let events = this.#events[id];
        if (!events) {
          events = this.#events[id] = new Events2();
          if (typeof events.setContext === "function") {
            events.setContext(this.#endpoint, type);
          }
        }
        return events;
      },
      enumerable: true,
      configurable: true
    });
  }
}
function clusterOf(behavior) {
  return behavior?.cluster;
}
export {
  Behaviors
};
//# sourceMappingURL=Behaviors.js.map
