import { Request, Response, CookieOptions } from 'express';
import * as authService from '../services/auth.service.js';
import { AuthRequest, ApiResponse } from '../types/index.js';
import prisma from '../lib/prisma.js';
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
import { validatePasswordComplexity, STAFF_MIN_PASSWORD_LENGTH, PORTAL_MIN_PASSWORD_LENGTH } from '../utils/passwordGenerator.js';

// Refresh-Token-Cookie-Konfiguration. Der Cookie:
// - httpOnly → kein JavaScript-Zugriff → bei XSS nicht klaubar
// - secure   → nur über HTTPS (in Prod via HTTPS_ENABLED, in Dev egal)
// - sameSite 'strict' → CSRF-Schutz; Cross-Site-Requests senden den Cookie nicht
// - path '/api/auth' → wird nur an Auth-Endpoints mitgeschickt
const REFRESH_COOKIE_NAME = 'refresh_token';
function getRefreshCookieOptions(): CookieOptions {
  return {
    httpOnly: true,
    secure: process.env.HTTPS_ENABLED === 'true',
    sameSite: 'strict',
    path: '/api/auth',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 Tage, gleicht Refresh-JWT-Lifetime
  };
}
function setRefreshCookie(res: Response, token: string): void {
  res.cookie(REFRESH_COOKIE_NAME, token, getRefreshCookieOptions());
}
function clearRefreshCookie(res: Response): void {
  res.clearCookie(REFRESH_COOKIE_NAME, { path: '/api/auth' });
}

// Whitelist von Fehlermeldungen, die wir an Login-Clients durchreichen dürfen.
// ALLES andere (Prisma-Internals, DB-Connection-Errors, Schema-Fehler, ...)
// wird als generisches "Anmeldung fehlgeschlagen" maskiert – die Original-
// Message bleibt im Server-Log, leakt aber nicht im HTTP-Response. Pentest
// Runde 3 (2026-05-16): `prisma.customer.findUnique() invocation: The column
// X does not exist` war im Body sichtbar → Tabellen-/Spaltennamen geleakt.
const SAFE_LOGIN_ERRORS = new Set([
  'Ungültige Anmeldedaten',
  'E-Mail und Passwort erforderlich',
]);
function safeLoginError(err: unknown): string {
  if (err instanceof Error && SAFE_LOGIN_ERRORS.has(err.message)) {
    return err.message;
  }
  if (err instanceof Error) {
    console.error('[Login] Unerwarteter Fehler (maskiert):', err.message);
  }
  return 'Anmeldung fehlgeschlagen';
}

// Mitarbeiter-Login
export async function login(req: Request, res: Response): Promise<void> {
  const { email, password } = req.body || {};
  const ctx = contextFromRequest(req);
  try {
    if (!email || !password) {
      res.status(400).json({
        success: false,
        error: 'E-Mail und Passwort erforderlich',
      } as ApiResponse);
      return;
    }

    const result = await authService.login(email, password);
    // Refresh-Token in httpOnly-Cookie, Access-Token im Body (Frontend hält
    // ihn nur in memory). `token`-Feld bleibt aus Kompatibilität bestehen.
    setRefreshCookie(res, result.refreshToken);
    emitSecurityEvent({
      type: 'LOGIN_SUCCESS',
      severity: 'INFO',
      message: `Mitarbeiter-Login: ${email}`,
      ipAddress: ctx.ipAddress,
      userId: result.user.id,
      userEmail: email,
      endpoint: ctx.endpoint,
    });
    res.json({
      success: true,
      data: { token: result.accessToken, user: result.user },
    } as ApiResponse);
  } catch (error) {
    emitSecurityEvent({
      type: 'LOGIN_FAILED',
      severity: 'LOW',
      message: `Login-Fehlversuch (Mitarbeiter): ${email || '<leer>'}`,
      ipAddress: ctx.ipAddress,
      userEmail: email,
      endpoint: ctx.endpoint,
    });
    res.status(401).json({
      success: false,
      error: safeLoginError(error),
    } as ApiResponse);
  }
}

// Kundenportal-Login
export async function customerLogin(req: Request, res: Response): Promise<void> {
  const { email, password } = req.body || {};
  const ctx = contextFromRequest(req);
  try {
    if (!email || !password) {
      res.status(400).json({
        success: false,
        error: 'E-Mail und Passwort erforderlich',
      } as ApiResponse);
      return;
    }

    const result = await authService.customerLogin(email, password);
    setRefreshCookie(res, result.refreshToken);
    emitSecurityEvent({
      type: 'LOGIN_SUCCESS',
      severity: 'INFO',
      message: `Portal-Login: ${email}`,
      ipAddress: ctx.ipAddress,
      customerId: result.user.customerId,
      userEmail: email,
      endpoint: ctx.endpoint,
    });
    res.json({
      success: true,
      data: { token: result.accessToken, user: result.user },
    } as ApiResponse);
  } catch (error) {
    emitSecurityEvent({
      type: 'LOGIN_FAILED',
      severity: 'LOW',
      message: `Login-Fehlversuch (Portal): ${email || '<leer>'}`,
      ipAddress: ctx.ipAddress,
      userEmail: email,
      endpoint: ctx.endpoint,
    });
    res.status(401).json({
      success: false,
      error: safeLoginError(error),
    } as ApiResponse);
  }
}

export async function me(req: AuthRequest, res: Response): Promise<void> {
  try {
    if (!req.user) {
      res.status(401).json({
        success: false,
        error: 'Nicht authentifiziert',
      } as ApiResponse);
      return;
    }

    // Kundenportal-Login
    if (req.user.isCustomerPortal && req.user.customerId) {
      const customer = await authService.getCustomerPortalUser(req.user.customerId);
      if (!customer) {
        res.status(404).json({
          success: false,
          error: 'Kunde nicht gefunden',
        } as ApiResponse);
        return;
      }
      res.json({ success: true, data: customer } as ApiResponse);
      return;
    }

    // Mitarbeiter-Login
    if (!req.user.userId) {
      res.status(401).json({
        success: false,
        error: 'Ungültige Authentifizierung',
      } as ApiResponse);
      return;
    }

    const user = await authService.getUserById(req.user.userId);
    if (!user) {
      res.status(404).json({
        success: false,
        error: 'Benutzer nicht gefunden',
      } as ApiResponse);
      return;
    }

    res.json({ success: true, data: user } as ApiResponse);
  } catch (error) {
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Benutzerdaten',
    } as ApiResponse);
  }
}

/**
 * Passwort-Reset anfordern (Email + Token per Mail).
 * Immer 200 OK zurückgeben um Email-Existenz nicht preiszugeben (User-Enumeration-Schutz).
 */
export async function requestPasswordReset(req: Request, res: Response): Promise<void> {
  try {
    const { email, userType } = req.body; // userType: 'admin' | 'portal'

    if (!email) {
      res.status(400).json({ success: false, error: 'E-Mail erforderlich' } as ApiResponse);
      return;
    }

    await authService.requestPasswordReset(email, userType === 'portal' ? 'portal' : 'admin');

    const ctx = contextFromRequest(req);
    emitSecurityEvent({
      type: 'PASSWORD_RESET_REQUEST',
      severity: 'MEDIUM',
      message: `Passwort-Reset angefordert (${userType === 'portal' ? 'Portal' : 'Mitarbeiter'}): ${email}`,
      ipAddress: ctx.ipAddress,
      userEmail: email,
      endpoint: ctx.endpoint,
      details: { userType: userType === 'portal' ? 'portal' : 'admin' },
    });

    // IMMER success senden, damit Angreifer nicht herausfinden kann welche Emails existieren
    res.json({
      success: true,
      message: 'Wenn ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.',
    } as ApiResponse);
  } catch (error) {
    console.error('Password reset request error:', error);
    // Auch bei Fehlern dieselbe Antwort
    res.json({
      success: true,
      message: 'Wenn ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.',
    } as ApiResponse);
  }
}

/**
 * Passwort-Reset bestätigen (Token + neues Passwort).
 */
export async function confirmPasswordReset(req: Request, res: Response): Promise<void> {
  try {
    const { token, password } = req.body;

    if (!token || !password) {
      res.status(400).json({
        success: false,
        error: 'Token und neues Passwort erforderlich',
      } as ApiResponse);
      return;
    }

    // Audience anhand des Tokens bestimmen, damit Admin-Reset 25 Zeichen
    // verlangt und Portal-Customer-Reset weiterhin 12 reicht.
    const audience = await authService.getPasswordResetAudience(token);
    const minLength = audience === 'admin' ? STAFF_MIN_PASSWORD_LENGTH : PORTAL_MIN_PASSWORD_LENGTH;
    const complexity = validatePasswordComplexity(password, { minLength });
    if (!complexity.ok) {
      res.status(400).json({
        success: false,
        error: 'Passwort erfüllt Mindestanforderungen nicht: ' + complexity.errors.join(', '),
      } as ApiResponse);
      return;
    }

    await authService.confirmPasswordReset(token, password);

    const ctx = contextFromRequest(req);
    emitSecurityEvent({
      type: 'PASSWORD_RESET_CONFIRM',
      severity: 'HIGH',
      message: 'Passwort-Reset abgeschlossen',
      ipAddress: ctx.ipAddress,
      endpoint: ctx.endpoint,
    });

    res.json({
      success: true,
      message: 'Passwort erfolgreich zurückgesetzt. Du kannst dich jetzt einloggen.',
    } as ApiResponse);
  } catch (error) {
    const ctx = contextFromRequest(req);
    emitSecurityEvent({
      type: 'TOKEN_REJECTED',
      severity: 'MEDIUM',
      message: 'Passwort-Reset mit ungültigem/abgelaufenem Token versucht',
      ipAddress: ctx.ipAddress,
      endpoint: ctx.endpoint,
    });
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Passwort-Reset fehlgeschlagen',
    } as ApiResponse);
  }
}

/**
 * Logout: invalidiert den aktuellen JWT serverseitig durch Setzen von
 * tokenInvalidatedAt / portalTokenInvalidatedAt auf jetzt. Auth-Middleware
 * prüft dieses Feld und lehnt Tokens ab, deren `iat` davor liegt.
 *
 * Hinweis: Da JWTs stateless sind, gibt es keine echte Token-Revocation
 * ohne dieses Pattern. Logout invalidiert ALLE aktiven Sessions des Users
 * (auch andere Geräte) – akzeptabel für ein Sicherheits-Logout.
 */
export async function logout(req: AuthRequest, res: Response): Promise<void> {
  try {
    const user = req.user as any;
    if (!user) {
      res.json({ success: true, message: 'Bereits abgemeldet' } as ApiResponse);
      return;
    }
    if (user.isCustomerPortal && user.customerId) {
      await prisma.customer.update({
        where: { id: user.customerId },
        data: { portalTokenInvalidatedAt: new Date() },
      });
      // Ausgegebene Refresh-Tokens zusätzlich hart widerrufen (R164-02):
      // der Zeitstempel-Vergleich allein greift erst beim nächsten Refresh,
      // der Widerruf macht die Tokens sofort unbrauchbar.
      await authService.revokeAllRefreshTokens({ customerId: user.customerId }, 'Logout');
    } else if (user.userId) {
      await prisma.user.update({
        where: { id: user.userId },
        data: { tokenInvalidatedAt: new Date() },
      });
      await authService.revokeAllRefreshTokens({ userId: user.userId }, 'Logout');
    }
    // Refresh-Cookie löschen, sonst könnte der Browser einen abgemeldeten User
    // direkt wieder einloggen (server-seitige Invalidation oben fängt das ab,
    // aber UI würde sich verirren).
    clearRefreshCookie(res);
    const ctx = contextFromRequest(req);
    emitSecurityEvent({
      type: 'LOGOUT',
      severity: 'INFO',
      message: `Logout: ${user.email || (user.isCustomerPortal ? 'Portal-User' : 'Mitarbeiter')}`,
      ipAddress: ctx.ipAddress,
      userId: ctx.userId,
      customerId: ctx.customerId,
      userEmail: user.email,
      endpoint: ctx.endpoint,
    });
    res.json({ success: true, message: 'Abgemeldet' } as ApiResponse);
  } catch (error) {
    res.status(500).json({
      success: false,
      error: 'Fehler beim Abmelden',
    } as ApiResponse);
  }
}

// Neuen Access-Token aus dem httpOnly-Refresh-Cookie holen. Wird vom Frontend
// (axios-Interceptor) bei 401 oder beim App-Start aufgerufen.
export async function refresh(req: Request, res: Response): Promise<void> {
  try {
    const cookies = (req as any).cookies || {};
    const refreshToken = cookies[REFRESH_COOKIE_NAME];
    if (!refreshToken) {
      res.status(401).json({ success: false, error: 'Kein Refresh-Token vorhanden' } as ApiResponse);
      return;
    }

    const result = await authService.refreshAccessToken(refreshToken);
    // Refresh-Cookie rotieren – verhindert Replay eines geklauten Refresh-Tokens
    // bis zur vollen Lifetime.
    setRefreshCookie(res, result.refreshToken);
    res.json({
      success: true,
      data: { token: result.accessToken, user: result.user },
    } as ApiResponse);
  } catch (error) {
    // Refresh fehlgeschlagen: Cookie wegputzen, damit der Browser nicht
    // weiter mit einem invaliden Token weiterhin den Endpoint klopft.
    clearRefreshCookie(res);
    // Detection (Pentest R164-01): ein vorgelegter, aber abgelehnter Refresh-Token
    // ist potenzieller Replay/Brute-Force → TOKEN_REJECTED emittieren, damit die
    // bestehende Schwelle (>=3 TOKEN_REJECTED/5min/IP → CRITICAL) greift.
    // Severity wie beim Access-Token (auth-Middleware): abgelaufen/revoked = LOW
    // (benigne, kein Sofort-Alert), ungültige Signatur/Manipulation = HIGH.
    const ctx = contextFromRequest(req);
    const code = (error as { code?: string })?.code;
    const msg = error instanceof Error ? error.message : 'Refresh fehlgeschlagen';

    if (code === 'REFRESH_REPLAY') {
      // Ein bereits eingelöster Refresh-Token wurde erneut vorgelegt. Das ist
      // kein Betriebsfehler, sondern der Abdruck eines gestohlenen Tokens –
      // die Sitzungsfamilie ist bereits widerrufen (R164-02).
      emitSecurityEvent({
        type: 'SUSPICIOUS',
        severity: 'CRITICAL',
        message: 'Refresh-Token-Replay erkannt – Sitzungsfamilie widerrufen',
        ipAddress: ctx.ipAddress,
        endpoint: ctx.endpoint,
      });
    } else {
      // REFRESH_LEGACY = Token aus der Zeit vor dem Replay-Schutz; das ist eine
      // einmalige Neuanmeldung nach dem Deploy, kein Angriffsindiz.
      const benign = code === 'REFRESH_EXPIRED' || code === 'REFRESH_LEGACY'
        || code === 'REFRESH_REVOKED' || /invalidiert/i.test(msg);
      emitSecurityEvent({
        type: 'TOKEN_REJECTED',
        severity: benign ? 'LOW' : 'HIGH',
        message: `Refresh-Token abgelehnt: ${msg}`,
        ipAddress: ctx.ipAddress,
        endpoint: ctx.endpoint,
      });
    }
    res.status(401).json({
      success: false,
      error: msg,
    } as ApiResponse);
  }
}

export async function register(req: Request, res: Response): Promise<void> {
  try {
    const { email, password, firstName, lastName, roleIds } = req.body;

    if (!email || !password || !firstName || !lastName) {
      res.status(400).json({
        success: false,
        error: 'Alle Pflichtfelder müssen ausgefüllt sein',
      } as ApiResponse);
      return;
    }

    // Mitarbeiter-Anlage: 25-Zeichen-Schwellwert
    const complexity = validatePasswordComplexity(password, { minLength: STAFF_MIN_PASSWORD_LENGTH });
    if (!complexity.ok) {
      res.status(400).json({
        success: false,
        error: 'Passwort erfüllt Mindestanforderungen nicht: ' + complexity.errors.join(', '),
      } as ApiResponse);
      return;
    }

    const user = await authService.createUser({
      email,
      password,
      firstName,
      lastName,
      roleIds: roleIds || [2], // Default to employee role
    });

    res.status(201).json({ success: true, data: user } as ApiResponse);
  } catch (error) {
    res.status(400).json({
      success: false,
      error:
        error instanceof Error
          ? error.message
          : 'Benutzer konnte nicht erstellt werden',
    } as ApiResponse);
  }
}

// Kurzlebiger Download-Token (60s) für Aufrufe, die den Token in der URL
// brauchen (PDF-iframes, window.open für Audit-Export usw.). Aufrufer
// authentifiziert sich normal per Bearer-Header. Antwort: ein download-
// scoped JWT, das die Auth-Middleware nur via `?token=` akzeptiert.
export async function createDownloadToken(req: AuthRequest, res: Response): Promise<void> {
  try {
    if (!req.user) {
      res.status(401).json({ success: false, error: 'Nicht authentifiziert' } as ApiResponse);
      return;
    }
    const payload: any = {
      email: req.user.email,
      permissions: req.user.permissions,
      isCustomerPortal: !!req.user.isCustomerPortal,
    };
    if (req.user.userId) payload.userId = req.user.userId;
    if (req.user.customerId) payload.customerId = req.user.customerId;
    if ((req.user as any).representedCustomerIds) {
      payload.representedCustomerIds = (req.user as any).representedCustomerIds;
    }
    const token = authService.signDownloadToken(payload);
    res.json({ success: true, data: { token } } as ApiResponse);
  } catch (error) {
    res.status(500).json({
      success: false,
      error: 'Fehler beim Erstellen des Download-Tokens',
    } as ApiResponse);
  }
}

// Vom Endkunden selbst nach Einmalpasswort-Login aufgerufen, um sein eigenes
// Passwort zu vergeben. Server invalidiert die laufende Session, Frontend
// loggt aus und schickt zurück zum Login.
export async function changeInitialPortalPassword(req: AuthRequest, res: Response): Promise<void> {
  try {
    if (!req.user?.isCustomerPortal || !req.user?.customerId) {
      res.status(403).json({
        success: false,
        error: 'Nur für Kundenportal-Login',
      } as ApiResponse);
      return;
    }
    // Pflicht-Check: NUR im Einmalpasswort-Flow erlaubt. Sonst könnte jeder
    // eingeloggte Portal-User sein Passwort ohne Kenntnis des alten ändern
    // (z.B. nach XSS-Token-Diebstahl). Pentest Runde 5 (2026-05-16) – KRITISCH.
    const customer = await prisma.customer.findUnique({
      where: { id: req.user.customerId },
      select: { portalPasswordMustChange: true },
    });
    if (!customer?.portalPasswordMustChange) {
      res.status(403).json({
        success: false,
        error: 'Nicht erlaubt',
      } as ApiResponse);
      return;
    }
    const { newPassword } = req.body || {};
    if (!newPassword || typeof newPassword !== 'string') {
      res.status(400).json({
        success: false,
        error: 'Neues Passwort erforderlich',
      } as ApiResponse);
      return;
    }
    const complexity = validatePasswordComplexity(newPassword);
    if (!complexity.ok) {
      res.status(400).json({
        success: false,
        error: 'Passwort erfüllt Mindestanforderungen nicht: ' + complexity.errors.join(', '),
      } as ApiResponse);
      return;
    }
    await authService.changeInitialPortalPassword(req.user.customerId, newPassword);
    clearRefreshCookie(res);
    res.json({ success: true, message: 'Passwort geändert' } as ApiResponse);
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error instanceof Error ? error.message : 'Passwort konnte nicht geändert werden',
    } as ApiResponse);
  }
}
