/**
 * @license
 * Copyright 2022-2024 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { camelize, Diagnostic, ImplementationError, InternalError, isObject, Logger, MaybePromise } from "#general";
import { ElementTag } from "#model";
import {
  AttributeServer,
  CommandServer,
  createAttributeServer as ConstructAttributeServer,
  EventServer,
  FabricManager,
  SecureSession
} from "#protocol";
import { TlvNoResponse } from "#types";
import { AccessControl } from "../AccessControl.js";
import { ValidatedElements } from "../cluster/ValidatedElements.js";
import { Contextual } from "../context/Contextual.js";
import { StructManager } from "../state/managed/values/StructManager.js";
import { Status } from "../state/transaction/Status.js";
import { ServerBehaviorBacking } from "./ServerBehaviorBacking.js";
const logger = Logger.get("Behavior");
class ClusterServerBacking extends ServerBehaviorBacking {
  #server;
  #clusterServer;
  get clusterServer() {
    return this.#clusterServer;
  }
  get runtime() {
    return this.#server.endpoint.env.runtime;
  }
  get server() {
    return this.#server;
  }
  get type() {
    return super.type;
  }
  constructor(server, type) {
    super(server.endpoint, type);
    this.#server = server;
  }
  invokeInitializer(behavior, options) {
    const { id, name, attributes, commands, events } = this.type.cluster;
    if (!options) {
      options = {};
    }
    const clusterServer = this.#clusterServer = {
      id,
      name,
      datasource: this.#createClusterDatasource(),
      attributes: {},
      commands: {},
      events: {}
    };
    const result = super.invokeInitializer(behavior, options);
    const createClusterServer = () => {
      behavior.state.eventList = [];
      const elements = new ValidatedElements(this.type, behavior);
      elements.report();
      this.#configureServers(
        elements.attributes,
        attributes,
        clusterServer.attributes,
        behavior,
        ["attributeList"],
        createAttributeServer
      );
      this.#configureServers(
        elements.commands,
        commands,
        clusterServer.commands,
        behavior,
        ["acceptedCommandList", "generatedCommandList"],
        createCommandServer
      );
      this.#configureServers(
        elements.events,
        events,
        clusterServer.events,
        behavior,
        ["eventList"],
        createEventServer
      );
      this.#server.addClusterServer(clusterServer);
    };
    if (MaybePromise.is(result)) {
      return result.then(createClusterServer);
    }
    createClusterServer();
  }
  get datasourceOptions() {
    return {
      ...super.datasourceOptions,
      cluster: this.type.cluster.id
    };
  }
  /**
   * Create the {@link ClusterDatasource} that adapts the Behavior API to the AttributeServer API.
   */
  #createClusterDatasource() {
    const eventHandler = this.eventHandler;
    const datasource = this.datasource;
    const env = this.endpoint.env;
    return {
      get version() {
        return datasource.version;
      },
      get eventHandler() {
        return eventHandler;
      },
      get fabrics() {
        return env.get(FabricManager).getFabrics();
      },
      // We handle change management ourselves
      changed() {
      },
      // We handle version management ourselves
      increaseVersion() {
        return datasource.version;
      }
    };
  }
  /**
   * Configure the servers for a set of elements (attributes, commands or events).
   */
  #configureServers(names, definitions, servers, behavior, attributeNames, addServer) {
    const collectedIds = Array();
    attributeNames.forEach(() => collectedIds.push(/* @__PURE__ */ new Set()));
    for (const name of names) {
      const definition = definitions[name];
      const { ids, server } = addServer(name, definition, this, behavior);
      ids.forEach((id, index) => collectedIds[index].add(id));
      servers[name] = server;
    }
    attributeNames.forEach((attributeName, index) => {
      behavior.state[attributeName] = [...collectedIds[index].values()];
    });
  }
}
function createAttributeServer(name, definition, backing, behavior) {
  function getter(_session, _endpoint, _isFabricFiltered, message) {
    if (!message) {
      return backing.datasource.view[name];
    }
    const behavior2 = behaviorFor(backing, message);
    behavior2.context.activity?.frame(`read ${name}`);
    const trace = behavior2.context.trace;
    if (trace) {
      trace.path = backing.path.at(name);
    }
    logger.debug("Read", Diagnostic.strong(`${backing}.state.${name}`), "via", behavior2.context.transaction.via);
    const state = behavior2.state;
    StructManager.assertDirectReadAuthorized(state, name);
    if (trace) {
      trace.output = state[name];
    }
    return state[name];
  }
  function setter(value, _session, _endpoint, message) {
    const behavior2 = behaviorFor(backing, message);
    behavior2.context.activity?.frame(`write ${name}`);
    logger.info("Write", Diagnostic.strong(`${backing}.state.${name}`), "via", behavior2.context.transaction.via);
    const trace = behavior2.context.trace;
    if (trace) {
      trace.path = backing.path.at(name);
      trace.input = value;
    }
    const state = behavior2.state;
    state[name] = value;
    return behavior2.context.transaction?.status === Status.Exclusive;
  }
  const server = ConstructAttributeServer(
    backing.type.cluster,
    definition,
    name,
    behavior.state[name],
    backing.clusterServer.datasource,
    getter,
    setter
  );
  if (server instanceof AttributeServer) {
    const observable = backing.events[`${name}$Changed`];
    observable?.on((_value, _oldValue, context) => {
      const session = context.session;
      if (session instanceof SecureSession) {
        server.updated(session);
      } else {
        server.updatedLocal();
      }
    });
  }
  server.assignToEndpoint(backing.server);
  return {
    ids: [definition.id],
    server
  };
}
function createCommandServer(name, definition, backing) {
  const schema = backing.type.schema?.member(camelize(name, true), [ElementTag.Command]);
  if (schema === void 0) {
    throw new ImplementationError(`There is no metadata for command ${name}`);
  }
  const access = AccessControl(schema);
  const handler = (request, _session, message) => {
    let requestDiagnostic;
    if (isObject(request)) {
      requestDiagnostic = Diagnostic.dict(request);
    } else if (request !== void 0) {
      requestDiagnostic = request;
    } else {
      requestDiagnostic = Diagnostic.weak("(no payload)");
    }
    const behavior = behaviorFor(backing, message);
    const path = backing.path.at(name);
    const trace = behavior.context.trace;
    if (trace) {
      trace.path = backing.path.at(name);
      trace.input = request;
    }
    logger.info("Invoke", Diagnostic.strong(path.toString()), behavior.context.transaction.via, requestDiagnostic);
    access.authorizeInvoke(behavior.context, {
      path,
      cluster: behavior.cluster.id
    });
    let isAsync = false;
    let activity;
    let result;
    try {
      activity = behavior.context?.activity?.frame(`invoke ${name}`);
      result = behavior[name](request);
      if (MaybePromise.is(result)) {
        isAsync = true;
        result = Promise.resolve(result).then((result2) => {
          if (trace) {
            trace.output = result2;
          }
          return result2;
        }).finally(() => activity?.[Symbol.dispose]());
      } else if (trace) {
        trace.output = result;
      }
    } finally {
      if (!isAsync) {
        activity?.[Symbol.dispose]();
      }
    }
    return result;
  };
  const server = new CommandServer(
    definition.requestId,
    definition.responseId,
    name,
    definition.requestSchema,
    definition.responseSchema,
    definition.timed,
    definition.invokeAcl,
    handler
  );
  server.debug = () => {
  };
  const ids = [definition.requestId];
  if (definition.responseSchema !== TlvNoResponse) {
    ids.push(definition.responseId);
  }
  return {
    ids,
    server
  };
}
function createEventServer(name, definition, backing) {
  const observable = backing.events[name];
  const server = new EventServer(
    definition.id,
    backing.type.cluster.id,
    name,
    definition.schema,
    definition.priority,
    definition.readAcl
  );
  observable?.on((payload, _context) => {
    const maybePromise = server.triggerEvent(payload);
    if (MaybePromise.is(maybePromise)) {
      backing.runtime.add(maybePromise);
    }
  });
  server.assignToEndpoint(backing.server);
  const promise = server.bindToEventHandler(backing.eventHandler);
  if (MaybePromise.is(promise)) {
    throw new InternalError("Event handler binding returned a promise");
  }
  return {
    ids: [definition.id],
    server
  };
}
function behaviorFor(backing, message) {
  const context = Contextual.contextOf(message);
  if (!context) {
    throw new InternalError("Message context not installed");
  }
  const agent = context.agentFor(backing.endpoint);
  return agent.get(backing.type);
}
export {
  ClusterServerBacking
};
//# sourceMappingURL=ClusterServerBacking.js.map
