import { ActionRequest, ActionId, ActionResponse } from "./protocol.js";
import { JSONValue, convexToJson, jsonToConvex } from "../../values/index.js";
import { createError, logToConsole } from "../logging.js";

type ActionStatus = {
  message: ActionRequest;
  onResult: (result: any) => void;
  onFailure: (reason: any) => void;
};

export class ActionManager {
  private inflightActions: Map<ActionId, ActionStatus>;

  constructor() {
    this.inflightActions = new Map();
  }

  request(
    udfPath: string,
    args: any[],
    actionId: ActionId
  ): {
    message: ActionRequest;
    result: Promise<any>;
  } {
    const message: ActionRequest = {
      type: "Action",
      actionId,
      udfPath,
      args: <JSONValue[]>convexToJson(args),
    };

    const result = new Promise((resolve, reject) => {
      this.inflightActions.set(actionId, {
        message,
        onResult: resolve,
        onFailure: reject,
      });
    });

    return { message, result };
  }

  /**
   * Update the state after receiving a action response.
   */
  onResponse(response: ActionResponse) {
    const actionInfo = this.inflightActions.get(response.actionId);
    if (actionInfo === undefined) {
      // Got a response of a message that we don't know about. That shouldn't
      // really happen unless we get duplicate messages or something.
      return;
    }

    this.inflightActions.delete(response.actionId);
    const udfPath = actionInfo.message.udfPath;
    for (const line of response.logLines) {
      logToConsole("info", "action", udfPath, line);
    }
    if (response.success) {
      actionInfo.onResult(jsonToConvex(response.result));
    } else {
      logToConsole("error", "action", udfPath, response.result);
      actionInfo.onFailure(createError("action", udfPath, response.result));
    }
  }

  hasInflightActions(): boolean {
    return this.inflightActions.size > 0;
  }

  restart() {
    // Unlike mutations, actions are not idempotent. When we reconnect to the
    // backend, we don't know if it is safe to resend in-flight actions, so we
    // cancel them and consider them failed.
    // TODO(presley): If we make the server remember it has started executing a
    // function, we can resend here and remove browser to backend connectivity as
    // a source of transient errors. For example, if a function has never reached
    // the server, then we can safely execute it. If a function was executed
    // successfully but response didn't reach the client. Server can return
    // success on reconnect.
    for (const [actionId, actionInfo] of this.inflightActions) {
      this.inflightActions.delete(actionId);
      const udfPath = actionInfo.message.udfPath;
      actionInfo.onFailure(createError("action", udfPath, "Transient error"));
    }
  }
}
