import { SystemConfig } from '@app/infra/db/entities';
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { ClientMetadata, custom, generators, Issuer, UserinfoResponse } from 'openid-client';
import { ISystemConfigRepository } from '../system-config';
import { SystemConfigCore } from '../system-config/system-config.core';
import { OAuthConfigDto } from './dto';
import { MOBILE_REDIRECT } from './oauth.constants';
import { OAuthConfigResponseDto } from './response-dto';

type OAuthProfile = UserinfoResponse & {
  email: string;
};

@Injectable()
export class OAuthCore {
  private readonly logger = new Logger(OAuthCore.name);
  private configCore: SystemConfigCore;

  constructor(configRepository: ISystemConfigRepository, private config: SystemConfig) {
    this.configCore = new SystemConfigCore(configRepository);

    custom.setHttpOptionsDefaults({
      timeout: 30000,
    });

    this.configCore.config$.subscribe((config) => (this.config = config));
  }

  async generateConfig(dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
    const response = {
      enabled: this.config.oauth.enabled,
      passwordLoginEnabled: this.config.passwordLogin.enabled,
    };

    if (!response.enabled) {
      return response;
    }

    const { scope, buttonText, autoLaunch } = this.config.oauth;
    const url = (await this.getClient()).authorizationUrl({
      redirect_uri: this.normalize(dto.redirectUri),
      scope,
      state: generators.state(),
    });

    return { ...response, buttonText, url, autoLaunch };
  }

  async callback(url: string): Promise<OAuthProfile> {
    const redirectUri = this.normalize(url.split('?')[0]);
    const client = await this.getClient();
    const params = client.callbackParams(url);
    const tokens = await client.callback(redirectUri, params, { state: params.state });
    return await client.userinfo<OAuthProfile>(tokens.access_token || '');
  }

  isAutoRegisterEnabled() {
    return this.config.oauth.autoRegister;
  }

  asUser(profile: OAuthProfile) {
    return {
      firstName: profile.given_name || '',
      lastName: profile.family_name || '',
      email: profile.email,
      oauthId: profile.sub,
    };
  }

  async getLogoutEndpoint(): Promise<string | null> {
    if (!this.config.oauth.enabled) {
      return null;
    }
    return (await this.getClient()).issuer.metadata.end_session_endpoint || null;
  }

  private async getClient() {
    const { enabled, clientId, clientSecret, issuerUrl } = this.config.oauth;

    if (!enabled) {
      throw new BadRequestException('OAuth2 is not enabled');
    }

    const metadata: ClientMetadata = {
      client_id: clientId,
      client_secret: clientSecret,
      response_types: ['code'],
    };

    const issuer = await Issuer.discover(issuerUrl);
    const algorithms = (issuer.id_token_signing_alg_values_supported || []) as string[];
    if (algorithms[0] === 'HS256') {
      metadata.id_token_signed_response_alg = algorithms[0];
    }

    return new issuer.Client(metadata);
  }

  private normalize(redirectUri: string) {
    const isMobile = redirectUri === MOBILE_REDIRECT;
    const { mobileRedirectUri, mobileOverrideEnabled } = this.config.oauth;
    if (isMobile && mobileOverrideEnabled && mobileRedirectUri) {
      return mobileRedirectUri;
    }
    return redirectUri;
  }
}
