/**
 * Scheduler für automatische Vertrags-Status-Übergänge.
 *
 * Einmal täglich um 02:00: alle Verträge mit status=ACTIVE und
 * endDate < heute werden auf EXPIRED umgestellt (+ Audit-Log).
 *
 * Läuft zusätzlich 60 Sekunden nach Server-Start als Catch-up falls
 * der Prozess zum 02:00-Slot neu gestartet wurde.
 */
import cron from 'node-cron';
import prisma from '../lib/prisma.js';
import { createAuditLog, logChange } from './audit.service.js';
import { ApiError } from '../utils/apiError.js';

async function runExpireCheck(): Promise<void> {
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  const expiring = await prisma.contract.findMany({
    where: {
      status: 'ACTIVE',
      endDate: { not: null, lt: today },
    },
    select: {
      id: true,
      contractNumber: true,
      customerId: true,
      endDate: true,
    },
  });

  if (expiring.length === 0) {
    console.log('[ContractStatusScheduler] Keine abgelaufenen Verträge.');
    return;
  }

  console.log(`[ContractStatusScheduler] ${expiring.length} Vertrag/Verträge auf EXPIRED setzen.`);

  for (const c of expiring) {
    try {
      await prisma.contract.update({
        where: { id: c.id },
        data: { status: 'EXPIRED' },
      });

      await createAuditLog({
        userEmail: 'system',
        userRole: 'System',
        action: 'UPDATE',
        resourceType: 'Contract',
        resourceId: c.id.toString(),
        resourceLabel: `Vertrag ${c.contractNumber} automatisch auf EXPIRED gesetzt (Laufzeit überschritten)`,
        endpoint: 'scheduler:contract-status',
        httpMethod: 'SYSTEM',
        ipAddress: 'localhost',
        dataSubjectId: c.customerId,
        changesBefore: { status: 'ACTIVE' },
        changesAfter: { status: 'EXPIRED', endDate: c.endDate?.toISOString() },
      });
    } catch (err) {
      console.error(`[ContractStatusScheduler] Fehler bei Vertrag #${c.id}:`, err);
    }
  }

  console.log('[ContractStatusScheduler] Fertig.');
}

export function startContractStatusScheduler(): void {
  // Täglich um 02:00 Uhr (Server-Zeit)
  cron.schedule('0 2 * * *', () => {
    runExpireCheck().catch((err) =>
      console.error('[ContractStatusScheduler] Daily run failed:', err),
    );
  });

  // Catch-up 60 Sekunden nach Start
  setTimeout(() => {
    runExpireCheck().catch((err) =>
      console.error('[ContractStatusScheduler] Catch-up run failed:', err),
    );
  }, 60_000);

  console.log('[ContractStatusScheduler] Gestartet – täglich um 02:00 + Catch-up nach 60s');
}

export { runExpireCheck };

/**
 * Pentest 55.4 (LOW, 2026-06-01): 5 parallele Lieferbestätigung-Requests
 * erzeugten 5 ContractDocuments. Application-Lock per (contractId,
 * documentType) verhindert das in der Praxis (single-instance) und bietet
 * für Cluster wenigstens eine deutliche Verzögerung gegen Spam-Sprays.
 *
 * Plus DB-Check „kürzlich angelegt": rejected, falls innerhalb der
 * letzten 10 s schon ein Eintrag mit gleichem Typ existiert. Schließt
 * den größten Teil des Race-Windows und unterscheidet Spam-Attacks von
 * legitimen Sekunden-später-Updates.
 */
const docCreateLocks = new Map<string, Promise<void>>();

export async function assertNoRecentDuplicateDocument(
  contractId: number,
  documentType: string,
): Promise<void> {
  const recent = await prisma.contractDocument.findFirst({
    where: {
      contractId,
      documentType,
      createdAt: { gte: new Date(Date.now() - 10_000) },
    },
    select: { id: true },
  });
  if (recent) {
    // Pentest 64.1: ApiError(400) statt generischem Error – Caller
    // mappt das auf 400 Bad Request statt pauschal 500.
    throw new ApiError(400, 'Ein Dokument dieses Typs wurde vor wenigen Sekunden bereits angelegt – bitte kurz warten und Seite neu laden.');
  }
}

export async function withContractDocumentLock<T>(
  contractId: number,
  documentType: string,
  fn: () => Promise<T>,
): Promise<T> {
  const key = `${contractId}|${documentType.trim().toLowerCase()}`;
  const previous = docCreateLocks.get(key);
  let release: () => void = () => {};
  const slot = new Promise<void>((resolve) => { release = resolve; });
  docCreateLocks.set(key, (previous ?? Promise.resolve()).then(() => slot));
  if (previous) await previous;
  try {
    await assertNoRecentDuplicateDocument(contractId, documentType);
    return await fn();
  } finally {
    release();
    // Map-Aufräumen: wenn niemand mehr in der Kette wartet
    if (docCreateLocks.get(key) === (previous ?? Promise.resolve()).then(() => slot)) {
      docCreateLocks.delete(key);
    }
  }
}

/**
 * Wird nach einem ContractDocument-Upload aufgerufen. Wenn der Typ eine
 * Lieferbestätigung ist:
 *   - Contract.status von DRAFT auf ACTIVE setzen (falls DRAFT)
 *   - Contract.startDate auf das Lieferdatum setzen:
 *       * Explizit eingegebenes deliveryDate → IMMER als Vertragsbeginn
 *         übernehmen, auch wenn schon ein Datum gesetzt war. Die
 *         Lieferbestätigung ist das maßgebliche tatsächliche Startdatum
 *         und korrigiert ein evtl. vorher geschätztes Beginndatum.
 *       * Kein deliveryDate angegeben → Fallback "heute", aber NUR wenn
 *         startDate noch leer ist. Ein bestehendes (echtes) Datum darf
 *         nicht versehentlich mit "heute" überschrieben werden.
 *
 * Schreibweise "Lieferbestätigung" stammt aus dem Frontend-Dropdown
 * (SaveAttachmentModal / ContractDetail). Vergleich case-insensitive +
 * getrimmt zur Robustheit.
 */
/**
 * Wird aufgerufen, wenn zu einem Vertrag eine Kündigungsbestätigung
 * hinzugefügt wird – entweder als Datum (`cancellationConfirmationDate`,
 * z.B. über das Vertragsformular) und/oder als Dokument
 * (`cancellationConfirmationPath`, Upload). Effekt:
 *   - Vertrag ACTIVE → CANCELLED (nur aus ACTIVE; andere Status werden
 *     bewusst nicht angetastet).
 *   - Berechnetes Vertragsende (`endDate`) = Kündigungs(bestätigungs)datum,
 *     sofern eines vorliegt.
 * Idempotent: läuft nur, wenn tatsächlich eine Bestätigung vorhanden ist,
 * und schreibt nur bei echten Änderungen.
 */
export async function maybeCancelOnCancellationConfirmation(
  contractId: number,
  req: unknown,
): Promise<void> {
  const c = await prisma.contract.findUnique({
    where: { id: contractId },
    select: {
      status: true,
      endDate: true,
      contractNumber: true,
      customerId: true,
      cancellationConfirmationPath: true,
      cancellationConfirmationDate: true,
    },
  });
  if (!c) return;

  const hasConfirmation = !!c.cancellationConfirmationPath || !!c.cancellationConfirmationDate;
  if (!hasConfirmation) return;

  const asDay = (d: Date | null | undefined) =>
    d ? new Date(d).toISOString().split('T')[0] : null;

  const updateData: Record<string, unknown> = {};
  const changes: Record<string, { vorher: unknown; nachher: unknown }> = {};

  // Kündigungsbestätigung liegt vor (Datum/Dokument) → "Gekündigt / bestätigt".
  // Von jedem noch "lebenden" Status heben – auch aus "Gekündigt / Bestätigung
  // abwarten" (CANCELLED) – aber NIE aus DRAFT (Vorlage) oder DEACTIVATED
  // (archiviert). Ein bereits bestätigter Vertrag bleibt bestätigt (No-op).
  if (['ACTIVE', 'PENDING', 'ONGOING', 'EXPIRED', 'CANCELLED'].includes(c.status)) {
    updateData.status = 'CANCELLED_CONFIRMED';
    changes.status = { vorher: c.status, nachher: 'CANCELLED_CONFIRMED' };
  }

  // Vertragsende = Kündigungsdatum (Bestätigungsdatum), falls vorhanden.
  // NICHT bei DRAFT: ein Entwurf ist nur eine Vorlage und bekommt kein
  // berechnetes Vertragsende (Pentest R138, Hygiene-Punkt).
  if (
    c.status !== 'DRAFT' &&
    c.cancellationConfirmationDate &&
    asDay(c.endDate) !== asDay(c.cancellationConfirmationDate)
  ) {
    updateData.endDate = c.cancellationConfirmationDate;
    changes.endDate = { vorher: asDay(c.endDate), nachher: asDay(c.cancellationConfirmationDate) };
  }

  if (Object.keys(updateData).length === 0) return;

  await prisma.contract.update({ where: { id: contractId }, data: updateData });

  await logChange({
    req,
    action: 'UPDATE',
    resourceType: 'Contract',
    resourceId: contractId.toString(),
    label: `Vertrag ${c.contractNumber} automatisch aktualisiert (Kündigungsbestätigung)`,
    details: { ...changes, trigger: 'Kündigungsbestätigung' },
    customerId: c.customerId,
  });
}

/**
 * Setzt einen Vertrag automatisch auf "Gekündigt / Bestätigung abwarten"
 * (CANCELLED), sobald ein Kündigungsschreiben hinterlegt wurde. Hebt nur noch
 * "lebende" Verträge (ACTIVE/PENDING/ONGOING/EXPIRED) an – ein bereits bestätigt
 * gekündigter Vertrag (CANCELLED_CONFIRMED) wird NICHT zurückgestuft, ein
 * Entwurf (DRAFT) oder archivierter (DEACTIVATED) nicht angefasst.
 */
export async function maybeMarkAwaitingConfirmationOnLetter(
  contractId: number,
  req: unknown,
): Promise<void> {
  const c = await prisma.contract.findUnique({
    where: { id: contractId },
    select: {
      status: true,
      contractNumber: true,
      customerId: true,
      cancellationLetterPath: true,
    },
  });
  if (!c || !c.cancellationLetterPath) return;
  if (!['ACTIVE', 'PENDING', 'ONGOING', 'EXPIRED'].includes(c.status)) return;

  await prisma.contract.update({
    where: { id: contractId },
    data: { status: 'CANCELLED' },
  });

  await logChange({
    req,
    action: 'UPDATE',
    resourceType: 'Contract',
    resourceId: contractId.toString(),
    label: `Vertrag ${c.contractNumber} automatisch auf "Gekündigt / Bestätigung abwarten" gesetzt (Kündigungsschreiben)`,
    details: { status: { vorher: c.status, nachher: 'CANCELLED' }, trigger: 'Kündigungsschreiben' },
    customerId: c.customerId,
  });
}

export async function maybeActivateOnDeliveryConfirmation(
  contractId: number,
  documentType: string,
  req: unknown,
  deliveryDate?: Date | string | null,
): Promise<void> {
  if (!documentType || typeof documentType !== 'string') return;
  if (documentType.trim().toLowerCase() !== 'lieferbestätigung') return;

  const contract = await prisma.contract.findUnique({
    where: { id: contractId },
    select: { status: true, contractNumber: true, customerId: true, startDate: true },
  });
  if (!contract) return;

  // Explizit eingegebenes Lieferdatum parsen (null = keins angegeben).
  let parsedDate: Date | null = null;
  if (deliveryDate) {
    const parsed = new Date(deliveryDate);
    if (!isNaN(parsed.getTime())) parsedDate = parsed;
  }

  const updateData: Record<string, unknown> = {};
  const changes: Record<string, { vorher: unknown; nachher: unknown }> = {};

  if (contract.status === 'DRAFT') {
    updateData.status = 'ACTIVE';
    changes.status = { vorher: 'DRAFT', nachher: 'ACTIVE' };
  }

  const asDay = (d: Date | null | undefined) =>
    d ? new Date(d).toISOString().split('T')[0] : null;

  if (parsedDate) {
    // Explizites Lieferdatum: als Vertragsbeginn übernehmen, auch überschreibend.
    // No-op vermeiden, wenn der Tag schon exakt passt.
    if (asDay(contract.startDate) !== asDay(parsedDate)) {
      updateData.startDate = parsedDate;
      changes.startDate = { vorher: asDay(contract.startDate), nachher: asDay(parsedDate) };
    }
  } else if (!contract.startDate) {
    // Kein Datum angegeben → Fallback heute, nur bei leerem Startdatum.
    const today = new Date();
    updateData.startDate = today;
    changes.startDate = { vorher: null, nachher: asDay(today) };
  }

  if (Object.keys(updateData).length === 0) return;

  await prisma.contract.update({
    where: { id: contractId },
    data: updateData,
  });

  await logChange({
    req,
    action: 'UPDATE',
    resourceType: 'Contract',
    resourceId: contractId.toString(),
    label: `Vertrag ${contract.contractNumber} automatisch aktualisiert (Lieferbestätigung hochgeladen)`,
    details: { ...changes, trigger: 'Lieferbestätigung-Upload' },
    customerId: contract.customerId,
  });
}
