import {
  Controller,
  Get,
  Post,
  Body,
  Query,
  Res,
  Logger,
  ServiceUnavailableException,
  UnauthorizedException,
  UseGuards,
  BadRequestException,
} from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
import { Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { Public } from '../../../common/decorators/public.decorator';
import { Roles } from '../../../common/decorators/roles.decorator';
import { RolesGuard } from '../../../common/guards/roles.guard';
import { RedisService } from '../../../redis/redis.service';
import { EntraIdService, type SsoConfig } from './entra-id.service';
import { AuthService } from '../auth.service';

/**
 * SsoController - Microsoft Entra ID SSO Endpoints.
 *
 * Flow:
 * 1. GET /auth/sso/microsoft         → Redirect zu Microsoft Login
 * 2. GET /auth/sso/microsoft/callback → Callback von Microsoft, User anlegen/verknuepfen, JWT generieren
 *
 * Admin:
 * - GET  /auth/sso/config  → Aktuelle Konfiguration lesen (Secret maskiert)
 * - POST /auth/sso/config  → Konfiguration speichern und MSAL neu initialisieren
 */
@ApiTags('SSO')
@Controller('auth/sso')
export class SsoController {
  private readonly logger = new Logger(SsoController.name);

  constructor(
    private readonly entraIdService: EntraIdService,
    private readonly authService: AuthService,
    private readonly redis: RedisService,
    private readonly config: ConfigService,
  ) {}

  /**
   * GET /api/v1/auth/sso/config
   * Aktuelle SSO-Konfiguration lesen (nur PLATFORM_ADMIN).
   * Client Secret wird maskiert zurueckgegeben.
   */
  @Get('config')
  @Roles('PLATFORM_ADMIN')
  @UseGuards(RolesGuard)
  @ApiOperation({ summary: 'SSO-Konfiguration lesen (Admin)' })
  async getConfig() {
    const config = await this.entraIdService.getConfig();
    return {
      configured: this.entraIdService.isConfigured(),
      config: config || null,
    };
  }

  /**
   * POST /api/v1/auth/sso/config
   * SSO-Konfiguration speichern und MSAL Client neu initialisieren (nur PLATFORM_ADMIN).
   */
  @Post('config')
  @Roles('PLATFORM_ADMIN')
  @UseGuards(RolesGuard)
  @ApiOperation({ summary: 'SSO-Konfiguration speichern (Admin)' })
  async saveConfig(
    @Body()
    body: {
      tenantId: string;
      clientId: string;
      clientSecret?: string;
      redirectUri: string;
    },
  ) {
    // Validierung
    if (!body.tenantId || !body.clientId || !body.redirectUri) {
      throw new BadRequestException(
        'Tenant ID, Client ID und Redirect URI sind erforderlich',
      );
    }

    // Wenn kein neues Secret: bestehendes aus Redis laden
    let clientSecret = body.clientSecret;
    if (!clientSecret) {
      const existing = await this.entraIdService.getConfig();
      // Wir brauchen das echte Secret aus Redis
      const raw = await this.redis.get('sso_config');
      if (raw) {
        const parsed = JSON.parse(raw) as SsoConfig;
        clientSecret = parsed.clientSecret;
      }
    }

    if (!clientSecret) {
      throw new BadRequestException(
        'Client Secret ist erforderlich (kein bestehendes Secret vorhanden)',
      );
    }

    const ssoConfig: SsoConfig = {
      tenantId: body.tenantId.trim(),
      clientId: body.clientId.trim(),
      clientSecret: clientSecret.trim(),
      redirectUri: body.redirectUri.trim(),
    };

    await this.entraIdService.reconfigure(ssoConfig);

    this.logger.log('SSO-Konfiguration via Admin-UI aktualisiert');

    return {
      success: true,
      message: 'SSO-Konfiguration gespeichert und aktiviert',
      configured: true,
    };
  }

  /**
   * GET /api/v1/auth/sso/microsoft
   * Initiiert den OAuth2 Authorization Code Flow.
   * Redirectet den Browser zur Microsoft-Login-Seite.
   */
  @Get('microsoft')
  @Public()
  @ApiOperation({ summary: 'Microsoft SSO Login starten' })
  async initiate(@Res() res: Response): Promise<void> {
    if (!this.entraIdService.isConfigured()) {
      throw new ServiceUnavailableException(
        'Microsoft SSO ist nicht konfiguriert',
      );
    }

    // CSRF-State generieren und in Redis speichern (5 Minuten TTL)
    const state = uuidv4();
    await this.redis.set(`sso_state:${state}`, '1', 300);

    // Authorization URL von MSAL holen
    const authUrl = await this.entraIdService.getAuthUrl(state);

    this.logger.log('SSO Flow gestartet, Redirect zu Microsoft');
    res.redirect(authUrl);
  }

  /**
   * GET /api/v1/auth/sso/microsoft/callback
   * Callback von Microsoft nach erfolgreicher Authentifizierung.
   * Erstellt/verknuepft User, generiert JWT, redirectet zum Frontend.
   */
  @Get('microsoft/callback')
  @Public()
  @ApiOperation({ summary: 'Microsoft SSO Callback' })
  async callback(
    @Query('code') code: string,
    @Query('state') state: string,
    @Query('error') error: string,
    @Query('error_description') errorDescription: string,
    @Res() res: Response,
  ): Promise<void> {
    const frontendUrl = this.config.get<string>(
      'FRONTEND_URL',
      'http://172.20.10.59',
    );

    // Fehler von Microsoft
    if (error) {
      this.logger.warn(`SSO Fehler von Microsoft: ${error} - ${errorDescription}`);
      res.redirect(
        `${frontendUrl}/login?sso_error=${encodeURIComponent(errorDescription || error)}`,
      );
      return;
    }

    // Code und State validieren
    if (!code || !state) {
      this.logger.warn('SSO Callback ohne Code oder State');
      res.redirect(`${frontendUrl}/login?sso_error=Ungültige SSO-Antwort`);
      return;
    }

    // CSRF-State aus Redis validieren
    const storedState = await this.redis.get(`sso_state:${state}`);
    if (!storedState) {
      this.logger.warn('SSO State ungültig oder abgelaufen');
      res.redirect(
        `${frontendUrl}/login?sso_error=SSO-Sitzung abgelaufen. Bitte erneut versuchen.`,
      );
      return;
    }

    // State verbrauchen (einmalig verwendbar)
    await this.redis.del(`sso_state:${state}`);

    try {
      // Code gegen Tokens tauschen und User-Info extrahieren
      const msUser = await this.entraIdService.handleCallback(code);

      // User finden oder anlegen + JWT generieren
      const loginResult = await this.authService.loginViaSso(msUser);

      // Refresh-Token als HttpOnly Cookie setzen (wie beim normalen Login)
      const isProduction = process.env.NODE_ENV === 'production';
      res.cookie('refresh_token', loginResult.refreshToken, {
        httpOnly: true,
        secure: isProduction,
        sameSite: isProduction ? 'strict' : 'lax',
        path: '/api/v1/auth',
        maxAge: 7 * 24 * 60 * 60 * 1000, // 7 Tage
      });

      // Redirect zum Frontend mit Access-Token
      this.logger.log(`SSO Login erfolgreich: ${msUser.email}`);
      res.redirect(
        `${frontendUrl}/auth/sso/callback?token=${loginResult.accessToken}`,
      );
    } catch (err) {
      this.logger.error(`SSO Callback Fehler: ${(err as Error).message}`);

      if (err instanceof UnauthorizedException) {
        res.redirect(
          `${frontendUrl}/login?sso_error=${encodeURIComponent((err as Error).message)}`,
        );
        return;
      }

      res.redirect(
        `${frontendUrl}/login?sso_error=SSO-Anmeldung fehlgeschlagen. Bitte erneut versuchen.`,
      );
    }
  }

  /**
   * GET /api/v1/auth/sso/status
   * Pruefen ob Microsoft SSO konfiguriert ist.
   * Wird vom Frontend genutzt um den SSO-Button anzuzeigen.
   */
  @Get('status')
  @Public()
  @ApiOperation({ summary: 'SSO Status abfragen' })
  async getStatus() {
    return {
      microsoft: this.entraIdService.isConfigured(),
    };
  }
}
