import { AuditAction, AuditSensitivity, Prisma } from '@prisma/client';
import crypto from 'crypto';
import { encrypt, decrypt } from '../utils/encryption.js';
import prisma from '../lib/prisma.js';

/**
 * Vereinfachte Audit-Log-Funktion für gezielte Änderungsprotokolle.
 * Wird direkt in Controllern aufgerufen mit aussagekräftigen Details.
 */
export async function logChange(opts: {
  req: any;                    // Express Request (für userId, email, IP)
  action: AuditAction;
  resourceType: string;
  resourceId?: string;
  label: string;               // Menschenlesbares Label z.B. "Vollmacht für Stefan Hacker widerrufen"
  details?: Record<string, unknown>;  // Zusätzliche Details z.B. { vorher: 'erteilt', nachher: 'widerrufen' }
  customerId?: number;
}) {
  try {
    const user = opts.req?.user;
    await createAuditLog({
      userId: user?.userId,
      userEmail: user?.email || 'system',
      userRole: user?.isCustomerPortal ? 'Kundenportal' : 'Mitarbeiter',
      customerId: user?.customerId,
      isCustomerPortal: user?.isCustomerPortal,
      action: opts.action,
      resourceType: opts.resourceType,
      resourceId: opts.resourceId,
      resourceLabel: opts.label,
      endpoint: opts.req?.path || '',
      httpMethod: opts.req?.method || '',
      ipAddress: opts.req?.socket?.remoteAddress || opts.req?.headers?.['x-forwarded-for'] || 'unknown',
      dataSubjectId: opts.customerId,
      changesAfter: opts.details,
    });
  } catch (error) {
    console.error('[logChange] Fehler:', error);
  }
}

export interface CreateAuditLogData {
  userId?: number;
  userEmail: string;
  userRole?: string;
  customerId?: number;
  isCustomerPortal?: boolean;
  action: AuditAction;
  sensitivity?: AuditSensitivity;
  resourceType: string;
  resourceId?: string;
  resourceLabel?: string;
  endpoint: string;
  httpMethod: string;
  ipAddress: string;
  userAgent?: string;
  changesBefore?: Record<string, unknown>;
  changesAfter?: Record<string, unknown>;
  dataSubjectId?: number;
  legalBasis?: string;
  success?: boolean;
  errorMessage?: string;
  durationMs?: number;
}

export interface AuditLogSearchParams {
  userId?: number;
  customerId?: number;
  dataSubjectId?: number;
  action?: AuditAction;
  sensitivity?: AuditSensitivity;
  resourceType?: string;
  resourceId?: string;
  startDate?: Date;
  endDate?: Date;
  success?: boolean;
  search?: string;
  page?: number;
  limit?: number;
}

/**
 * Generiert einen SHA-256 Hash für einen Audit-Log-Eintrag
 */
function generateHash(data: {
  userEmail: string;
  action: AuditAction;
  resourceType: string;
  resourceId?: string | null;
  endpoint: string;
  createdAt: Date;
  previousHash?: string | null;
}): string {
  // WICHTIG – Pentest R121 (Verify-Fehlalarm, ~73% "manipuliert"):
  // Beim Schreiben war `resourceId` bei middleware-generierten Einträgen
  // oft `undefined`. JSON.stringify LÄSST einen undefined-Wert weg – der
  // Hash wurde also OHNE `resourceId`-Key gebildet. In der DB landet der
  // Wert aber als `NULL`. verifyIntegrity/rehashAll lasen ihn als `null`
  // zurück, und `JSON.stringify({resourceId: null})` schreibt
  // `"resourceId":null` REIN → anderer Hash → falscher Tamper-Alarm für
  // jede Zeile mit leerem resourceId.
  //
  // Fix: nullish resourceId weglassen – reproduziert exakt das historische
  // Schreibverhalten (undefined → Key weg). Kein Caller hat je `null`
  // explizit übergeben (verifiziert), daher matchen ALLE Bestands-Hashes
  // ohne Rehash. Feldreihenfolge bleibt identisch zur Alt-Serialisierung.
  const payload: Record<string, unknown> = {
    userEmail: data.userEmail,
    action: data.action,
    resourceType: data.resourceType,
  };
  if (data.resourceId !== null && data.resourceId !== undefined) {
    payload.resourceId = data.resourceId;
  }
  payload.endpoint = data.endpoint;
  payload.createdAt = data.createdAt.toISOString();
  payload.previousHash = data.previousHash || '';

  return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
}

/**
 * Bestimmt die Sensitivität basierend auf dem Ressourcentyp
 */
function determineSensitivity(resourceType: string): AuditSensitivity {
  const sensitivityMap: Record<string, AuditSensitivity> = {
    // CRITICAL
    Authentication: 'CRITICAL',
    BankCard: 'CRITICAL',
    IdentityDocument: 'CRITICAL',
    // Klartext-Passwort-Reads – jeder Decrypt-Vorgang muss nachvollziehbar sein
    PortalPassword: 'CRITICAL',
    ContractPassword: 'CRITICAL',
    SimCardCredentials: 'CRITICAL',
    InternetCredentials: 'CRITICAL',
    SipCredentials: 'CRITICAL',
    MailboxCredentials: 'CRITICAL',
    // HIGH
    Customer: 'HIGH',
    User: 'HIGH',
    CustomerConsent: 'HIGH',
    DataDeletionRequest: 'HIGH',
    // Abgewehrter IDOR-/Cross-Boundary-Zugriffsversuch (canAccess*-403).
    // HIGH, weil ein Treffer auf gezielte Fremddaten-Enumeration hindeutet.
    AccessDenied: 'HIGH',
    // MEDIUM
    Contract: 'MEDIUM',
    Address: 'MEDIUM',
    Meter: 'MEDIUM',
    MeterReading: 'MEDIUM',
    StressfreiEmail: 'MEDIUM',
    CachedEmail: 'MEDIUM',
    // LOW
    Provider: 'LOW',
    Tariff: 'LOW',
    SalesPlatform: 'LOW',
    AppSetting: 'LOW',
    ContractCategory: 'LOW',
    CancellationPeriod: 'LOW',
    ContractDuration: 'LOW',
  };
  return sensitivityMap[resourceType] || 'MEDIUM';
}

/**
 * Prüft ob Änderungen verschlüsselt werden sollen.
 * Deaktiviert - sensible Felder werden bereits von der Prisma-Middleware als [REDACTED] gefiltert.
 */
function shouldEncryptChanges(_resourceType: string): boolean {
  return false;
}

/**
 * Erstellt einen neuen Audit-Log-Eintrag mit Hash-Kette
 */
export async function createAuditLog(data: CreateAuditLogData): Promise<void> {
  try {
    // Letzten Hash abrufen für die Kette
    const lastLog = await prisma.auditLog.findFirst({
      orderBy: { id: 'desc' },
      select: { hash: true },
    });

    const previousHash = lastLog?.hash || null;
    const createdAt = new Date();

    // Sensitivität bestimmen falls nicht angegeben
    const sensitivity = data.sensitivity || determineSensitivity(data.resourceType);

    // Änderungen serialisieren und ggf. verschlüsseln
    let changesBefore: string | null = null;
    let changesAfter: string | null = null;
    let changesEncrypted = false;

    if (data.changesBefore || data.changesAfter) {
      changesEncrypted = shouldEncryptChanges(data.resourceType);

      if (data.changesBefore) {
        const json = JSON.stringify(data.changesBefore);
        changesBefore = changesEncrypted ? encrypt(json) : json;
      }

      if (data.changesAfter) {
        const json = JSON.stringify(data.changesAfter);
        changesAfter = changesEncrypted ? encrypt(json) : json;
      }
    }

    // Hash generieren
    const hash = generateHash({
      userEmail: data.userEmail,
      action: data.action,
      resourceType: data.resourceType,
      resourceId: data.resourceId,
      endpoint: data.endpoint,
      createdAt,
      previousHash,
    });

    // Eintrag erstellen
    await prisma.auditLog.create({
      data: {
        userId: data.userId,
        userEmail: data.userEmail,
        userRole: data.userRole,
        customerId: data.customerId,
        isCustomerPortal: data.isCustomerPortal || false,
        action: data.action,
        sensitivity,
        resourceType: data.resourceType,
        resourceId: data.resourceId,
        resourceLabel: data.resourceLabel,
        endpoint: data.endpoint,
        httpMethod: data.httpMethod,
        ipAddress: data.ipAddress,
        userAgent: data.userAgent,
        changesBefore,
        changesAfter,
        changesEncrypted,
        dataSubjectId: data.dataSubjectId,
        legalBasis: data.legalBasis,
        success: data.success ?? true,
        errorMessage: data.errorMessage,
        durationMs: data.durationMs,
        createdAt,
        hash,
        previousHash,
      },
    });
  } catch (error) {
    // Audit-Logging darf niemals die Hauptoperation blockieren
    console.error('[AuditService] Fehler beim Erstellen des Audit-Logs:', error);
  }
}

/**
 * Sucht Audit-Logs mit Filtern und Paginierung
 */
export async function searchAuditLogs(params: AuditLogSearchParams) {
  const {
    userId,
    customerId,
    dataSubjectId,
    action,
    sensitivity,
    resourceType,
    resourceId,
    startDate,
    endDate,
    success,
    search,
    page = 1,
    limit = 50,
  } = params;

  const where: Prisma.AuditLogWhereInput = {};

  if (userId !== undefined) where.userId = userId;
  if (customerId !== undefined) where.customerId = customerId;
  if (dataSubjectId !== undefined) where.dataSubjectId = dataSubjectId;
  if (action) where.action = action;
  if (sensitivity) where.sensitivity = sensitivity;
  if (resourceType) where.resourceType = resourceType;
  if (resourceId) where.resourceId = resourceId;
  if (success !== undefined) where.success = success;

  if (startDate || endDate) {
    where.createdAt = {};
    if (startDate) where.createdAt.gte = startDate;
    if (endDate) where.createdAt.lte = endDate;
  }

  if (search) {
    where.OR = [
      { userEmail: { contains: search } },
      { resourceLabel: { contains: search } },
      { endpoint: { contains: search } },
    ];
  }

  const [logs, total] = await Promise.all([
    prisma.auditLog.findMany({
      where,
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit,
    }),
    prisma.auditLog.count({ where }),
  ]);

  // Entschlüsselung der Änderungen wenn nötig
  const decryptedLogs = logs.map((log) => ({
    ...log,
    changesBefore: log.changesBefore && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesBefore))
      : log.changesBefore ? JSON.parse(log.changesBefore) : null,
    changesAfter: log.changesAfter && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesAfter))
      : log.changesAfter ? JSON.parse(log.changesAfter) : null,
  }));

  return {
    data: decryptedLogs,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  };
}

/**
 * Holt einen einzelnen Audit-Log-Eintrag
 */
export async function getAuditLogById(id: number) {
  const log = await prisma.auditLog.findUnique({
    where: { id },
  });

  if (!log) return null;

  return {
    ...log,
    changesBefore: log.changesBefore && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesBefore))
      : log.changesBefore ? JSON.parse(log.changesBefore) : null,
    changesAfter: log.changesAfter && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesAfter))
      : log.changesAfter ? JSON.parse(log.changesAfter) : null,
  };
}

/**
 * Holt alle Audit-Logs für eine betroffene Person (DSGVO)
 */
export async function getAuditLogsByDataSubject(customerId: number) {
  const logs = await prisma.auditLog.findMany({
    where: { dataSubjectId: customerId },
    orderBy: { createdAt: 'desc' },
  });

  return logs.map((log) => ({
    ...log,
    changesBefore: log.changesBefore && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesBefore))
      : log.changesBefore ? JSON.parse(log.changesBefore) : null,
    changesAfter: log.changesAfter && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesAfter))
      : log.changesAfter ? JSON.parse(log.changesAfter) : null,
  }));
}

/**
 * Verifiziert die Integrität der Hash-Kette
 */
export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
  valid: boolean;
  checkedCount: number;
  invalidEntries: number[];
}> {
  const where: Prisma.AuditLogWhereInput = {};

  if (fromId !== undefined) where.id = { gte: fromId };
  if (toId !== undefined) where.id = { ...(where.id as object || {}), lte: toId };

  const logs = await prisma.auditLog.findMany({
    where,
    orderBy: { id: 'asc' },
    select: {
      id: true,
      userEmail: true,
      action: true,
      resourceType: true,
      resourceId: true,
      endpoint: true,
      createdAt: true,
      hash: true,
      previousHash: true,
    },
  });

  const invalidEntries: number[] = [];

  for (let i = 0; i < logs.length; i++) {
    const log = logs[i];

    // Hash neu berechnen
    const expectedHash = generateHash({
      userEmail: log.userEmail,
      action: log.action,
      resourceType: log.resourceType,
      resourceId: log.resourceId,
      endpoint: log.endpoint,
      createdAt: log.createdAt,
      previousHash: log.previousHash,
    });

    // Prüfen ob Hash übereinstimmt
    if (log.hash !== expectedHash) {
      invalidEntries.push(log.id);
      continue;
    }

    // Prüfen ob previousHash mit dem Hash des vorherigen Eintrags übereinstimmt
    if (i > 0) {
      const previousLog = logs[i - 1];
      if (log.previousHash !== previousLog.hash) {
        invalidEntries.push(log.id);
      }
    }
  }

  return {
    valid: invalidEntries.length === 0,
    checkedCount: logs.length,
    invalidEntries,
  };
}

/**
 * Hash-Kette komplett neu berechnen (Reparatur)
 */
export async function rehashAll(): Promise<{ rehashedCount: number }> {
  const logs = await prisma.auditLog.findMany({
    orderBy: { id: 'asc' },
    select: {
      id: true,
      userEmail: true,
      action: true,
      resourceType: true,
      resourceId: true,
      endpoint: true,
      createdAt: true,
    },
  });

  let previousHash: string | null = null;
  let count = 0;

  for (const log of logs) {
    const hash = generateHash({
      userEmail: log.userEmail,
      action: log.action,
      resourceType: log.resourceType,
      resourceId: log.resourceId,
      endpoint: log.endpoint,
      createdAt: log.createdAt,
      previousHash,
    });

    await prisma.auditLog.update({
      where: { id: log.id },
      data: { hash, previousHash },
    });

    previousHash = hash;
    count++;
  }

  return { rehashedCount: count };
}

/**
 * Exportiert Audit-Logs als JSON oder CSV
 */
export async function exportAuditLogs(
  params: AuditLogSearchParams,
  format: 'json' | 'csv' = 'json'
): Promise<string> {
  // Alle Logs ohne Paginierung
  const result = await searchAuditLogs({ ...params, limit: 100000, page: 1 });
  const logs = result.data;

  if (format === 'json') {
    return JSON.stringify(logs, null, 2);
  }

  // CSV Export
  const headers = [
    'ID',
    'Zeitstempel',
    'Benutzer',
    'Aktion',
    'Ressource',
    'Ressource-ID',
    'Bezeichnung',
    'Endpoint',
    'IP-Adresse',
    'Erfolg',
    'Sensitivität',
  ];

  const rows = logs.map((log) => [
    log.id.toString(),
    log.createdAt.toISOString(),
    log.userEmail,
    log.action,
    log.resourceType,
    log.resourceId || '',
    log.resourceLabel || '',
    log.endpoint,
    log.ipAddress,
    log.success ? 'Ja' : 'Nein',
    log.sensitivity,
  ]);

  const csvContent = [
    headers.join(';'),
    ...rows.map((row) => row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(';')),
  ].join('\n');

  return csvContent;
}

/**
 * Löscht alte Audit-Logs basierend auf Retention-Policies
 * Hinweis: Diese Funktion sollte nur von einem autorisierten Admin-Prozess aufgerufen werden
 */
export async function runRetentionCleanup(): Promise<{
  deletedCount: number;
  policies: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }>;
}> {
  const policies = await prisma.auditRetentionPolicy.findMany({
    where: { isActive: true },
  });

  const results: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }> = [];
  let totalDeleted = 0;

  for (const policy of policies) {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - policy.retentionDays);

    const where: Prisma.AuditLogWhereInput = {
      createdAt: { lt: cutoffDate },
    };

    if (policy.resourceType !== '*') {
      where.resourceType = policy.resourceType;
    }

    if (policy.sensitivity) {
      where.sensitivity = policy.sensitivity;
    }

    const deleted = await prisma.auditLog.deleteMany({ where });

    results.push({
      resourceType: policy.resourceType,
      sensitivity: policy.sensitivity,
      deletedCount: deleted.count,
    });

    totalDeleted += deleted.count;
  }

  return {
    deletedCount: totalDeleted,
    policies: results,
  };
}

/**
 * Holt die Retention-Policies
 */
export async function getRetentionPolicies() {
  return prisma.auditRetentionPolicy.findMany({
    orderBy: [{ resourceType: 'asc' }, { sensitivity: 'asc' }],
  });
}

/**
 * Aktualisiert eine Retention-Policy
 */
export async function updateRetentionPolicy(
  id: number,
  data: { retentionDays?: number; description?: string; legalBasis?: string; isActive?: boolean }
) {
  return prisma.auditRetentionPolicy.update({
    where: { id },
    data,
  });
}
