package de.gecheckt.pdf.umbenenner.application.usecase;

import java.util.Objects;

import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameDocumentNotFound;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameFileSystemFailure;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameInvalidState;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameNoOpIdenticalTarget;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenamePersistenceFailure;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameRequest;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameResult;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameSourceFileMissing;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameSuccess;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.ExistingIdenticalTargetFile;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.port.out.ResolvedTargetFilename;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameFailureFileNotFound;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameFailureTargetExists;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenamePort;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameTechnicalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderTechnicalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;

/**
 * Standardimplementierung von {@link ManualFileRenameUseCase}.
 * <p>
 * Führt die manuelle Umbenennung einer Zieldatei als atomare Operation durch:
 * Entweder werden Dateisystem und Persistenz beide aktualisiert, oder beide
 * bleiben im vorherigen Zustand.
 * <p>
 * <strong>Ablauf:</strong>
 * <ol>
 *   <li>Dokument-Stammsatz aus dem Repository laden und Zustand prüfen.</li>
 *   <li>Prüfen, ob der gewünschte Name bereits dem aktuellen entspricht (No-Op).</li>
 *   <li>Eindeutigen Zieldateinamen über {@link TargetFolderPort} auflösen.</li>
 *   <li>Zieldatei im Dateisystem umbenennen via {@link TargetFileRenamePort}.</li>
 *   <li>Dokument-Stammsatz in der Persistenz aktualisieren.</li>
 *   <li>Bei Persistenzfehler: Best-Effort-Rollback der Dateisystem-Umbenennung.</li>
 * </ol>
 * <p>
 * Eine Umbenennung ist ausschließlich für Dokumente mit Status
 * {@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus#SUCCESS} zulässig,
 * die einen bekannten letzten Zieldateinamen und Zielpfad haben.
 */
public class DefaultManualFileRenameUseCase implements ManualFileRenameUseCase {

    private final DocumentRecordRepository repository;
    private final TargetFolderPort targetFolderPort;
    private final TargetFileRenamePort targetFileRenamePort;
    private final UnitOfWorkPort unitOfWorkPort;
    private final ClockPort clock;
    private final ProcessingLogger logger;

    /**
     * Erstellt den Use-Case mit allen erforderlichen Ports.
     *
     * @param repository           Repository zum Lesen und Schreiben des Dokument-Stammsatzes;
     *                             darf nicht null sein
     * @param targetFolderPort     Port zur Auflösung eindeutiger Zieldateinamen;
     *                             darf nicht null sein
     * @param targetFileRenamePort Port zum physischen Umbenennen einer Zieldatei;
     *                             darf nicht null sein
     * @param unitOfWorkPort       Port zur atomaren Persistenzaktualisierung;
     *                             darf nicht null sein
     * @param clock                Port zur Abfrage des aktuellen Zeitstempels;
     *                             darf nicht null sein
     * @param logger               für die Protokollierung von Betriebsereignissen;
     *                             darf nicht null sein
     * @throws NullPointerException wenn einer der Parameter null ist
     */
    public DefaultManualFileRenameUseCase(
            DocumentRecordRepository repository,
            TargetFolderPort targetFolderPort,
            TargetFileRenamePort targetFileRenamePort,
            UnitOfWorkPort unitOfWorkPort,
            ClockPort clock,
            ProcessingLogger logger) {
        this.repository = Objects.requireNonNull(repository, "repository must not be null");
        this.targetFolderPort = Objects.requireNonNull(targetFolderPort, "targetFolderPort must not be null");
        this.targetFileRenamePort = Objects.requireNonNull(targetFileRenamePort, "targetFileRenamePort must not be null");
        this.unitOfWorkPort = Objects.requireNonNull(unitOfWorkPort, "unitOfWorkPort must not be null");
        this.clock = Objects.requireNonNull(clock, "clock must not be null");
        this.logger = Objects.requireNonNull(logger, "logger must not be null");
    }

    /**
     * Benennt die Zieldatei eines erfolgreich verarbeiteten Dokuments manuell um.
     * <p>
     * Der Aufruf ist atomar: Entweder werden Dateisystem und Persistenz beide
     * aktualisiert, oder beide bleiben unverändert. Bei einem Persistenzfehler nach
     * erfolgreicher Dateisystem-Umbenennung wird die Umbenennung im Dateisystem im
     * Rahmen eines Best-Effort-Rollbacks rückgängig gemacht.
     *
     * @param request die Umbenennungsanfrage mit Fingerprint und gewünschtem Basisdateinamen;
     *                darf nicht null sein
     * @return das Ergebnis der Umbenennung; nie null
     * @throws NullPointerException wenn {@code request} null ist
     */
    @Override
    public ManualFileRenameResult rename(ManualFileRenameRequest request) {
        Objects.requireNonNull(request, "request must not be null");

        DocumentFingerprint fingerprint = request.fingerprint();
        String desiredFullName = request.desiredBaseFileName() + ".pdf";

        logger.info("Manuelle Umbenennung angefordert: Fingerprint={}, Zielname={}",
                fingerprint.sha256Hex(), desiredFullName);

        // Schritt 1: Dokument-Stammsatz laden und Zustand prüfen
        var lookupResult = repository.findByFingerprint(fingerprint);

        if (lookupResult instanceof DocumentTerminalFinalFailure) {
            logger.warn("Manuelle Umbenennung verweigert: Dokument hat terminalen Fehlerstatus. Fingerprint={}",
                    fingerprint.sha256Hex());
            return new ManualFileRenameInvalidState(
                    "Dokument ist final fehlgeschlagen und kann nicht umbenannt werden.");
        }

        if (!(lookupResult instanceof DocumentTerminalSuccess terminalSuccess)) {
            logger.warn("Manuelle Umbenennung verweigert: Dokument nicht gefunden oder nicht im Erfolgsstatus. Fingerprint={}",
                    fingerprint.sha256Hex());
            return new ManualFileRenameDocumentNotFound(
                    "Kein erfolgreich verarbeitetes Dokument mit dem angegebenen Fingerprint gefunden.");
        }

        DocumentRecord record = terminalSuccess.record();

        if (record.lastTargetFileName() == null || record.lastTargetPath() == null) {
            logger.warn("Manuelle Umbenennung verweigert: Kein Zieldateiname im Stammsatz vorhanden. Fingerprint={}",
                    fingerprint.sha256Hex());
            return new ManualFileRenameInvalidState(
                    "Dokument hat keinen gespeicherten Zieldateinamen und kann nicht umbenannt werden.");
        }

        String currentFileName = record.lastTargetFileName();

        // Schritt 2: Prüfen, ob der gewünschte Name bereits dem aktuellen entspricht
        if (desiredFullName.equals(currentFileName)) {
            logger.info("Manuelle Umbenennung: Kein Handlungsbedarf, Name ist bereits identisch. Fingerprint={}",
                    fingerprint.sha256Hex());
            return new ManualFileRenameNoOpIdenticalTarget(currentFileName);
        }

        // Schritt 3: Eindeutigen Zieldateinamen über TargetFolderPort auflösen
        var resolutionResult = targetFolderPort.resolveUniqueFilename(desiredFullName, fingerprint);

        if (resolutionResult instanceof ExistingIdenticalTargetFile identical) {
            logger.info("Manuelle Umbenennung: Identische Datei bereits im Zielordner vorhanden. Fingerprint={}",
                    fingerprint.sha256Hex());
            return new ManualFileRenameNoOpIdenticalTarget(identical.existingFilename());
        }

        if (resolutionResult instanceof TargetFolderTechnicalFailure folderFailure) {
            logger.warn("Manuelle Umbenennung fehlgeschlagen: Technischer Fehler beim Zielordner-Zugriff. Fingerprint={}, Ursache={}",
                    fingerprint.sha256Hex(), folderFailure.errorMessage());
            return new ManualFileRenameFileSystemFailure(
                    "Zielordner nicht zugänglich: " + folderFailure.errorMessage());
        }

        // resolutionResult ist jetzt ResolvedTargetFilename
        String appliedFileName = ((ResolvedTargetFilename) resolutionResult).resolvedFilename();

        // Schritt 4: Zieldatei im Dateisystem umbenennen
        var renameResult = targetFileRenamePort.rename(currentFileName, appliedFileName);

        if (renameResult instanceof TargetFileRenameFailureFileNotFound notFound) {
            logger.warn("Manuelle Umbenennung fehlgeschlagen: Bisherige Zieldatei nicht gefunden. Fingerprint={}, Datei={}",
                    fingerprint.sha256Hex(), notFound.oldFileName());
            return new ManualFileRenameSourceFileMissing(notFound.oldFileName());
        }

        if (renameResult instanceof TargetFileRenameFailureTargetExists targetExists) {
            logger.warn("Manuelle Umbenennung fehlgeschlagen: Zieldatei bereits vorhanden (defensiv). Fingerprint={}, Datei={}",
                    fingerprint.sha256Hex(), targetExists.newFileName());
            return new ManualFileRenameFileSystemFailure(
                    "Zieldatei bereits vorhanden: " + targetExists.newFileName());
        }

        if (renameResult instanceof TargetFileRenameTechnicalFailure technical) {
            logger.warn("Manuelle Umbenennung fehlgeschlagen: Technischer Dateisystemfehler. Fingerprint={}, Ursache={}",
                    fingerprint.sha256Hex(), technical.message());
            return new ManualFileRenameFileSystemFailure(technical.message());
        }

        // Schritt 5: Persistenz aktualisieren (renameResult ist jetzt TargetFileRenameSuccess)
        DocumentRecord updatedRecord = new DocumentRecord(
                record.fingerprint(),
                record.lastKnownSourceLocator(),
                record.lastKnownSourceFileName(),
                record.overallStatus(),
                record.failureCounters(),
                record.lastFailureInstant(),
                record.lastSuccessInstant(),
                record.createdAt(),
                clock.now(),
                record.lastTargetPath(),
                appliedFileName);

        try {
            unitOfWorkPort.executeInTransaction(tx -> tx.updateDocumentRecord(updatedRecord));
        } catch (RuntimeException persistenceException) {
            // Best-Effort-Rollback: Dateisystem-Umbenennung rückgängig machen
            String errorMessage = persistenceException.getMessage() != null
                    ? persistenceException.getMessage()
                    : persistenceException.getClass().getSimpleName();

            logger.warn("Manuelle Umbenennung: Persistenzfehler nach erfolgreicher Dateisystem-Umbenennung. " +
                    "Versuche Rollback. Fingerprint={}, Ursache={}", fingerprint.sha256Hex(), errorMessage);

            var rollbackResult = targetFileRenamePort.rename(appliedFileName, currentFileName);
            if (!(rollbackResult instanceof TargetFileRenameSuccess)) {
                logger.error("Rollback der Dateisystem-Umbenennung fehlgeschlagen: {} → {}. " +
                        "Dateisystem und Persistenz sind möglicherweise inkonsistent. Fingerprint={}",
                        appliedFileName, currentFileName, fingerprint.sha256Hex());
            }

            return new ManualFileRenamePersistenceFailure(
                    "Persistenzfehler nach Umbenennung: " + errorMessage);
        }

        boolean conflictSuffixApplied = !appliedFileName.equals(desiredFullName);

        logger.info("Manuelle Umbenennung erfolgreich: {} → {} (Suffix angewendet: {})",
                currentFileName, appliedFileName, conflictSuffixApplied);

        return new ManualFileRenameSuccess(currentFileName, appliedFileName, conflictSuffixApplied);
    }
}
