/**
 * Access-Control-Helper für Portal-Kunden-Isolation.
 *
 * Portal-Kunden haben die Permission `contracts:read` / `customers:read`, damit
 * sie ihre eigenen Daten sehen können. Damit sie aber NICHT fremde Daten über
 * geratene IDs abrufen (IDOR), muss bei jedem Endpoint der eine sensible
 * Ressource (Vertrag, Rechnung, Passwort, ...) zurückliefert, der Kunde auf
 * Besitz/Vollmacht geprüft werden.
 */
import { Response } from 'express';
import prisma from '../lib/prisma.js';
import * as authorizationService from '../services/authorization.service.js';
import { AuthRequest } from '../types/index.js';
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
import { logChange } from '../services/audit.service.js';

/**
 * Wird intern aufgerufen, wenn ein canAccess*-Check 403 zurückgibt.
 *
 * Schreibt in ZWEI Streams:
 *  1. SecurityEvent (Monitoring): Alerting + Threshold-Detection, aber
 *     über /api/monitoring/events löschbar und nicht hash-verkettet.
 *  2. AuditLog (tamper-evident): hash-verketteter Eintrag, überlebt ein
 *     Clearen des Monitoring-Streams. Pentest R120 hat aufgedeckt, dass
 *     der reine SecurityEvent-Stream für Forensik „lautlos genug" wirkt –
 *     ein Cross-Customer-Zugriffsversuch (IDOR) soll manipulationssicher
 *     nachweisbar sein. `action: READ`, weil es ein unautorisierter
 *     Lese-VERSUCH ist (kein eigener Enum-Wert nötig); der distinktive
 *     resourceType `AccessDenied` macht es filterbar
 *     (`searchAuditLogs({ resourceType: 'AccessDenied' })`).
 */
function emitAccessDenied(req: AuthRequest, label: string, targetId: number | string): void {
  const ctx = contextFromRequest(req);
  emitSecurityEvent({
    type: 'ACCESS_DENIED',
    severity: 'MEDIUM',
    message: `Zugriff verweigert: ${label} #${targetId}`,
    ipAddress: ctx.ipAddress,
    userId: ctx.userId,
    customerId: ctx.customerId,
    userEmail: ctx.userEmail,
    endpoint: ctx.endpoint,
    details: { resource: label, targetId },
  });
  // Fire-and-forget; logChange fängt eigene Fehler intern ab und darf die
  // 403-Response nie blockieren.
  void logChange({
    req,
    action: 'READ',
    resourceType: 'AccessDenied',
    resourceId: String(targetId),
    label: `IDOR-Zugriffsversuch abgewehrt: ${label} #${targetId}`,
    details: { resource: label, targetId, endpoint: ctx.endpoint },
  });
}

/**
 * Prüft ob der authentifizierte User auf einen bestimmten Vertrag zugreifen darf.
 * - Mitarbeiter/Admin mit customers:read / contracts:read: ja, immer
 * - Portal-Kunde: nur wenn contract.customerId = eigener customerId ODER
 *   wenn er einen Vertreter für diesen Kunden ist MIT gültiger Vollmacht
 *
 * @returns true = erlaubt, false = Zugriff verweigert (Response wurde bereits gesendet)
 */
export async function canAccessContract(
  req: AuthRequest,
  res: Response,
  contractId: number,
): Promise<boolean> {
  // Nicht-Portal-User (Mitarbeiter/Admin) kommen hier immer durch, wenn sie die Permission haben
  if (!req.user?.isCustomerPortal) {
    return true;
  }

  if (!req.user.customerId) {
    res.status(403).json({ success: false, error: 'Kein Zugriff' });
    return false;
  }

  // Vertrag laden, Besitzer-ID prüfen
  const contract = await prisma.contract.findUnique({
    where: { id: contractId },
    select: { customerId: true },
  });

  if (!contract) {
    res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' });
    return false;
  }

  // Eigene Verträge = immer erlaubt
  if (contract.customerId === req.user.customerId) {
    return true;
  }

  // Fremde Verträge nur mit aktiver Vollmacht
  const representedIds: number[] = (req.user as any).representedCustomerIds || [];
  if (!representedIds.includes(contract.customerId)) {
    emitAccessDenied(req, 'Contract', contractId);
    res.status(403).json({ success: false, error: 'Kein Zugriff auf diesen Vertrag' });
    return false;
  }

  const hasAuth = await authorizationService.hasAuthorization(
    contract.customerId,
    req.user.customerId,
  );
  if (!hasAuth) {
    emitAccessDenied(req, 'Contract (Vollmacht fehlt)', contractId);
    res.status(403).json({ success: false, error: 'Vollmacht erforderlich' });
    return false;
  }

  return true;
}

/**
 * Prüft Zugriff auf einen Kunden (analog zu canAccessContract).
 */
export async function canAccessCustomer(
  req: AuthRequest,
  res: Response,
  customerId: number,
): Promise<boolean> {
  if (!req.user?.isCustomerPortal) {
    return true;
  }

  if (!req.user.customerId) {
    res.status(403).json({ success: false, error: 'Kein Zugriff' });
    return false;
  }

  if (customerId === req.user.customerId) {
    return true;
  }

  const representedIds: number[] = (req.user as any).representedCustomerIds || [];
  if (!representedIds.includes(customerId)) {
    emitAccessDenied(req, 'Customer', customerId);
    res.status(403).json({ success: false, error: 'Kein Zugriff auf diese Kundendaten' });
    return false;
  }

  const hasAuth = await authorizationService.hasAuthorization(customerId, req.user.customerId);
  if (!hasAuth) {
    emitAccessDenied(req, 'Customer (Vollmacht fehlt)', customerId);
    res.status(403).json({ success: false, error: 'Vollmacht erforderlich' });
    return false;
  }

  return true;
}

/**
 * Liefert die Liste aller Customer-IDs, auf die ein Portal-User aktuell
 * Zugriff hat: eigene + vertretene MIT aktiver Vollmacht (Live-Check via
 * `authorizationService.hasAuthorization`). Für Nicht-Portal-User wird
 * `null` zurückgegeben (= kein Filter, alle Kunden erlaubt).
 *
 * Diese Funktion fängt einen wiederkehrenden Pentest-Befund ab: ohne den
 * Live-Vollmacht-Check hätte ein Portal-User mit widerrufener Vollmacht
 * weiterhin Zugriff auf die Daten des vertretenen Kunden, nur weil seine
 * `representedCustomerIds` im JWT noch drin sind (Token kann bis zu
 * 15min alt sein).
 */
export async function getPortalAllowedCustomerIds(
  req: AuthRequest,
): Promise<number[] | null> {
  if (!req.user?.isCustomerPortal || !req.user.customerId) return null;
  const allowed: number[] = [req.user.customerId];
  const represented: number[] = (req.user as any).representedCustomerIds || [];
  for (const repCustId of represented) {
    const hasAuth = await authorizationService.hasAuthorization(
      repCustId,
      req.user.customerId,
    );
    if (hasAuth) allowed.push(repCustId);
  }
  return allowed;
}

/**
 * Generische Zugriffsprüfung: Ressource → customerId → canAccessCustomer.
 */
async function canAccessResourceByCustomerId(
  req: AuthRequest,
  res: Response,
  customerId: number | null | undefined,
  resourceLabel: string,
): Promise<boolean> {
  if (!req.user?.isCustomerPortal) return true;

  if (!customerId) {
    res.status(404).json({ success: false, error: `${resourceLabel} nicht gefunden` });
    return false;
  }
  return canAccessCustomer(req, res, customerId);
}

/**
 * Zugriff auf eine Adresse prüfen (lädt sie aus der DB, prüft customerId).
 */
export async function canAccessAddress(
  req: AuthRequest,
  res: Response,
  addressId: number,
): Promise<boolean> {
  if (!req.user?.isCustomerPortal) return true;
  const addr = await prisma.address.findUnique({
    where: { id: addressId },
    select: { customerId: true },
  });
  return canAccessResourceByCustomerId(req, res, addr?.customerId, 'Adresse');
}

/**
 * Zugriff auf eine BankCard prüfen.
 */
export async function canAccessBankCard(
  req: AuthRequest,
  res: Response,
  bankCardId: number,
): Promise<boolean> {
  if (!req.user?.isCustomerPortal) return true;
  const card = await prisma.bankCard.findUnique({
    where: { id: bankCardId },
    select: { customerId: true },
  });
  return canAccessResourceByCustomerId(req, res, card?.customerId, 'Bankkarte');
}

/**
 * Zugriff auf ein IdentityDocument prüfen.
 */
export async function canAccessIdentityDocument(
  req: AuthRequest,
  res: Response,
  documentId: number,
): Promise<boolean> {
  if (!req.user?.isCustomerPortal) return true;
  const doc = await prisma.identityDocument.findUnique({
    where: { id: documentId },
    select: { customerId: true },
  });
  return canAccessResourceByCustomerId(req, res, doc?.customerId, 'Ausweis');
}

/**
 * Zugriff auf einen Meter prüfen.
 */
export async function canAccessMeter(
  req: AuthRequest,
  res: Response,
  meterId: number,
): Promise<boolean> {
  if (!req.user?.isCustomerPortal) return true;
  const meter = await prisma.meter.findUnique({
    where: { id: meterId },
    select: { customerId: true },
  });
  return canAccessResourceByCustomerId(req, res, meter?.customerId, 'Zähler');
}

/**
 * Zugriff auf eine StressfreiEmail prüfen.
 */
export async function canAccessStressfreiEmail(
  req: AuthRequest,
  res: Response,
  stressfreiEmailId: number,
): Promise<boolean> {
  if (!req.user?.isCustomerPortal) return true;
  const sfe = await prisma.stressfreiEmail.findUnique({
    where: { id: stressfreiEmailId },
    select: { customerId: true },
  });
  return canAccessResourceByCustomerId(req, res, sfe?.customerId, 'E-Mail-Konto');
}

/**
 * Zugriff auf eine CachedEmail prüfen (StressfreiEmail → customerId).
 */
export async function canAccessCachedEmail(
  req: AuthRequest,
  res: Response,
  emailId: number,
): Promise<boolean> {
  if (!req.user?.isCustomerPortal) return true;
  const email = await prisma.cachedEmail.findUnique({
    where: { id: emailId },
    select: { stressfreiEmail: { select: { customerId: true } } },
  });
  return canAccessResourceByCustomerId(
    req,
    res,
    email?.stressfreiEmail?.customerId,
    'E-Mail',
  );
}

/**
 * Zugriff auf ein EnergyContractDetails prüfen (ECD → Contract → customerId).
 */
export async function canAccessEnergyContractDetails(
  req: AuthRequest,
  res: Response,
  ecdId: number,
): Promise<boolean> {
  if (!req.user?.isCustomerPortal) return true;
  const ecd = await prisma.energyContractDetails.findUnique({
    where: { id: ecdId },
    select: { contract: { select: { customerId: true } } },
  });
  return canAccessResourceByCustomerId(
    req,
    res,
    ecd?.contract?.customerId,
    'Energievertrag',
  );
}
