import { IdpRequests } from './classes.idprequests.js';
import * as plugins from './plugins.js';

export class IdpClient {
  // INSTANCE PRIVATE
  private helpers = {
    async extractDataFromJwtString(jwtString: string): Promise<plugins.idpInterfaces.data.IJwt> {
      return plugins.webjwt.getDataFromJwtString(jwtString);
    },
  };

  // INSTANCE PUBLIC

  public appData: plugins.idpInterfaces.data.IApp;
  public rolesReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1);
  public organizationsReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1);

  public parsedReceptionUrl: plugins.smarturl.Smarturl;
  constructor(receptionBaseUrlArg: string, appDataArg?: plugins.idpInterfaces.data.IApp) {
    if (receptionBaseUrlArg.endsWith('/')) {
      receptionBaseUrlArg = receptionBaseUrlArg.slice(0, -1);
    }
    if (!receptionBaseUrlArg.endsWith('/typedrequest')) {
      receptionBaseUrlArg = `${receptionBaseUrlArg}/typedrequest`;
    }
    this.parsedReceptionUrl = plugins.smarturl.Smarturl.createFromUrl(receptionBaseUrlArg);
    console.log(`reception client connecting to ${this.parsedReceptionUrl.toString()}`);
    if (!appDataArg) {
      appDataArg = {
        id: '', // TODO
        appUrl: `https://${window.location.host}/`,
        description: null,
        logoUrl: null,
        name: null,
      };
    }
    this.appData = appDataArg;
  }

  public requests = new IdpRequests(this);

  public checkWetherOnReceptionDomain() {
    return plugins.smarturl.Smarturl.createFromUrl(window.location.href).hostname ===
      this.parsedReceptionUrl.hostname;
  }

  /**
   * app data can be transferred when redirecting to the sso domain using query params
   * this message retrieves the app data when on the sso domain
   */
  public async getAppDataOnSsoDomain() {
    if (!window.location.href.startsWith('https://sso.workspace.global/')) {
      console.error('You are trying to access SSO appData on a non sso domain.');
      return null;
    }
    const appDataString = plugins.smarturl.Smarturl.createFromUrl(window.location.href).searchParams
      .appdata;
    if (!appDataString) {
      console.error('no appdata query arg detected');
      return null;
    }
    const appData = plugins.smartjson.parseBase64(appDataString);
    return appData;
  }

  public async setJwt(jwtStringArg: string) {
    await this.storeJwt(jwtStringArg);
  }

  /**
   * a typedsocket for going reactive
   */
  public typedsocket: plugins.typedsocket.TypedSocket;

  /**
   * a typed router to go reactive
   */
  public typedrouter = new plugins.typedrequest.TypedRouter();

  public statusObservable =
    new plugins.smartrx.rxjs.Subject<plugins.idpInterfaces.data.TLoginStatus>();

  public ssoStore = new plugins.webstore.WebStore({
    storeName: 'idpglobalStore',
    dbName: 'main',
  });

  public async storeJwt(jwtString: string) {
    await this.ssoStore.set('idpJwt', jwtString);
  }

  public async getJwt(): Promise<string> {
    return await this.ssoStore.get('idpJwt');
  }
  public async getJwtData(): Promise<plugins.idpInterfaces.data.IJwt> {
    return this.helpers.extractDataFromJwtString(await this.getJwt());
  }

  public async deleteJwt() {
    await this.ssoStore.delete('idpJwt');
    console.log('removed jwt');
  }

  /**
   * performs jwt housekeeping
   * only call if jwt is present
   * @returns
   */
  public async performJwtHousekeeping() {
    let jwt = await this.getJwt();
    if (!jwt) {
      return null;
    }
    const extractedJwt = await this.helpers.extractDataFromJwtString(jwt);
    if (extractedJwt.data.refreshFrom < Date.now() && Date.now() < extractedJwt.data.validUntil) {
      jwt = await this.refreshJwt();
    } else if (Date.now() > extractedJwt.data.validUntil) {
      this.deleteJwt();
    }
    return jwt;
  }

  public async refreshJwt(refreshTokenArg?: string): Promise<string> {
    let extractedJwt: plugins.idpInterfaces.data.IJwt;

    if (!refreshTokenArg) {
      extractedJwt = await this.helpers.extractDataFromJwtString(await this.getJwt());
    }
    const refreshJwtReq =
      new plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
        this.parsedReceptionUrl.toString(),
        'refreshJwt'
      );
    const response = await refreshJwtReq.fire({
      refreshToken: refreshTokenArg || extractedJwt.data.refreshToken,
    });
    if (response.jwt) {
      await this.storeJwt(response.jwt);
    } else {
      await this.deleteJwt();
    }
    this.statusObservable.next(response.status);
    return await this.getJwt();
  }

  /**
   * can be used to switch between pages
   */
  public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IApp): Promise<string> {
    const jwt = await this.performJwtHousekeeping();
    const extractedJwt = await this.helpers.extractDataFromJwtString(jwt);
    const getTransferToken =
      new plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>(
        this.parsedReceptionUrl.toString(),
        'exchangeRefreshTokenAndTransferToken'
      );
    const response = await getTransferToken.fire({
      refreshToken: extractedJwt.data.refreshToken,
      appData: appDataArg || this.appData,
    });
    return response.transferToken;
  }

  /**
   * gets a transfer token and switches to a location
   */
  public async getTransferTokenAndSwitchToLocation(newLocationArg: string): Promise<void> {
    const transferToken = await this.getTransferToken();
    if (!transferToken) {
      alert('failed to get transfer token!');
    }
    const urlInstance = plugins.smarturl.Smarturl.createFromUrl(newLocationArg, {
      searchParams: {
        transfertoken: transferToken,
      },
    });
    const transferUrl = urlInstance.toString();
    window.location.href = transferUrl;
    return;
  }

  /**
   * processes a transfer token
   */
  public async processTransferToken(): Promise<boolean> {
    const href = window.location.href;
    const url = plugins.smarturl.Smarturl.createFromUrl(href);
    const transferToken = url.searchParams['transfertoken'];
    if (transferToken) {
      const getTransferToken =
        new plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>(
          this.parsedReceptionUrl.toString(),
          'exchangeRefreshTokenAndTransferToken'
        );
      const response = await getTransferToken.fire({
        transferToken,
        appData: this.appData,
      });
      if (response.refreshToken) {
        await this.refreshJwt(response.refreshToken);
      } else {
        globalThis.alert?.('transfer token invalid');
        return false;
      }
      return true;
    } else {
      return false;
    }
  }

  // Login Status stuff
  public async checkJwtPresent() {
    const jwt = await this.performJwtHousekeeping();
    if (jwt) {
      return true;
    } else {
      return false;
    }
  }

  /**
   * determines if the user is logged in
   * accepts boolean to optionally require login
   * @param requireLoginArg
   * @returns
   */
  public async determineLoginStatus(requireLoginArg: boolean = false): Promise<boolean> {
    const jwtPresent = await this.checkJwtPresent();
    if (jwtPresent) {
      const jwt = await this.performJwtHousekeeping();
      return !!jwt;
    } else {
      const transferTokenResult = await this.processTransferToken();
      if (transferTokenResult) {
        // we are in the clear
        return true;
      } else {
        if (requireLoginArg) {
          const urlInstance = plugins.smarturl.Smarturl.createFromUrl(
            this.parsedReceptionUrl.clone().set('path', '/login').toString(),
            {
              searchParams: {
                appdata: plugins.smartjson.stringifyBase64(this.appData),
              },
            }
          );
          if (!globalThis.location.href.startsWith(this.parsedReceptionUrl.toString())) {
            globalThis.location.href = urlInstance.toString();
          }
        }
        return false;
      }
    }
  }

  /**
   * logs out the current user
   */
  public async logout() {
    const idpLogoutUrl = this.parsedReceptionUrl.clone().set('path', '/logout');
    if (!globalThis.location.href.startsWith(idpLogoutUrl.origin)) {
      // we are somewhere in an app
      await this.deleteJwt();
      globalThis.location.href = idpLogoutUrl.toString();
    } else {
      // we are in the sso page
      await this.enableTypedSocket();
      console.log(`logging out against ${this.parsedReceptionUrl.toString()}`);
      const logoutTr =
        this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.ILogoutRequest>(
          'logout'
        );
      await logoutTr.fire({
        refreshToken: (await this.getJwtData()).data.refreshToken,
      });
      await this.deleteJwt();
      const appData = await this.getAppDataOnSsoDomain();
      if (appData) {
        console.log(`redirecting to app after logout: ${appData.appUrl}`);
        window.location.href = appData.appUrl;
      } else {
        console.error('no appData provided. Not redirecting after logout.');
      }
      if (window.location.href.startsWith(idpLogoutUrl.origin)) {
        window.location.href = this.parsedReceptionUrl.origin;
      }
    }
  }

  public typedsocketDeferred = plugins.smartpromise.defer();
  public async enableTypedSocket() {
    if (this.typedsocketDeferred.claimed) {
      return this.typedsocketDeferred.promise;
    }
    this.typedsocketDeferred.claim();
    this.typedsocket = await plugins.typedsocket.TypedSocket.createClient(
      this.typedrouter,
      this.parsedReceptionUrl.toString()
    );
    this.typedsocketDeferred.resolve(this.typedsocket);
    return this.typedsocketDeferred.promise;
  }

  public async stop() {
    await this.typedsocket?.stop();
  }

  // ==================================
  // Organization and Settings stuff
  // ==================================
  public async createOrganization(
    orgNameArg: string,
    orgSlugArg: string,
    modeArg: 'checkAvailability' | 'manifest'
  ) {
    await this.typedsocketDeferred.promise;
    const validateOrg =
      this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreateOrganization>(
        'createOrganization'
      );
    const response = await validateOrg.fire({
      jwt: await this.getJwt(),
      action: modeArg,
      organizationName: orgNameArg,
      organizationSlug: orgSlugArg,
      userId: (await this.getJwtData()).id,
    });
    return response;
  }

  /**
   * gets the current OrganizationRoles
   */
  public async getRolesAndOrganizations() {
    console.log('idpclient: getting roles and orgs...');
    await this.typedsocketDeferred.promise;
    const rolesAndOrganizationsForUserId =
      this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetRolesAndOrganizationsForUserId>(
        'getRolesAndOrganizationsForUserId'
      );
    const response = await rolesAndOrganizationsForUserId.fire({
      jwt: await this.getJwt(),
      userId: (await this.getJwtData()).id,
    });
    return response;
  }

  /**
   * updates the PaddleCheckoutId for an organization.
   */
  public async updatePaddleCheckoutId(orgIdArg: string, checkoutIdArg: string) {
    await this.typedsocketDeferred.promise;
    const updateBillingPlan =
      this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdatePaymentMethod>(
        'updatePaymentMethod'
      );
    const response = await updateBillingPlan.fire({
      jwtString: await this.getJwt(),
      orgId: orgIdArg,
      paddle: {
        checkoutId: checkoutIdArg,
      },
    });
    return response;
  }

  public async whoIs() {
    await this.typedsocketDeferred.promise;
    const whoIs =
      this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_WhoIs>(
        'whoIs'
      );
    const response = await whoIs.fire({
      jwt: await this.getJwt(),
    });
    return response;
  }
}
