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';

/** Token-Daten die nach Integration-Callback gespeichert werden */
export interface M365TokenResult {
  accessToken: string;
  /** Kann leer sein wenn MSAL keinen Refresh-Token zurueckgibt */
  refreshToken: string;
  expiresAt: Date;
  scopes: string[];
  msTenantId: string;
}

/**
 * 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 integrationRedirectUri = '';
  private readonly scopes = ['openid', 'profile', 'email', 'User.Read'];
  private readonly integrationScopes = [
    'offline_access',
    'https://graph.microsoft.com/Mail.Read',
    'https://graph.microsoft.com/Calendars.ReadWrite',
    'https://graph.microsoft.com/Contacts.ReadWrite',
    'https://graph.microsoft.com/Tasks.ReadWrite',
  ];

  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;

    // Integration-Redirect-URI: Env-Variable hat Vorrang.
    // Fallback: Base-URL der SSO-URI + fester Pfad
    const explicitIntegrationUri = this.config.get<string>('AZURE_INTEGRATION_REDIRECT_URI');
    if (explicitIntegrationUri) {
      this.integrationRedirectUri = explicitIntegrationUri;
    } else {
      try {
        const ssoUrl = new URL(ssoConfig.redirectUri);
        this.integrationRedirectUri = `${ssoUrl.protocol}//${ssoUrl.host}/api/v1/auth/integrations/microsoft-365/callback`;
      } catch {
        this.integrationRedirectUri = 'http://localhost:3000/api/v1/auth/integrations/microsoft-365/callback';
      }
    }
  }

  /**
   * 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,
    };
  }

  // ── Microsoft 365 Integration (Graph API Scopes) ──────────────────────

  /**
   * Authorization-URL fuer MS365 Integration generieren.
   * Direkte URL-Konstruktion (kein MSAL) — umgeht MSAL v5 Kompatibilitaetsprobleme
   * mit reinen Graph-API-Scopes (ohne openid).
   * @param state CSRF-Token
   * @param redirectUri Optionaler Override (fuer dynamischen Host-basierten URI)
   */
  async getIntegrationAuthUrl(state: string, redirectUri?: string): Promise<string> {
    const ssoConfig = await this.loadConfigFromRedis();
    const tenantId =
      ssoConfig?.tenantId ?? this.config.get<string>('AZURE_TENANT_ID') ?? '';
    const clientId =
      ssoConfig?.clientId ?? this.config.get<string>('AZURE_CLIENT_ID') ?? '';

    if (!tenantId || !clientId) {
      throw new ServiceUnavailableException(
        'Microsoft SSO ist nicht konfiguriert',
      );
    }

    const params = new URLSearchParams({
      client_id: clientId,
      response_type: 'code',
      redirect_uri: redirectUri || this.integrationRedirectUri,
      scope: this.integrationScopes.join(' '),
      state,
      prompt: 'consent',
      response_mode: 'query',
    });

    const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params.toString()}`;
    this.logger.log(`M365 Integration Auth URL generiert (tenant: ${tenantId})`);
    return authUrl;
  }

  /**
   * Authorization Code gegen M365-Tokens tauschen (Standard OAuth2 POST).
   * Direkte fetch-Implementierung analog zu refreshIntegrationToken —
   * umgeht MSAL v5 Probleme mit acquireTokenByCode fuer Graph-Scopes.
   * @param code Authorization Code von Microsoft
   * @param redirectUri Muss exakt mit dem URI aus dem Auth-Request uebereinstimmen
   */
  async handleIntegrationCallback(code: string, redirectUri?: string): Promise<M365TokenResult> {
    const ssoConfig = await this.loadConfigFromRedis();
    const tenantId =
      ssoConfig?.tenantId ?? this.config.get<string>('AZURE_TENANT_ID') ?? '';
    const clientId =
      ssoConfig?.clientId ?? this.config.get<string>('AZURE_CLIENT_ID') ?? '';
    const clientSecret =
      ssoConfig?.clientSecret ??
      this.config.get<string>('AZURE_CLIENT_SECRET') ??
      '';

    if (!tenantId || !clientId || !clientSecret) {
      throw new ServiceUnavailableException(
        'Azure-Konfiguration fehlt fuer Token-Exchange',
      );
    }

    const params = new URLSearchParams({
      client_id: clientId,
      client_secret: clientSecret,
      code,
      redirect_uri: redirectUri || this.integrationRedirectUri,
      grant_type: 'authorization_code',
      scope: this.integrationScopes.join(' '),
    });

    const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
    const resp = await fetch(tokenEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: params.toString(),
    });

    if (!resp.ok) {
      const err = (await resp.json()) as {
        error_description?: string;
        error?: string;
      };
      throw new Error(
        `Token-Exchange fehlgeschlagen: ${err.error_description ?? err.error ?? resp.statusText}`,
      );
    }

    const data = (await resp.json()) as {
      access_token: string;
      refresh_token?: string;
      expires_in: number;
    };

    this.logger.log('M365 Integration Token erhalten');

    return {
      accessToken: data.access_token,
      refreshToken: data.refresh_token ?? '',
      expiresAt: new Date(Date.now() + data.expires_in * 1000),
      scopes: this.integrationScopes,
      msTenantId: tenantId,
    };
  }

  /**
   * Access-Token per Refresh-Token erneuern (Standard OAuth2 Token Endpoint).
   * Wird von IntegrationsService aufgerufen wenn Token abgelaufen.
   */
  async refreshIntegrationToken(
    refreshToken: string,
  ): Promise<{ accessToken: string; expiresAt: Date; newRefreshToken?: string }> {
    const ssoConfig = await this.loadConfigFromRedis();
    const tenantId =
      ssoConfig?.tenantId ?? this.config.get<string>('AZURE_TENANT_ID') ?? '';
    const clientId =
      ssoConfig?.clientId ?? this.config.get<string>('AZURE_CLIENT_ID') ?? '';
    const clientSecret =
      ssoConfig?.clientSecret ??
      this.config.get<string>('AZURE_CLIENT_SECRET') ??
      '';

    if (!tenantId || !clientId || !clientSecret) {
      throw new ServiceUnavailableException(
        'Azure-Konfiguration fehlt fuer Token-Refresh',
      );
    }

    const params = new URLSearchParams({
      client_id: clientId,
      client_secret: clientSecret,
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      scope: this.integrationScopes.join(' '),
    });

    const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
    const resp = await fetch(tokenEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: params.toString(),
    });

    if (!resp.ok) {
      const err = (await resp.json()) as { error_description?: string };
      throw new Error(
        `Token-Refresh fehlgeschlagen: ${err.error_description ?? resp.statusText}`,
      );
    }

    const data = (await resp.json()) as {
      access_token: string;
      expires_in: number;
      refresh_token?: string;
    };

    return {
      accessToken: data.access_token,
      expiresAt: new Date(Date.now() + data.expires_in * 1000),
      newRefreshToken: data.refresh_token,
    };
  }

  /** Integration-Redirect-URI zurueckgeben */
  getIntegrationRedirectUri(): string {
    return this.integrationRedirectUri;
  }
}
