import { HttpService } from '@nestjs/axios';
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { SettingService } from '../../setting/setting.service';
import { BaseOAuthProvider } from './abstract.provider';

@Injectable()
export class MicrosoftEntraIdProvider extends BaseOAuthProvider {
  private readonly logger = new Logger(MicrosoftEntraIdProvider.name);

  constructor(
    http: HttpService,
    @Inject(forwardRef(() => SettingService))
    private readonly setting: SettingService,
  ) {
    super(http);
  }

  private normalizeBaseUrl(url: string) {
    return String(url).trim().replace(/\/+$/, '');
  }

  private buildRedirectUri(baseUrl: string, callbackPath: string) {
    const normalizedBaseUrl = this.normalizeBaseUrl(baseUrl);
    const normalizedCallbackPath = callbackPath.startsWith('/')
      ? callbackPath
      : `/${callbackPath}`;

    return new URL(normalizedCallbackPath, `${normalizedBaseUrl}/`).toString();
  }

  getProviderType() {
    return 'MICROSOFT-ENTRA-ID';
  }

  async getAuthUrl(callbackPath: string) {
    const settings = await this.setting.getSettingValues([
      'microsoft_entra_id_client_id',
      'microsoft_entra_id_client_secret',
      'microsoft_entra_id_scopes',
      'microsoft_entra_id_tenant_id',
      'url',
    ]);

    if (
      !settings['microsoft_entra_id_client_id'] ||
      !settings['microsoft_entra_id_client_secret'] ||
      !settings['microsoft_entra_id_scopes'] ||
      !settings['microsoft_entra_id_tenant_id'] ||
      !settings['url']
    ) {
      this.logger.error(
        'Microsoft Entra ID OAuth is missing required settings. Verify client id, client secret, scopes, tenant id, and url.',
      );
      throw new Error('Microsoft Entra ID OAuth is not configured properly. Please configure microsoft_entra_id_client_id, microsoft_entra_id_client_secret, microsoft_entra_id_scopes, microsoft_entra_id_tenant_id, and url settings.');
    }

    const tenantId = settings['microsoft_entra_id_tenant_id'];
    const redirectURI = this.buildRedirectUri(settings['url'], callbackPath);
    const baseScopes = Array.isArray(settings['microsoft_entra_id_scopes'])
      ? settings['microsoft_entra_id_scopes']
      : [];
    const scopes = baseScopes.includes('offline_access')
      ? baseScopes
      : [...baseScopes, 'offline_access'];
    const isConnectFlow = callbackPath.includes('/connect');
    const params = new URLSearchParams({
      client_id: settings['microsoft_entra_id_client_id'],
      redirect_uri: redirectURI,
      response_type: 'code',
      scope: scopes.join(' '),
      response_mode: 'query',
    });

    this.logger.log(
      `OAuth auth URL requested: provider=MICROSOFT-ENTRA-ID redirectUri=${redirectURI} hasOfflineAccess=${scopes.includes('offline_access') ? 'yes' : 'no'} connectFlow=${isConnectFlow ? 'yes' : 'no'} tenantConfigured=${tenantId ? 'yes' : 'no'}`,
    );

    // Force consent only for explicit account connect, not for routine login.
    if (isConnectFlow) {
      params.set('prompt', 'consent');
    }

    const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params.toString()}`;
    return authUrl;
  }

  async getProfile(code: string, type: string): Promise<any> {
    let redirectUri = '';

    try {
      const settings = await this.setting.getSettingValues([
        'microsoft_entra_id_client_id',
        'microsoft_entra_id_client_secret',
        'microsoft_entra_id_scopes',
        'microsoft_entra_id_tenant_id',
        'url',
      ]);
      const tenantId = settings['microsoft_entra_id_tenant_id'];
      const scopes = settings['microsoft_entra_id_scopes'] ?? [];
      redirectUri = this.buildRedirectUri(
        settings['url'],
        `/callback/microsoft-entra-id/${type}`,
      );

      this.logger.log(
        `OAuth token exchange started: provider=MICROSOFT-ENTRA-ID type=${type} redirectUri=${redirectUri} hasCode=${code ? 'yes' : 'no'} codeLength=${code?.length ?? 0}`,
      );

      const token = await this.fetchToken({
        code,
        url: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
        clientId: settings['microsoft_entra_id_client_id'],
        clientSecret: settings['microsoft_entra_id_client_secret'],
        redirectUri,
      });

      const profile = await this.fetchProfile(
        token.access_token,
        'https://graph.microsoft.com/v1.0/me',
      );

      this.logger.log(
        `OAuth profile fetched: provider=MICROSOFT-ENTRA-ID type=${type} profileIdPresent=${profile?.id ? 'yes' : 'no'} emailPresent=${profile?.mail || profile?.userPrincipalName ? 'yes' : 'no'}`,
      );

      const pictureUrl = 'https://graph.microsoft.com/v1.0/me/photo/$value';
      return {
        id: profile.id,
        email: profile.mail || profile.userPrincipalName,
        name: profile.displayName,
        picture: pictureUrl,
        oauth_scopes: scopes.join(','),
        oauth_tokens: {
          access_token: token.access_token,
          refresh_token: token.refresh_token,
          expires_in: token.expires_in,
          token_type: token.token_type,
        },
      };
    } catch (error: any) {
      const mappedError = this.mapProviderError('Microsoft Entra ID', error);

      this.logger.warn(
        `OAuth callback failed: provider=MICROSOFT-ENTRA-ID status=${mappedError.status ?? 'unknown'} error=${mappedError.providerError ?? 'unknown'} providerCode=${mappedError.providerErrorCode ?? 'n/a'} reason=${mappedError.reason} redirectUri=${redirectUri || 'unresolved'}`,
      );

      throw mappedError;
    }
  }
}
