/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { isClientBehavior } from "#behavior/cluster/cluster-behavior-utils.js";
import { Migration } from "#behavior/state/migrations/Migration.js";
import { BehaviorInitializationError } from "#endpoint/errors.js";
import { ChangeNotificationService } from "#node/integration/ChangeNotificationService.js";
import { ProtocolService } from "#node/integration/ProtocolService.js";
import {
  Construction,
  Entropy,
  ImplementationError,
  InternalError,
  Lifecycle,
  Lifetime,
  Logger,
  MaybePromise
} from "@matter/general";
import { Datasource } from "../state/managed/Datasource.js";
import { BackingEvents } from "./BackingEvents.js";
import { Reactors } from "./Reactors.js";
const logger = Logger.get("BehaviorBacking");
class BehaviorBacking {
  #endpoint;
  #type;
  #protocol;
  #changeTracking;
  #internal;
  #events;
  #options;
  #datasource;
  #reactors;
  #construction;
  get construction() {
    return this.#construction;
  }
  constructor(endpoint, type, store, options) {
    this.#endpoint = endpoint;
    this.#type = type;
    this.#protocol = endpoint.env.get(ProtocolService);
    this.#changeTracking = endpoint.env.get(ChangeNotificationService);
    this.store = store;
    this.#options = options;
    this.#construction = Construction(this);
    this.#construction.onError((error) => {
      if (endpoint.lifecycle.isReady) {
        if (error instanceof BehaviorInitializationError) {
          logger.error(error);
        } else {
          logger.error(`Error initializing ${this}:`, error);
        }
      }
    });
  }
  toString() {
    return this.path.toString();
  }
  get path() {
    return this.#endpoint.path.at(this.type.id);
  }
  /**
   * Initialize state by applying values from options and invoking the behavior's initialize() function.
   *
   * Initiated via {@link Construction#start} by Behaviors class once the backing is installed.
   */
  [Construction.construct](agent) {
    let crashError;
    const crash = (cause) => {
      if (!crashError) {
        crashError = new BehaviorInitializationError(`Error initializing ${this}`, cause);
      }
      throw crashError;
    };
    try {
      const behavior = this.#lifecycleInstance(agent);
      const promise = this.invokeInitializer(behavior, this.#options);
      return MaybePromise.then(promise, () => this.#protocol.addCluster(this), crash);
    } catch (e) {
      crash(e);
    }
  }
  get [Lifetime.owner]() {
    return this.#endpoint.construction;
  }
  initializeDataSource() {
    if (this.#datasource) {
      return;
    }
    if (this.store.initialValues !== void 0) {
      Migration.migrate(this.type, this.store.initialValues);
    }
    this.#datasource = Datasource(this.datasourceOptions);
  }
  /**
   * Destroy the backing.
   */
  close(agent) {
    const initialized = this.construction.status === Lifecycle.Status.Active;
    if (!initialized) {
      agent = void 0;
    }
    return this.construction.close(() => {
      let result = MaybePromise.then(
        () => this.#reactors?.close(),
        () => {
          this.#reactors = void 0;
        }
      );
      if (agent) {
        result = MaybePromise.then(result, () => this.#invokeClose(agent));
      }
      this.#datasource?.close();
      this.#datasource = void 0;
      return result;
    });
  }
  /**
   * Set state from options and invoke {@link Behavior.invokeInitializer}.
   *
   * This is an optional extension point for derivatives.  Errors thrown here are recorded and place the behavior into
   * crashed state.
   */
  invokeInitializer(behavior, options) {
    return behavior.initialize(options);
  }
  /**
   * The {@link Endpoint} that owns the behavior.
   */
  get endpoint() {
    return this.#endpoint;
  }
  /**
   * The {@link Behavior.Type} backed.
   */
  get type() {
    return this.#type;
  }
  set type(type) {
    if (type.supports(this.#type)) {
      this.#type = type;
      return;
    }
    logger.info(`Updated behavior ${this} to match changed peer cluster structure`);
    this.#type = type;
    this.refreshForChangedType();
  }
  /**
   * Rebuild state derived from {@link type} after the backed cluster structure changes.  Derivatives that cache
   * additional type-derived data override to refresh it.
   */
  refreshForChangedType() {
    const datasource = this.#datasource;
    if (datasource === void 0) {
      return;
    }
    const store = this.store;
    if (!("reclaimValues" in store) || typeof store.reclaimValues !== "function") {
      return;
    }
    store.reclaimValues();
    datasource.close();
    this.#datasource = Datasource(this.datasourceOptions);
  }
  /**
   * Create an instance of the backed {@link Behavior}.
   *
   * Derivatives may override to perform additional setup beyond simple instantiation.
   */
  createBehavior(agent, type) {
    const behavior = new this.#type(agent, this);
    if (behavior instanceof type || isClientBehavior(type)) {
      return behavior;
    }
    throw new ImplementationError(
      `Cannot create ${this.#endpoint}.${type.id} because installed implementation is incompatible`
    );
  }
  /**
   * The source of raw data that backs managed state instances.
   */
  get datasource() {
    if (!this.#datasource) {
      throw new InternalError("Datasource not yet initialized");
    }
    return this.#datasource;
  }
  /**
   * Access the datasource if present.
   */
  get maybeDatasource() {
    return this.#datasource;
  }
  get datasourceOptions() {
    return {
      entropy: this.#endpoint.env.get(Entropy),
      location: {
        path: this.#endpoint.path.at(this.#type.id).at("state"),
        endpoint: this.#endpoint.number,
        cluster: this.type.schema.tag === "cluster" ? this.type.schema.id : void 0
      },
      supervisor: this.type.supervisor,
      type: this.type.State,
      events: this.events,
      defaults: this.#endpoint.behaviors.defaultsFor(this.type),
      store: this.store,
      owner: this.#endpoint,
      onChange: this.broadcastChanges.bind(this)
    };
  }
  /**
   * The data provider for {@link datasource}.
   */
  store;
  /**
   * Obtain internal state for a behavior instance.
   */
  getInternal() {
    if (!this.#internal) {
      this.#internal = new this.#type.Internal();
    }
    return this.#internal;
  }
  /**
   * Access the event object.  Unlike state, the events object does not vary by instance.
   */
  get events() {
    if (!this.#events) {
      this.#events = BackingEvents(this);
    }
    return this.#events;
  }
  /**
   * The status of the behavior.
   */
  get status() {
    return this.construction.status;
  }
  /**
   * A read-only offline view of behavior state.
   */
  get stateView() {
    return this.datasource.view ?? {};
  }
  /**
   * Install a reactor.
   */
  reactTo(observable, reactor, options) {
    if (!this.#reactors) {
      this.#reactors = new Reactors(this);
    }
    this.#reactors.add(observable, reactor, options);
  }
  /**
   * Terminate reactions.
   */
  async stopReacting(selector) {
    if (this.#reactors === void 0) {
      return;
    }
    if (selector?.observable === void 0 && selector?.reactor === void 0) {
      await this.#reactors.close();
      this.#reactors = void 0;
      return;
    }
    await this.#reactors.remove(selector);
  }
  /**
   * Invoke {@link Behavior.destroy} to clean up application logic.
   */
  #invokeClose(agent) {
    const behavior = this.#lifecycleInstance(agent);
    return MaybePromise.finally(
      () => MaybePromise.then(
        () => behavior?.[Symbol.asyncDispose](),
        void 0,
        (e) => logger.warn(`Destroying ${this}:`, e)
      ),
      () => this.#events?.[Symbol.dispose]()
    );
  }
  /**
   * Obtain a behavior instance for lifecycle methods (initialize and close).
   *
   * Under these circumstances we can't use {@link Agent.get} because it will throw if the endpoint is initializing
   * or closing.
   *
   * Instead we use a "friend" method of agent to retrieve any existing behavior or create a new one.
   */
  #lifecycleInstance(agent) {
    if (agent.isLoaded(this.#type)) {
      return agent.get(this.#type);
    }
    const behavior = this.createBehavior(agent, this.#type);
    return behavior;
  }
  /**
   * We provide two forms of optimized change tracking.
   *
   * {@link ProtocolService} offers a low-level service that supports the Matter protocol.
   *
   * {@link ChangeNotificationService} is a higher-level service that supports state synchronization use cases.
   *
   * This method informs these services of changes.
   */
  broadcastChanges(props) {
    this.#protocol.handleChange(this, props);
    this.#changeTracking.broadcastUpdate(this, props);
  }
}
export {
  BehaviorBacking
};
//# sourceMappingURL=BehaviorBacking.js.map
