import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import prisma from '../lib/prisma.js';
import * as userService from '../services/user.service.js';
import { logChange } from '../services/audit.service.js';
import { AUDIT_OPS_ROLLE } from '../services/user.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
import { pickUserCreate, pickUserUpdate, isValidEmail, sanitizePhoneField } from '../utils/sanitize.js';
import { validatePasswordComplexity, STAFF_MIN_PASSWORD_LENGTH } from '../utils/passwordGenerator.js';

// Users
export async function getUsers(req: Request, res: Response): Promise<void> {
  try {
    const { search, isActive, roleId, page, limit } = req.query;
    const result = await userService.getAllUsers({
      search: search as string,
      isActive: isActive !== undefined ? isActive === 'true' : undefined,
      roleId: roleId ? parseInt(roleId as string) : undefined,
      page: page ? parseInt(page as string) : undefined,
      limit: limit ? parseInt(limit as string) : undefined,
    });
    res.json({
      success: true,
      data: result.users,
      pagination: result.pagination,
    } as ApiResponse);
  } catch (error) {
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Benutzer',
    } as ApiResponse);
  }
}

export async function getUser(req: Request, res: Response): Promise<void> {
  try {
    const user = await userService.getUserById(parseInt(req.params.id));
    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 des Benutzers',
    } as ApiResponse);
  }
}

export async function createUser(req: Request, res: Response): Promise<void> {
  try {
    // Whitelist: nur erlaubte Felder aus req.body übernehmen (Mass-Assignment-Schutz)
    const data = pickUserCreate(req.body) as any;
    // Email-Format prüfen, sonst landet "x@y\nBcc:..." in der DB
    // (Pentest 29.4 – SMTP-Header-Injection).
    if (!isValidEmail(data?.email) || !data?.email) {
      res.status(400).json({ success: false, error: 'Ungültiges E-Mail-Format' } as ApiResponse);
      return;
    }
    if (data?.password) {
      const c = validatePasswordComplexity(data.password, { minLength: STAFF_MIN_PASSWORD_LENGTH });
      if (!c.ok) {
        res.status(400).json({
          success: false,
          error: 'Passwort erfüllt Mindestanforderungen nicht: ' + c.errors.join(', '),
        } as ApiResponse);
        return;
      }
    }
    // 60.3: WhatsApp- und Signal-Nummern gegen CRLF/Header-Injection sichern.
    try {
      if ('whatsappNumber' in data) {
        const cleaned = sanitizePhoneField(data.whatsappNumber, 'WhatsApp-Nummer');
        data.whatsappNumber = cleaned ?? null;
      }
      if ('signalNumber' in data) {
        const cleaned = sanitizePhoneField(data.signalNumber, 'Signal-Nummer');
        data.signalNumber = cleaned ?? null;
      }
    } catch (err) {
      res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültige Nummer' } as ApiResponse);
      return;
    }
    const user = await userService.createUser(data);
    await logChange({
      req, action: 'CREATE', resourceType: 'User',
      resourceId: user.id.toString(),
      label: `Benutzer ${user.firstName} ${user.lastName} angelegt`,
    });
    res.status(201).json({ success: true, data: user } as ApiResponse);
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Benutzers',
    } as ApiResponse);
  }
}

export async function updateUser(req: AuthRequest, res: Response): Promise<void> {
  try {
    const userId = parseInt(req.params.id);
    // `permissions` und `password` darf der generische Update nicht
    // entgegennehmen. Vorher landeten sie auf dem Floor (Whitelist-Drop),
    // der Caller bekam aber 200 zurück und glaubte fälschlich, die Werte
    // wären übernommen worden. Stattdessen sofort 400, damit Tooling /
    // Client den Fehler sieht. (Pentest 2026-05-20)
    //   - permissions kommen aus Rollen (PUT roleIds bzw. die DSGVO-/
    //     Developer-Checkboxen) und können nicht direkt am User hängen.
    //   - password wird über POST /users/:id/password gesetzt
    //     (eigene Komplexitäts-Validierung + Audit-Trail).
    const body = req.body || {};
    const forbidden = ['permissions', 'password', 'passwordHash'];
    const offending = forbidden.filter((k) => k in body);
    if (offending.length > 0) {
      res.status(400).json({
        success: false,
        error: `Felder nicht erlaubt: ${offending.join(', ')}. ` +
          (offending.includes('permissions')
            ? 'Permissions werden über roleIds / hasGdprAccess / hasDeveloperAccess gesteuert. '
            : '') +
          (offending.includes('password') || offending.includes('passwordHash')
            ? `Passwort über POST /users/${userId}/password setzen.`
            : ''),
      } as ApiResponse);
      return;
    }
    // Whitelist: nur erlaubte Felder aus req.body übernehmen (Mass-Assignment-Schutz)
    const data = pickUserUpdate(req.body) as Record<string, unknown>;
    // Email-Validierung gegen SMTP-Header-Injection (Pentest 29.4).
    // null/leer ist OK (Email darf optional sein), nur falsches Format prüfen.
    if (data?.email !== undefined && !isValidEmail(data.email)) {
      res.status(400).json({ success: false, error: 'Ungültiges E-Mail-Format' } as ApiResponse);
      return;
    }
    // 60.3: WhatsApp- und Signal-Nummern gegen CRLF/Header-Injection sichern.
    try {
      if ('whatsappNumber' in data) {
        const cleaned = sanitizePhoneField(data.whatsappNumber, 'WhatsApp-Nummer');
        data.whatsappNumber = cleaned ?? null;
      }
      if ('signalNumber' in data) {
        const cleaned = sanitizePhoneField(data.signalNumber, 'Signal-Nummer');
        data.signalNumber = cleaned ?? null;
      }
    } catch (err) {
      res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültige Nummer' } as ApiResponse);
      return;
    }

    // Vorherigen Stand laden für Audit – inkl. Rollen, damit hasGdprAccess /
    // hasDeveloperAccess (versteckte Rollen) korrekt verglichen werden.
    const beforeUser = await prisma.user.findUnique({
      where: { id: userId },
      include: { roles: { include: { role: true } } },
    });
    const before = beforeUser
      ? {
          ...beforeUser,
          hasGdprAccess: beforeUser.roles.some((ur) => ur.role.name === 'DSGVO'),
          hasDeveloperAccess: beforeUser.roles.some((ur) => ur.role.name === 'Developer'),
          hasAuditOpsAccess: beforeUser.roles.some((ur) => ur.role.name === AUDIT_OPS_ROLLE),
        }
      : null;

    // Der Audit-Betrieb-Haken vergibt die EINGREIFENDEN Rechte am Protokoll
    // (versiegeln, neu berechnen, aufraeumen, Aufbewahrung). Ihn zu setzen ist
    // kein gewoehnliches Benutzer-Update: Er entscheidet, wer die
    // Beweisgrundlage ersetzen darf.
    //
    // Eine eigene Rechte-Huerde davorzusetzen scheitert am Henne-Ei-Problem -
    // nach der Aufteilung haelt zunaechst niemand `audit:admin`, und dann
    // koennte ihn auch niemand vergeben. Stattdessen wird die Vergabe LAUT:
    // CRITICAL im Protokoll und zusaetzlich in den Alarmkanal, so wie beim
    // Dienstkonto-Kennzeichen (R184-01).
    const setztAuditBetrieb =
      data.hasAuditOpsAccess !== undefined &&
      before !== null &&
      data.hasAuditOpsAccess !== (before as any).hasAuditOpsAccess;
    const aktiviertAuditBetrieb = data.hasAuditOpsAccess === true;

    // Das Dienstkonto-Kennzeichen SENKT die Alarmstufe der Anmeldungen dieses
    // Kontos (Pentest R184). Damit ist es selbst ein Hebel zur Waesche: Wer sein
    // Konto so markiert, laesst die eigenen auffaelligen Anmeldungen als
    // Routine erscheinen. Es bekommt deshalb dieselbe Behandlung wie das
    // Absenken einer Aufbewahrungsfrist – Bestaetigung, laute Protokollierung,
    // und kein Selbstbedienen.
    const setztDienstkonto =
      typeof (data as any).isServiceAccount === 'boolean' &&
      (data as any).isServiceAccount !== before?.isServiceAccount;
    const aktiviertDienstkonto = setztDienstkonto && (data as any).isServiceAccount === true;

    // R184-02: Das Kennzeichen ist ein Audit-Governance-Eingriff, kein
    // gewoehnliches Benutzerfeld – es ist Geschwister von retention-shorten,
    // seal-backlog, rehash und cleanup, die alle `audit:admin` verlangen. Es
    // haengt aber an der Route mit `users:update`. Heute deckungsgleich, weil
    // nur die Administratorrolle beides hat – aber jede kuenftige Rolle mit
    // „Benutzer bearbeiten“ wuerde still „Login-Alarme herunterstufen“ erben.
    // Deshalb hier zusaetzlich auf die Domaene pruefen, in der der Schaden
    // liegt.
    if (setztDienstkonto && !(req.user?.permissions || []).includes('audit:admin')) {
      res.status(403).json({
        success: false,
        error:
          'Das Dienstkonto-Kennzeichen ändert, wie Anmeldungen im Audit-Log bewertet werden. ' +
          'Dafür genügt „Benutzer bearbeiten“ nicht – es erfordert die Berechtigung ' +
          'audit:admin, wie das Verkürzen von Aufbewahrungsfristen auch.',
      } as ApiResponse);
      return;
    }

    if (setztDienstkonto && req.user?.userId === userId) {
      res.status(403).json({
        success: false,
        error:
          'Das Dienstkonto-Kennzeichen lässt sich nicht am eigenen Konto setzen oder entfernen. ' +
          'Es stuft Anmeldungen dieses Kontos als Routine ein – wer das für sich selbst täte, ' +
          'könnte die eigenen Anmeldungen unauffällig machen. Bitte von einem anderen ' +
          'Administrator vornehmen lassen.',
      } as ApiResponse);
      return;
    }

    // Gate in BEIDE Richtungen (Pentest R184-01). Das Entfernen war ungegatet –
    // und es ist der gefaehrlichere Weg: Ein un-geflaggtes Konto faellt aus der
    // Heartbeat-Wache, weil diese am Live-Kennzeichen haengt. Genau das
    // „stilllegen und auf Stille setzen“, gegen das der Wachhund gebaut wurde.
    if (setztDienstkonto && req.body?.confirm !== 'SERVICE_ACCOUNT') {
      res.status(400).json({
        success: false,
        error: aktiviertDienstkonto
          ? `Damit werden künftige Anmeldungen von ${before?.email ?? 'diesem Konto'} im ` +
            'Audit-Log als Routine geführt statt als kritisches Ereignis. Das ist für ' +
            'planmäßig arbeitende Dienste gedacht (etwa das Gegenbuch) – für ein Konto, das ' +
            'ein Mensch benutzt, wäre es eine Tarnung. ' +
            'Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.'
          : `Damit fällt ${before?.email ?? 'dieses Konto'} aus der Überwachung heraus: Es wird ` +
            'dann nicht mehr gemeldet, wenn es sich nicht mehr anmeldet. Bei einem laufenden ' +
            'Dienst wie dem Gegenbuch hieße das, sein Ausfall bliebe unbemerkt. ' +
            'Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.',
      } as ApiResponse);
      return;
    }

    const user = await userService.updateUser(userId, data as any);
    if (user) {
      // Audit: Geänderte Felder ermitteln und loggen
      if (before) {
        const changes: Record<string, { von: unknown; nach: unknown }> = {};
        const fieldLabels: Record<string, string> = {
          email: 'E-Mail', firstName: 'Vorname', lastName: 'Nachname', isActive: 'Aktiv',
          hasGdprAccess: 'DSGVO-Zugriff', hasDeveloperAccess: 'Entwicklerzugriff',
          hasAuditOpsAccess: 'Audit-Betrieb (versiegeln, aufräumen, Aufbewahrung)',
          isServiceAccount: 'Dienstkonto (Anmeldungen als Routine)',
        };
        for (const [key, newVal] of Object.entries(data)) {
          if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
          const oldVal = (before as any)[key];
          const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
          if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
            const label = fieldLabels[key] || key;
            const formatVal = (v: unknown) => {
              if (v === null || v === undefined || v === '') return '-';
              if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
              return String(v);
            };
            changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
          }
        }
        const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von} → ${c.nach}`).join(', ');
        await logChange({
          req, action: 'UPDATE', resourceType: 'User',
          resourceId: user.id.toString(),
          label: setztDienstkonto
            ? `Dienstkonto-Kennzeichen ${aktiviertDienstkonto ? 'GESETZT' : 'entfernt'} für ` +
              `${user.email} – Anmeldungen werden künftig ` +
              `${aktiviertDienstkonto ? 'als Routine' : 'wieder als kritisch'} geführt`
            : changeList
              ? `Benutzer ${user.firstName} ${user.lastName} aktualisiert: ${changeList}`
              : `Benutzer ${user.firstName} ${user.lastName} aktualisiert`,
          // Die Aenderung dieses Kennzeichens wird wie ihre Wirkung eingestuft:
          // Sie beeinflusst, wie kuenftige Anmeldungen bewertet werden.
          sensitivity: setztDienstkonto || setztAuditBetrieb ? 'CRITICAL' : undefined,
          details: Object.keys(changes).length > 0 ? changes : undefined,
        });

        // Zusaetzlich in den Alarmkanal (Pentest R184-01). Eine CRITICAL-Zeile
        // im Audit-Log muss jemand LESEN – das ist die R183-02-Klasse. Das
        // Entfernen des Kennzeichens nimmt das Konto aus der Heartbeat-Wache;
        // ab dann faellt sein Ausbleiben nicht mehr auf. Diese Aenderung
        // gehoert deshalb dorthin, wo automatisch reagiert wird.
        if (setztDienstkonto) {
          const ctx = contextFromRequest(req);
          emitSecurityEvent({
            type: 'PERMISSION_CHANGED',
            severity: 'CRITICAL',
            message: aktiviertDienstkonto
              ? `Konto ${user.email} als Dienstkonto markiert – seine Anmeldungen gelten ab ` +
                'jetzt als Routine statt als kritisches Ereignis.'
              : `Konto ${user.email} ist KEIN Dienstkonto mehr – es fällt damit aus der ` +
                'Überwachung heraus: Ein Ausbleiben seiner Anmeldungen wird nicht mehr gemeldet.',
            ipAddress: ctx.ipAddress,
            userId: req.user?.userId,
            userEmail: req.user?.email,
            endpoint: ctx.endpoint,
            details: { betroffenesKonto: user.email, aktiviert: aktiviertDienstkonto },
          });
        }

        if (setztAuditBetrieb) {
          const ctx = contextFromRequest(req);
          emitSecurityEvent({
            type: 'PERMISSION_CHANGED',
            severity: 'CRITICAL',
            message: aktiviertAuditBetrieb
              ? `Konto ${user.email} darf ab jetzt am Audit-Protokoll EINGREIFEN: versiegeln, ` +
                'neu berechnen, aufräumen, Aufbewahrung ändern. Damit kann es die Grundlage ' +
                'ersetzen, gegen die Manipulation nachgewiesen wird.'
              : `Konto ${user.email} darf nicht mehr am Audit-Protokoll eingreifen.`,
            ipAddress: ctx.ipAddress,
            userId: req.user?.userId,
            userEmail: req.user?.email,
            endpoint: ctx.endpoint,
            details: {
              betroffenesKonto: user.email,
              aktiviert: aktiviertAuditBetrieb,
              selbstvergabe: user.id === req.user?.userId,
            },
          });
        }
      } else {
        await logChange({
          req, action: 'UPDATE', resourceType: 'User',
          resourceId: user.id.toString(),
          label: `Benutzer ${user.firstName} ${user.lastName} aktualisiert`,
        });
      }
    }
    res.json({ success: true, data: user } as ApiResponse);
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Benutzers',
    } as ApiResponse);
  }
}

// Admin setzt das Passwort eines anderen Users zurück. Separat vom
// generischen Update damit der Vorgang explizit auditiert wird und nicht
// versehentlich über Mass-Assignment passieren kann.
// Pentest Runde 12 (2026-05-18) MITTEL.
export async function setUserPassword(req: Request, res: Response): Promise<void> {
  try {
    const userId = parseInt(req.params.id);
    const { password, currentPassword } = req.body || {};
    if (!password || typeof password !== 'string') {
      res.status(400).json({ success: false, error: 'Passwort erforderlich' } as ApiResponse);
      return;
    }

    // Pentest 47.3 (MEDIUM, 2026-06-01): Re-Auth verpflichtend.
    // Ein gestohlener Admin-JWT reichte bisher, um Staff-Passwörter
    // umzuschreiben. Jetzt muss der aufrufende Admin sein eigenes
    // Passwort mitsenden – CSRF/Token-Klau allein reicht nicht mehr.
    const authReq = req as AuthRequest;
    const callerId = authReq.user?.userId;
    if (!callerId) {
      res.status(401).json({ success: false, error: 'Nicht authentifiziert' } as ApiResponse);
      return;
    }
    if (!currentPassword || typeof currentPassword !== 'string') {
      res.status(400).json({
        success: false,
        error: 'Bitte das eigene Passwort zur Bestätigung mitsenden.',
      } as ApiResponse);
      return;
    }
    const caller = await prisma.user.findUnique({
      where: { id: callerId },
      select: { password: true },
    });
    // Timing-Schutz: immer einen bcrypt.compare laufen lassen
    const callerHash = caller?.password ?? '$2a$10$0000000000000000000000000000000000000000000000000000';
    const reAuthOk = await bcrypt.compare(currentPassword, callerHash);
    if (!caller || !reAuthOk) {
      res.status(403).json({
        success: false,
        error: 'Eigenes Passwort ist falsch.',
      } as ApiResponse);
      return;
    }

    const c = validatePasswordComplexity(password, { minLength: STAFF_MIN_PASSWORD_LENGTH });
    if (!c.ok) {
      res.status(400).json({
        success: false,
        error: 'Passwort erfüllt Mindestanforderungen nicht: ' + c.errors.join(', '),
      } as ApiResponse);
      return;
    }
    const user = await userService.updateUser(userId, { password } as any);
    if (!user) {
      res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' } as ApiResponse);
      return;
    }
    // Pentest 48.4 (INFO, 2026-06-01): Bestehende JWTs für den Ziel-User
    // sofort invalidieren. Greift insbesondere bei Self-Reset (Admin setzt
    // sich selbst zurück) – der gestohlene Token wird damit ungültig,
    // statt noch bis zum natürlichen Ablauf brauchbar zu bleiben.
    // Die Middleware-Auth liest tokenInvalidatedAt und vergleicht gegen
    // den `iat`-Claim des JWT.
    await prisma.user.update({
      where: { id: user.id },
      data: { tokenInvalidatedAt: new Date() },
    });
    await logChange({
      req, action: 'UPDATE', resourceType: 'User',
      resourceId: user.id.toString(),
      label: `Passwort für Benutzer ${user.firstName} ${user.lastName} (${user.email}) durch Admin gesetzt (Re-Auth bestätigt, Sessions invalidiert)`,
    });
    res.json({ success: true, message: 'Passwort gesetzt' } as ApiResponse);
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Setzen des Passworts',
    } as ApiResponse);
  }
}

export async function deleteUser(req: Request, res: Response): Promise<void> {
  try {
    const userId = parseInt(req.params.id);
    const userBefore = await userService.getUserById(userId);
    await userService.deleteUser(userId);
    await logChange({
      req, action: 'DELETE', resourceType: 'User',
      resourceId: userId.toString(),
      label: `Benutzer ${userBefore?.firstName || ''} ${userBefore?.lastName || ''} gelöscht`,
    });
    res.json({ success: true, message: 'Benutzer gelöscht' } as ApiResponse);
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Löschen des Benutzers',
    } as ApiResponse);
  }
}

// Roles
export async function getRoles(req: Request, res: Response): Promise<void> {
  try {
    const roles = await userService.getAllRoles();
    res.json({ success: true, data: roles } as ApiResponse);
  } catch (error) {
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Rollen',
    } as ApiResponse);
  }
}

export async function getRole(req: Request, res: Response): Promise<void> {
  try {
    const role = await userService.getRoleById(parseInt(req.params.id));
    if (!role) {
      res.status(404).json({
        success: false,
        error: 'Rolle nicht gefunden',
      } as ApiResponse);
      return;
    }
    res.json({ success: true, data: role } as ApiResponse);
  } catch (error) {
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Rolle',
    } as ApiResponse);
  }
}

export async function createRole(req: Request, res: Response): Promise<void> {
  try {
    const role = await userService.createRole(req.body);
    await logChange({
      req, action: 'CREATE', resourceType: 'Role',
      resourceId: role.id.toString(),
      label: `Rolle ${role.name} angelegt`,
    });
    res.status(201).json({ success: true, data: role } as ApiResponse);
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Rolle',
    } as ApiResponse);
  }
}

export async function updateRole(req: Request, res: Response): Promise<void> {
  try {
    const role = await userService.updateRole(parseInt(req.params.id), req.body);
    if (role) {
      await logChange({
        req, action: 'UPDATE', resourceType: 'Role',
        resourceId: role.id.toString(),
        label: `Rolle ${role.name} aktualisiert`,
      });
    }
    res.json({ success: true, data: role } as ApiResponse);
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Rolle',
    } as ApiResponse);
  }
}

export async function deleteRole(req: Request, res: Response): Promise<void> {
  try {
    const roleId = parseInt(req.params.id);
    const role = await userService.getRoleById(roleId);
    await userService.deleteRole(roleId);
    await logChange({
      req, action: 'DELETE', resourceType: 'Role',
      resourceId: roleId.toString(),
      label: `Rolle ${role?.name || roleId} gelöscht`,
    });
    res.json({ success: true, message: 'Rolle gelöscht' } as ApiResponse);
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Löschen der Rolle',
    } as ApiResponse);
  }
}

// Permissions
export async function getPermissions(req: Request, res: Response): Promise<void> {
  try {
    const permissions = await userService.getAllPermissions();
    res.json({ success: true, data: permissions } as ApiResponse);
  } catch (error) {
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Berechtigungen',
    } as ApiResponse);
  }
}
