import {
  Injectable,
  UnauthorizedException,
  ForbiddenException,
  Logger,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import { PrismaService } from '../../prisma/prisma.service';
import { RedisService } from '../../redis/redis.service';
import { TotpService } from './totp.service';
import { LoginDto } from './dto/login.dto';
import { JwtPayload } from '../../common/decorators/current-user.decorator';

interface TokenPair {
  accessToken: string;
  refreshToken: string;
}

interface LoginResponse {
  accessToken: string;
  refreshToken?: string;
  user: {
    id: string;
    email: string;
    firstName: string;
    lastName: string;
    role: string;
    twoFactorEnabled: boolean;
  };
  requiresTwoFactor?: boolean;
}

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly jwt: JwtService,
    private readonly redis: RedisService,
    private readonly config: ConfigService,
    private readonly totp: TotpService,
  ) {}

  /**
   * Login mit E-Mail und Passwort.
   * Gibt AccessToken + Refresh-Token (HttpOnly Cookie) zurueck.
   */
  async login(dto: LoginDto): Promise<LoginResponse> {
    // User finden
    const user = await this.prisma.user.findUnique({
      where: { email: dto.email.toLowerCase() },
      include: {
        authProvider: true,
        tenantMemberships: {
          include: { tenant: true },
          where: { isActive: true },
          take: 1,
        },
      },
    });

    if (!user || !user.isActive) {
      // Generische Fehlermeldung (kein Hinweis ob User existiert)
      throw new UnauthorizedException('Ungültige Anmeldedaten');
    }

    // Passwort pruefen (nur fuer lokale Auth)
    const localAuth = user.authProvider.find((ap) => ap.provider === 'LOCAL');
    if (!localAuth?.passwordHash) {
      throw new UnauthorizedException('Ungültige Anmeldedaten');
    }

    const passwordValid = await bcrypt.compare(
      dto.password,
      localAuth.passwordHash,
    );
    if (!passwordValid) {
      // Failed Login zaehlen
      await this.prisma.user.update({
        where: { id: user.id },
        data: {
          failedLoginAttempts: { increment: 1 },
          lastFailedLogin: new Date(),
        },
      });
      throw new UnauthorizedException('Ungültige Anmeldedaten');
    }

    // Account-Sperre pruefen (nach 5 Fehlversuchen)
    if (user.failedLoginAttempts >= 5) {
      const lockoutEnd = user.lastFailedLogin
        ? new Date(user.lastFailedLogin.getTime() + 15 * 60 * 1000) // 15 Min Sperre
        : null;

      if (lockoutEnd && lockoutEnd > new Date()) {
        throw new ForbiddenException(
          'Account temporaer gesperrt. Versuchen Sie es in 15 Minuten erneut.',
        );
      }
    }

    // 2FA pruefen
    if (user.twoFactorEnabled) {
      if (!dto.totpCode) {
        return {
          accessToken: '',
          user: {
            id: user.id,
            email: user.email,
            firstName: user.firstName,
            lastName: user.lastName,
            role: user.role,
            twoFactorEnabled: user.twoFactorEnabled,
          },
          requiresTwoFactor: true,
        };
      }

      const totpValid = this.totp.verify(
        dto.totpCode,
        localAuth.totpSecret ?? '',
      );
      if (!totpValid) {
        throw new UnauthorizedException('Ungültiger 2FA-Code');
      }
    }

    // Erfolgreicher Login: Counter zurücksetzen
    await this.prisma.user.update({
      where: { id: user.id },
      data: {
        failedLoginAttempts: 0,
        lastLogin: new Date(),
      },
    });

    // Tenant-Info
    const primaryMembership = user.tenantMemberships[0];

    // Tokens generieren
    const tokens = await this.generateTokenPair({
      sub: user.id,
      email: user.email,
      role: user.role,
      tenantId: primaryMembership?.tenant.id,
      tenantSlug: primaryMembership?.tenant.slug,
    });

    this.logger.log(`Login erfolgreich: ${user.email}`);

    return {
      accessToken: tokens.accessToken,
      refreshToken: tokens.refreshToken,
      user: {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        role: user.role,
        twoFactorEnabled: user.twoFactorEnabled,
      },
    };
  }

  /**
   * Refresh-Token gegen neues Token-Paar tauschen.
   */
  async refreshTokens(refreshToken: string): Promise<TokenPair> {
    try {
      const payload = this.jwt.verify<JwtPayload>(refreshToken);

      // Refresh-Token-Familie pruefen (Token-Rotation)
      const isValid = await this.redis.isRefreshTokenFamilyValid(
        payload.sub,
        payload.jti,
      );
      if (!isValid) {
        // Möglicherweise Refresh-Token-Diebstahl: alle invalidieren
        this.logger.warn(
          `Verdächtiger Refresh-Token Wiederverwendung für User ${payload.sub}`,
        );
        await this.redis.invalidateAllRefreshTokens(payload.sub);
        throw new UnauthorizedException('Refresh Token ungültig');
      }

      // Alten Refresh-Token invalidieren
      await this.redis.blockToken(payload.jti, 7 * 24 * 60 * 60);

      // Neue Tokens generieren
      return this.generateTokenPair({
        sub: payload.sub,
        email: payload.email,
        role: payload.role,
        tenantId: payload.tenantId,
        tenantSlug: payload.tenantSlug,
      });
    } catch (error) {
      if (error instanceof UnauthorizedException) throw error;
      throw new UnauthorizedException('Refresh Token ungültig oder abgelaufen');
    }
  }

  /**
   * Logout: Access- und Refresh-Token invalidieren.
   */
  async logout(accessToken: JwtPayload, refreshToken?: string): Promise<void> {
    // Access-Token blocken (Restlaufzeit)
    const ttl = accessToken.exp - Math.floor(Date.now() / 1000);
    if (ttl > 0) {
      await this.redis.blockToken(accessToken.jti, ttl);
    }

    // Refresh-Token blocken
    if (refreshToken) {
      try {
        const refreshPayload = this.jwt.verify<JwtPayload>(refreshToken);
        const refreshTtl =
          refreshPayload.exp - Math.floor(Date.now() / 1000);
        if (refreshTtl > 0) {
          await this.redis.blockToken(refreshPayload.jti, refreshTtl);
        }
      } catch {
        // Refresh-Token ist bereits abgelaufen - ignorieren
      }
    }

    this.logger.log(`Logout: User ${accessToken.sub}`);
  }

  /**
   * 2FA-Setup: Neues TOTP-Secret generieren und QR-Code zurueckgeben.
   * Secret wird temporaer in Redis gespeichert (5 Minuten TTL).
   * Erst nach Verifizierung wird das Secret permanent in der DB gespeichert.
   */
  async setup2fa(userId: string): Promise<{ qrCode: string; secret: string }> {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
    });

    if (!user) {
      throw new UnauthorizedException('Benutzer nicht gefunden');
    }

    if (user.twoFactorEnabled) {
      throw new ForbiddenException('2FA ist bereits aktiviert');
    }

    const secret = this.totp.generateSecret();
    const qrCode = await this.totp.generateQrCode(user.email, secret);

    // Secret temporaer in Redis speichern (5 Minuten zum Einrichten)
    await this.redis.set(`2fa_setup:${userId}`, secret, 300);

    this.logger.log(`2FA-Setup gestartet fuer User ${user.email}`);

    return { qrCode, secret };
  }

  /**
   * 2FA aktivieren: TOTP-Code verifizieren und Secret permanent speichern.
   */
  async enable2fa(userId: string, totpCode: string): Promise<void> {
    // Temporaeres Secret aus Redis holen
    const secret = await this.redis.get(`2fa_setup:${userId}`);
    if (!secret) {
      throw new ForbiddenException(
        '2FA-Setup abgelaufen. Bitte erneut starten.',
      );
    }

    // TOTP-Code prüfen
    const isValid = this.totp.verify(totpCode, secret);
    if (!isValid) {
      throw new UnauthorizedException('Ungültiger 2FA-Code');
    }

    // Secret permanent in AuthProvider speichern + 2FA aktivieren
    await this.prisma.$transaction([
      this.prisma.authProvider.updateMany({
        where: { userId, provider: 'LOCAL' },
        data: { totpSecret: secret },
      }),
      this.prisma.user.update({
        where: { id: userId },
        data: { twoFactorEnabled: true },
      }),
    ]);

    // Temporäres Secret aus Redis löschen
    await this.redis.del(`2fa_setup:${userId}`);

    this.logger.log(`2FA aktiviert für User ${userId}`);
  }

  /**
   * 2FA deaktivieren: Passwort-Verifikation erforderlich.
   */
  async disable2fa(userId: string, password: string): Promise<void> {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      include: { authProvider: true },
    });

    if (!user) {
      throw new UnauthorizedException('Benutzer nicht gefunden');
    }

    if (!user.twoFactorEnabled) {
      throw new ForbiddenException('2FA ist nicht aktiviert');
    }

    // Passwort prüfen
    const localAuth = user.authProvider.find((ap) => ap.provider === 'LOCAL');
    if (!localAuth?.passwordHash) {
      throw new UnauthorizedException('Kein lokaler Auth-Provider gefunden');
    }

    const passwordValid = await bcrypt.compare(password, localAuth.passwordHash);
    if (!passwordValid) {
      throw new UnauthorizedException('Ungültiges Passwort');
    }

    // 2FA deaktivieren + Secret löschen
    await this.prisma.$transaction([
      this.prisma.authProvider.updateMany({
        where: { userId, provider: 'LOCAL' },
        data: { totpSecret: null },
      }),
      this.prisma.user.update({
        where: { id: userId },
        data: { twoFactorEnabled: false },
      }),
    ]);

    this.logger.log(`2FA deaktiviert für User ${userId}`);
  }

  /**
   * SSO-Login: User via Microsoft Entra ID (Azure AD) anmelden.
   *
   * Logik:
   * 1. User via AuthProvider(MS_SSO, providerId=oid) suchen
   * 2. Falls nicht gefunden: User via E-Mail suchen
   *    → Falls gefunden: MS_SSO AuthProvider verknuepfen (Auto-Link)
   *    → Falls nicht gefunden: Neuen User + MS_SSO AuthProvider anlegen
   * 3. User muss isActive sein
   * 4. JWT-Tokens generieren (wie bei normalem Login)
   */
  async loginViaSso(msUser: {
    oid: string;
    email: string;
    firstName: string;
    lastName: string;
  }): Promise<LoginResponse & { refreshToken: string }> {
    // 1. Bestehenden MS_SSO AuthProvider suchen (Provider ID = MS Object ID)
    const existingAuth = await this.prisma.authProvider.findFirst({
      where: {
        provider: 'MS_SSO',
        providerId: msUser.oid,
      },
      include: {
        user: {
          include: {
            tenantMemberships: {
              include: { tenant: true },
              where: { isActive: true },
              take: 1,
            },
          },
        },
      },
    });

    let user: {
      id: string;
      email: string;
      firstName: string;
      lastName: string;
      role: string;
      twoFactorEnabled: boolean;
      tenantMemberships?: Array<{
        tenant: { id: string; slug: string };
      }>;
    };

    if (existingAuth) {
      // Bekannter SSO-User
      user = existingAuth.user;
      this.logger.log(`SSO Login: Bekannter User ${user.email}`);
    } else {
      // 2. User via E-Mail suchen
      const existingUser = await this.prisma.user.findUnique({
        where: { email: msUser.email },
        include: {
          authProvider: true,
          tenantMemberships: {
            include: { tenant: true },
            where: { isActive: true },
            take: 1,
          },
        },
      });

      if (existingUser) {
        // MS_SSO AuthProvider verknuepfen (Auto-Link)
        await this.prisma.authProvider.create({
          data: {
            userId: existingUser.id,
            provider: 'MS_SSO',
            providerId: msUser.oid,
          },
        });
        user = existingUser;
        this.logger.log(
          `SSO Auto-Link: MS_SSO Provider fuer ${user.email} verknuepft`,
        );
      } else {
        // 3. Neuen User + MS_SSO AuthProvider anlegen
        const newUser = await this.prisma.user.create({
          data: {
            email: msUser.email,
            firstName: msUser.firstName || 'SSO',
            lastName: msUser.lastName || 'User',
            role: 'USER',
            isActive: true,
            authProvider: {
              create: {
                provider: 'MS_SSO',
                providerId: msUser.oid,
              },
            },
          },
          include: {
            tenantMemberships: {
              include: { tenant: true },
              where: { isActive: true },
              take: 1,
            },
          },
        });
        user = newUser;
        this.logger.log(`SSO Neuer User angelegt: ${user.email}`);
      }
    }

    // User muss aktiv sein
    if (!('isActive' in user) || !(user as { isActive: boolean }).isActive) {
      throw new UnauthorizedException(
        'Ihr Benutzerkonto ist deaktiviert. Bitte wenden Sie sich an den Administrator.',
      );
    }

    // lastLogin aktualisieren
    await this.prisma.user.update({
      where: { id: user.id },
      data: {
        lastLogin: new Date(),
        failedLoginAttempts: 0,
      },
    });

    // Tenant-Info
    const primaryMembership = user.tenantMemberships?.[0];

    // Tokens generieren
    const tokens = await this.generateTokenPair({
      sub: user.id,
      email: user.email,
      role: user.role,
      tenantId: primaryMembership?.tenant.id,
      tenantSlug: primaryMembership?.tenant.slug,
    });

    return {
      accessToken: tokens.accessToken,
      refreshToken: tokens.refreshToken,
      user: {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        role: user.role,
        twoFactorEnabled: user.twoFactorEnabled,
      },
    };
  }

  /**
   * Token-Paar generieren (Access + Refresh).
   */
  private async generateTokenPair(
    payload: Omit<JwtPayload, 'jti' | 'iat' | 'exp'>,
  ): Promise<TokenPair> {
    const accessJti = uuidv4();
    const refreshJti = uuidv4();

    const accessToken = this.jwt.sign({
      ...payload,
      jti: accessJti,
    });

    const refreshExpiry = this.config.get<string>(
      'JWT_REFRESH_TOKEN_EXPIRY',
      '7d',
    );
    const refreshToken = this.jwt.sign(
      {
        ...payload,
        jti: refreshJti,
      },
      { expiresIn: refreshExpiry },
    );

    // Refresh-Token-Familie in Redis registrieren
    await this.redis.setRefreshTokenFamily(
      payload.sub,
      refreshJti,
      7 * 24 * 60 * 60, // 7 Tage
    );

    return { accessToken, refreshToken };
  }
}
