import {
  Injectable,
  Logger,
  OnModuleInit,
  ServiceUnavailableException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
  ConfidentialClientApplication,
  type Configuration,
  type AuthorizationUrlRequest,
  type AuthorizationCodeRequest,
  type AuthenticationResult,
} from '@azure/msal-node';
import { RedisService } from '../../../redis/redis.service';

/**
 * Informationen aus dem Microsoft ID-Token.
 */
export interface MsUserInfo {
  /** Microsoft Object ID (eindeutig pro Tenant) */
  oid: string;
  /** E-Mail-Adresse */
  email: string;
  /** Vorname */
  firstName: string;
  /** Nachname */
  lastName: string;
}

/**
 * SSO-Konfiguration die in Redis gespeichert wird.
 */
export interface SsoConfig {
  tenantId: string;
  clientId: string;
  clientSecret: string;
  redirectUri: string;
}

/** Redis-Key fuer die SSO-Konfiguration (persistent, kein TTL) */
const SSO_CONFIG_KEY = 'sso_config';

/**
 * EntraIdService - Microsoft Entra ID (Azure AD) Integration.
 *
 * Nutzt MSAL ConfidentialClientApplication fuer den
 * Authorization Code Flow (Server-seitig).
 *
 * Konfiguration wird aus Redis geladen (dynamisch via Admin-UI),
 * mit Fallback auf Umgebungsvariablen.
 */
@Injectable()
export class EntraIdService implements OnModuleInit {
  private readonly logger = new Logger(EntraIdService.name);
  private msalClient: ConfidentialClientApplication | null = null;
  private redirectUri = '';
  private readonly scopes = ['openid', 'profile', 'email', 'User.Read'];

  constructor(
    private readonly config: ConfigService,
    private readonly redis: RedisService,
  ) {}

  /**
   * Beim Start: Konfiguration aus Redis laden (Fallback: Env-Vars).
   */
  async onModuleInit(): Promise<void> {
    // Versuche Konfiguration aus Redis zu laden
    const redisConfig = await this.loadConfigFromRedis();

    if (redisConfig) {
      this.initializeMsal(redisConfig);
      this.logger.log(
        'Microsoft Entra ID SSO aus Redis-Konfiguration initialisiert',
      );
      return;
    }

    // Fallback: Umgebungsvariablen
    const clientId = this.config.get<string>('AZURE_CLIENT_ID');
    const tenantId = this.config.get<string>('AZURE_TENANT_ID');
    const clientSecret = this.config.get<string>('AZURE_CLIENT_SECRET');
    const redirectUri = this.config.get<string>('AZURE_REDIRECT_URI');

    if (clientId && tenantId && clientSecret) {
      this.initializeMsal({
        tenantId,
        clientId,
        clientSecret,
        redirectUri:
          redirectUri ||
          'https://localhost/api/v1/auth/sso/microsoft/callback',
      });
      this.logger.log(
        'Microsoft Entra ID SSO aus Umgebungsvariablen initialisiert',
      );
    } else {
      this.logger.warn(
        'Microsoft Entra ID SSO nicht konfiguriert (weder Redis noch Umgebungsvariablen)',
      );
    }
  }

  /**
   * MSAL Client mit der gegebenen Konfiguration initialisieren.
   */
  private initializeMsal(ssoConfig: SsoConfig): void {
    const msalConfig: Configuration = {
      auth: {
        clientId: ssoConfig.clientId,
        authority: `https://login.microsoftonline.com/${ssoConfig.tenantId}`,
        clientSecret: ssoConfig.clientSecret,
      },
      system: {
        loggerOptions: {
          loggerCallback: (level, message) => {
            this.logger.debug(`MSAL [${level}]: ${message}`);
          },
        },
      },
    };

    this.msalClient = new ConfidentialClientApplication(msalConfig);
    this.redirectUri = ssoConfig.redirectUri;
  }

  /**
   * Konfiguration aus Redis laden.
   */
  private async loadConfigFromRedis(): Promise<SsoConfig | null> {
    try {
      const raw = await this.redis.get(SSO_CONFIG_KEY);
      if (!raw) return null;

      const config = JSON.parse(raw) as SsoConfig;
      if (config.tenantId && config.clientId && config.clientSecret) {
        return config;
      }
      return null;
    } catch (err) {
      this.logger.warn(
        `Fehler beim Laden der SSO-Config aus Redis: ${(err as Error).message}`,
      );
      return null;
    }
  }

  /**
   * Konfiguration speichern und MSAL Client neu initialisieren.
   * Wird vom Admin-UI aufgerufen.
   */
  async reconfigure(ssoConfig: SsoConfig): Promise<void> {
    // In Redis speichern (persistent, kein TTL)
    await this.redis.set(SSO_CONFIG_KEY, JSON.stringify(ssoConfig));

    // MSAL Client neu initialisieren
    this.initializeMsal(ssoConfig);

    this.logger.log('Microsoft Entra ID SSO neu konfiguriert via Admin-UI');
  }

  /**
   * Aktuelle Konfiguration lesen (Secret wird maskiert).
   */
  async getConfig(): Promise<
    | (Omit<SsoConfig, 'clientSecret'> & { clientSecretMasked: string })
    | null
  > {
    // Zuerst aus Redis
    const redisConfig = await this.loadConfigFromRedis();
    if (redisConfig) {
      return {
        tenantId: redisConfig.tenantId,
        clientId: redisConfig.clientId,
        redirectUri: redisConfig.redirectUri,
        clientSecretMasked: this.maskSecret(redisConfig.clientSecret),
      };
    }

    // Fallback: Env-Vars
    const clientId = this.config.get<string>('AZURE_CLIENT_ID');
    const tenantId = this.config.get<string>('AZURE_TENANT_ID');
    const clientSecret = this.config.get<string>('AZURE_CLIENT_SECRET');
    const redirectUri = this.config.get<string>('AZURE_REDIRECT_URI');

    if (clientId && tenantId && clientSecret) {
      return {
        tenantId,
        clientId,
        redirectUri:
          redirectUri ||
          'https://localhost/api/v1/auth/sso/microsoft/callback',
        clientSecretMasked: this.maskSecret(clientSecret),
      };
    }

    return null;
  }

  /**
   * Secret maskieren: nur die letzten 4 Zeichen anzeigen.
   */
  private maskSecret(secret: string): string {
    if (secret.length <= 4) return '****';
    return '****' + secret.slice(-4);
  }

  /**
   * Ist Entra ID SSO konfiguriert?
   */
  isConfigured(): boolean {
    return !!this.msalClient;
  }

  /**
   * Authorization-URL fuer den OAuth2 Flow generieren.
   * @param state CSRF-Token (wird in Redis gespeichert)
   */
  async getAuthUrl(state: string): Promise<string> {
    if (!this.msalClient) {
      throw new ServiceUnavailableException(
        'Microsoft SSO ist nicht konfiguriert',
      );
    }

    const authUrlRequest: AuthorizationUrlRequest = {
      scopes: this.scopes,
      redirectUri: this.redirectUri,
      state,
      prompt: 'select_account',
    };

    const authUrl = await this.msalClient.getAuthCodeUrl(authUrlRequest);
    this.logger.debug('Authorization URL generiert');
    return authUrl;
  }

  /**
   * Authorization Code gegen Tokens tauschen und User-Info extrahieren.
   * @param code Authorization Code von Microsoft
   */
  async handleCallback(code: string): Promise<MsUserInfo> {
    if (!this.msalClient) {
      throw new ServiceUnavailableException(
        'Microsoft SSO ist nicht konfiguriert',
      );
    }

    const tokenRequest: AuthorizationCodeRequest = {
      code,
      scopes: this.scopes,
      redirectUri: this.redirectUri,
    };

    const response: AuthenticationResult =
      await this.msalClient.acquireTokenByCode(tokenRequest);

    this.logger.debug('Token erfolgreich erhalten');

    // User-Informationen aus ID-Token Claims extrahieren
    const claims = response.idTokenClaims as Record<string, unknown>;

    const oid = (claims.oid as string) || (claims.sub as string);
    if (!oid) {
      throw new Error('Keine Object ID (oid) im ID-Token gefunden');
    }

    // E-Mail: preferred_username, email, oder upn
    const email =
      (claims.preferred_username as string) ||
      (claims.email as string) ||
      (claims.upn as string) ||
      '';

    if (!email) {
      throw new Error('Keine E-Mail-Adresse im ID-Token gefunden');
    }

    // Namen: given_name + family_name, oder name splitten
    let firstName = (claims.given_name as string) || '';
    let lastName = (claims.family_name as string) || '';

    if (!firstName && !lastName && claims.name) {
      const parts = (claims.name as string).split(' ');
      firstName = parts[0] || '';
      lastName = parts.slice(1).join(' ') || '';
    }

    this.logger.log(`MS SSO User: ${email} (OID: ${oid})`);

    return {
      oid,
      email: email.toLowerCase(),
      firstName,
      lastName,
    };
  }
}
