import { l as logger, a as safeStringify, a9 as CREDENTIAL_SYNC_ENDPOINT, p as prisma } from "./orgDomains-eTg-cjth.js";
import { O as OAuth2UniversalSchemaWithCalcomBackwardCompatibility, a as OAuth2TokenResponseInDbSchema } from "./universalSchema-lXT1PUH8.js";
const log = logger.getSubLogger({ prefix: ["app-store/_utils/oauth/OAuthManager"] });
class OAuthManager {
  constructor({
    getCurrentTokenObject,
    resourceOwner,
    appSlug,
    currentTokenObject,
    fetchNewTokenObject,
    updateTokenObject,
    /**
     * The fn must not crash. It is the responsibility of the caller to handle any error and appropriately decide what to return
     */
    isTokenObjectUnusable,
    /**
     * The fn must not crash. It is the responsibility of the caller to handle any error and appropriately decide what to return
     */
    isAccessTokenUnusable,
    invalidateTokenObject,
    expireAccessToken,
    credentialSyncVariables,
    autoCheckTokenExpiryOnRequest = true,
    isTokenExpiring = (token) => {
      const expireThreshold = 5e3;
      const isGoingToExpire = getExpiryDate() - expireThreshold <= Date.now();
      log.debug(
        "isTokenExpiring",
        safeStringify({
          isGoingToExpire,
          expiry_date: token.expiry_date,
          expires_in: token.expires_in,
          currentTime: Date.now(),
          expireThreshold
        })
      );
      return isGoingToExpire;
      function isRelativeToEpoch(relativeTimeInSeconds) {
        return relativeTimeInSeconds > 1e9;
      }
      function getExpiryDate() {
        if (token.expiry_date) {
          return token.expiry_date;
        }
        if (token.expires_in && isRelativeToEpoch(token.expires_in)) {
          return token.expires_in * 1e3;
        }
        return 0;
      }
    }
  }) {
    if (!getCurrentTokenObject && !currentTokenObject) {
      throw new Error("One of getCurrentTokenObject or currentTokenObject is required");
    }
    this.resourceOwner = resourceOwner;
    this.currentTokenObject = currentTokenObject ?? null;
    this.getCurrentTokenObject = getCurrentTokenObject ?? null;
    this.appSlug = appSlug;
    this.fetchNewTokenObject = fetchNewTokenObject;
    this.isTokenObjectUnusable = isTokenObjectUnusable;
    this.isAccessTokenUnusable = isAccessTokenUnusable;
    this.isTokenExpiring = isTokenExpiring;
    this.invalidateTokenObject = invalidateTokenObject;
    this.expireAccessToken = expireAccessToken;
    this.credentialSyncVariables = credentialSyncVariables;
    this.useCredentialSync = !!(credentialSyncVariables.APP_CREDENTIAL_SHARING_ENABLED && credentialSyncVariables.CREDENTIAL_SYNC_ENDPOINT && credentialSyncVariables.CREDENTIAL_SYNC_SECRET_HEADER_NAME && credentialSyncVariables.CREDENTIAL_SYNC_SECRET);
    if (this.useCredentialSync) {
      ensureValidResourceOwner(resourceOwner);
    }
    this.autoCheckTokenExpiryOnRequest = autoCheckTokenExpiryOnRequest;
    this.updateTokenObject = updateTokenObject;
  }
  isResponseNotOkay(response) {
    return !response.ok || response.status < 200 || response.status >= 300;
  }
  /**
   * Gets the current token object as is if not expired.
   * If expired, it refreshes the token and returns the new token object.
   * It also calls the `updateTokenObject` to update the token object in the database if it is changed.
   */
  async getTokenObjectOrFetch() {
    const myLog = log.getSubLogger({
      prefix: [`getTokenObjectOrFetch:appSlug=${this.appSlug}`]
    });
    let currentTokenObject;
    if (this.currentTokenObject) {
      currentTokenObject = this.currentTokenObject;
    } else if (this.getCurrentTokenObject) {
      this.currentTokenObject = currentTokenObject = await this.getCurrentTokenObject();
    } else {
      throw new Error("Neither currentTokenObject nor getCurrentTokenObject is set");
    }
    const isExpired = await this.isTokenExpiring(currentTokenObject);
    myLog.debug(
      "getTokenObjectOrFetch called",
      safeStringify({
        currentTokenObjectHasAccessToken: !!currentTokenObject.access_token,
        isExpired,
        resourceOwner: this.resourceOwner
      })
    );
    if (!isExpired) {
      myLog.debug("Token is not expired. Returning the current token object");
      return { token: this.normalizeNewlyReceivedToken(currentTokenObject), isUpdated: false };
    } else {
      const token = {
        // Keep the old token object as it is, as some integrations don't send back all the props e.g. refresh_token isn't sent again by Google Calendar
        // It also allows any other properties set to be retained.
        // Let's not use normalizedCurrentTokenObject here as `normalizeToken` could possible be not idempotent
        ...currentTokenObject,
        ...this.normalizeNewlyReceivedToken(
          await this.refreshOAuthToken({ refreshToken: currentTokenObject.refresh_token ?? null })
        )
      };
      myLog.debug("Token is expired. So, returning new token object");
      this.currentTokenObject = token;
      await this.updateTokenObject(token);
      return { token, isUpdated: true };
    }
  }
  /**
   * Send request automatically adding the Authorization header with the access token. More importantly, handles token invalidation
   */
  async request(customFetchOrUrlAndOptions) {
    let response;
    const myLog = log.getSubLogger({ prefix: ["request"] });
    if (this.autoCheckTokenExpiryOnRequest) {
      await this.getTokenObjectOrFetch();
    }
    if (typeof customFetchOrUrlAndOptions === "function") {
      myLog.debug("Sending request using customFetch");
      const customFetch = customFetchOrUrlAndOptions;
      try {
        response = await customFetch();
      } catch (e) {
        response = handleFetchError(e);
      }
    } else {
      this.assertCurrentTokenObjectIsSet();
      const { url, options } = customFetchOrUrlAndOptions;
      const headers = {
        Authorization: `Bearer ${this.currentTokenObject.access_token}`,
        "Content-Type": "application/json",
        ...options?.headers
      };
      myLog.debug("Sending request using fetch", safeStringify({ customFetchOrUrlAndOptions, headers }));
      response = await fetch(url, {
        method: "GET",
        ...options,
        headers
      });
    }
    myLog.debug(
      "Response from request",
      safeStringify({
        text: await response.clone().text(),
        status: response.status,
        statusText: response.statusText
      })
    );
    const { tokenStatus, json } = await this.getAndValidateOAuth2Response({
      response
    });
    if (tokenStatus === 0) {
      await this.invalidate();
    } else if (tokenStatus === 1) {
      await this.expireAccessToken();
    } else if (tokenStatus === 2) {
      await this.onInconclusiveResponse();
    }
    if ("myFetchError" in (json || {})) {
      throw new Error(json.myFetchError);
    }
    return { tokenStatus, json };
  }
  /**
   * currentTokenObject is set through getTokenObjectOrFetch call
   */
  assertCurrentTokenObjectIsSet() {
    if (!this.currentTokenObject) {
      throw new Error("currentTokenObject is not set");
    }
  }
  /**
   * It doesn't automatically detect the response for tokenObject and accessToken becoming invalid
   * Could be used when you expect a possible non JSON response as well.
   */
  async requestRaw({ url, options }) {
    const myLog = log.getSubLogger({ prefix: ["requestRaw"] });
    myLog.debug("Sending request using fetch", safeStringify({ url, options }));
    if (this.autoCheckTokenExpiryOnRequest) {
      await this.getTokenObjectOrFetch();
    }
    this.assertCurrentTokenObjectIsSet();
    const headers = {
      Authorization: `Bearer ${this.currentTokenObject.access_token}`,
      "Content-Type": "application/json",
      ...options?.headers
    };
    const response = await fetch(url, {
      method: "GET",
      ...options,
      headers
    });
    myLog.debug(
      "Response from request",
      safeStringify({
        text: await response.clone().text(),
        status: response.status,
        statusText: response.statusText
      })
    );
    if (this.isResponseNotOkay(response)) {
      await this.onInconclusiveResponse();
    }
    return response;
  }
  async onInconclusiveResponse() {
    const myLog = log.getSubLogger({ prefix: ["onInconclusiveResponse"] });
    myLog.debug("Expiring the access token");
    if (this.useCredentialSync) {
      await this.expireAccessToken();
    }
  }
  async invalidate() {
    const myLog = log.getSubLogger({ prefix: ["invalidate"] });
    if (this.useCredentialSync) {
      myLog.debug("Expiring the access token");
      await this.expireAccessToken();
    } else {
      myLog.debug("Invalidating the token object");
      await this.invalidateTokenObject();
    }
  }
  normalizeNewlyReceivedToken(token) {
    if (!token.expiry_date && !token.expires_in) {
      token.expiry_date = this.useCredentialSync ? Date.now() + 365 * 24 * 3600 * 1e3 : 0;
    } else if (token.expires_in !== void 0 && token.expiry_date === void 0) {
      token.expiry_date = Math.round(Date.now() + token.expires_in * 1e3);
      delete token.expires_in;
    }
    return token;
  }
  // TODO: On regenerating access_token successfully, we should call makeTokenObjectValid(to counter invalidateTokenObject). This should fix stale banner in UI to reconnect when the connection is working
  async refreshOAuthToken({ refreshToken }) {
    const myLog = log.getSubLogger({ prefix: ["refreshOAuthToken"] });
    let response;
    if (this.resourceOwner.id && this.useCredentialSync) {
      if (!this.credentialSyncVariables.CREDENTIAL_SYNC_SECRET || !this.credentialSyncVariables.CREDENTIAL_SYNC_SECRET_HEADER_NAME || !this.credentialSyncVariables.CREDENTIAL_SYNC_ENDPOINT) {
        throw new Error("Credential syncing is enabled but the required env variables are not set");
      }
      myLog.info(
        "Refreshing OAuth token from credential sync endpoint",
        safeStringify({
          appSlug: this.appSlug,
          resourceOwner: this.resourceOwner,
          endpoint: CREDENTIAL_SYNC_ENDPOINT
        })
      );
      try {
        response = await fetch(`${this.credentialSyncVariables.CREDENTIAL_SYNC_ENDPOINT}`, {
          method: "POST",
          headers: {
            [this.credentialSyncVariables.CREDENTIAL_SYNC_SECRET_HEADER_NAME]: this.credentialSyncVariables.CREDENTIAL_SYNC_SECRET
          },
          body: new URLSearchParams({
            calcomUserId: this.resourceOwner.id.toString(),
            appSlug: this.appSlug
          })
        });
      } catch (e) {
        myLog.error("Could not refresh the token.", safeStringify(e));
        throw new Error(
          `Could not refresh the token due to connection issue with the endpoint: ${CREDENTIAL_SYNC_ENDPOINT}`
        );
      }
    } else {
      myLog.info(
        "Refreshing OAuth token",
        safeStringify({
          appSlug: this.appSlug,
          resourceOwner: this.resourceOwner
        })
      );
      try {
        response = await this.fetchNewTokenObject({ refreshToken });
      } catch (e) {
        response = handleFetchError(e);
      }
      if (!response) {
        throw new Error("`fetchNewTokenObject` could not refresh the token");
      }
    }
    const clonedResponse = response.clone();
    myLog.info(
      "Response status from refreshOAuthToken",
      safeStringify({
        ok: clonedResponse.ok,
        status: clonedResponse.status,
        statusText: clonedResponse.statusText
      })
    );
    const { json, tokenStatus } = await this.getAndValidateOAuth2Response({
      response
    });
    if (tokenStatus === 0) {
      await this.invalidateTokenObject();
    } else if (tokenStatus === 1) {
      await this.expireAccessToken();
    }
    if (json && json.myFetchError) {
      throw new Error(json.myFetchError);
    }
    const parsedToken = OAuth2UniversalSchemaWithCalcomBackwardCompatibility.safeParse(json);
    if (!parsedToken.success) {
      myLog.error(
        "Token parsing error:",
        safeStringify({ issues: parsedToken.error.issues, oauth2response: json, tokenStatus })
      );
      throw new Error("Invalid token response");
    }
    return parsedToken.data;
  }
  async getAndValidateOAuth2Response({ response }) {
    const myLog = log.getSubLogger({ prefix: ["getAndValidateOAuth2Response"] });
    const clonedResponse = response.clone();
    if ((await clonedResponse.text()).trim() === "") {
      return { tokenStatus: 3, json: null, invalidReason: null };
    }
    const tokenObjectUsabilityRes = await this.isTokenObjectUnusable(response.clone());
    const accessTokenUsabilityRes = await this.isAccessTokenUnusable(response.clone());
    const isNotOkay = this.isResponseNotOkay(response);
    const json = await response.json();
    if (tokenObjectUsabilityRes?.reason) {
      myLog.error("Token Object has become unusable");
      return {
        tokenStatus: 0,
        invalidReason: tokenObjectUsabilityRes.reason,
        json
      };
    }
    if (accessTokenUsabilityRes?.reason) {
      myLog.error("Access Token has become unusable");
      return {
        tokenStatus: 1,
        invalidReason: accessTokenUsabilityRes?.reason,
        json
      };
    }
    if (isNotOkay) {
      return {
        tokenStatus: 2,
        invalidReason: response.statusText,
        json
      };
    }
    return { tokenStatus: 3, json, invalidReason: null };
  }
}
function ensureValidResourceOwner(resourceOwner) {
  if (resourceOwner.type === "team") {
    throw new Error("Teams are not supported");
  } else {
    if (!resourceOwner.id) {
      throw new Error("resourceOwner should have id set");
    }
  }
}
function handleFetchError(e) {
  const myLog = log.getSubLogger({ prefix: ["handleFetchError"] });
  myLog.debug("Error", safeStringify(e));
  if (e instanceof Error) {
    return new Response(JSON.stringify({ myFetchError: e.message }), { status: 500 });
  }
  return new Response(JSON.stringify({ myFetchError: "UNKNOWN_ERROR" }), { status: 500 });
}
function getTokenObjectFromCredential(credential) {
  const parsedTokenResponse = OAuth2TokenResponseInDbSchema.safeParse(credential.key);
  if (!parsedTokenResponse.success) {
    logger.error(
      "GoogleCalendarService-getTokenObjectFromCredential",
      safeStringify(parsedTokenResponse.error.issues)
    );
    throw new Error(
      `Could not parse credential.key ${credential.id} with error: ${parsedTokenResponse?.error}`
    );
  }
  const tokenResponse = parsedTokenResponse.data;
  if (!tokenResponse) {
    throw new Error(`credential.key is not set for credential ${credential.id}`);
  }
  return tokenResponse;
}
const invalidateCredential = async (credentialId) => {
  const credential = await prisma.credential.findUnique({
    where: {
      id: credentialId
    }
  });
  if (credential) {
    await prisma.credential.update({
      where: {
        id: credentialId
      },
      data: {
        invalid: true
      }
    });
  }
};
const markTokenAsExpired = async (credential) => {
  const tokenResponse = getTokenObjectFromCredential(credential);
  if (credential && credential.key) {
    await prisma.credential.update({
      where: {
        id: credential.id
      },
      data: {
        key: {
          ...tokenResponse,
          expiry_date: Date.now() - 3600 * 1e3
        }
      }
    });
  }
};
export {
  OAuthManager as O,
  getTokenObjectFromCredential as g,
  invalidateCredential as i,
  markTokenAsExpired as m
};
