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

import java.util.Objects;

import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyDocumentNotFound;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyFileSystemFailure;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyInvalidState;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyNoOpIdenticalTarget;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyPersistenceFailure;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyRequest;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyResult;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopySuccess;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentKnownProcessable;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult;
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.DocumentUnknown;
import de.gecheckt.pdf.umbenenner.application.port.out.ExistingIdenticalTargetFile;
import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure;
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.TargetFileCopyPort;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopySuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopyTechnicalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFilenameResolutionResult;
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;
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;

/**
 * Standardimplementierung von {@link ManualFileCopyUseCase}.
 * <p>
 * Führt die manuelle Kopie der Quelldatei eines bislang nicht erfolgreich verarbeiteten
 * Dokuments ins Zielverzeichnis 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
 *       (Status muss verarbeitbar oder final fehlgeschlagen sein, nicht {@code SUCCESS}).</li>
 *   <li>Eindeutigen Zieldateinamen über {@link TargetFolderPort} auflösen.</li>
 *   <li>Wenn der Zielordner bereits eine Datei mit identischem Inhalt enthält, wird
 *       keine erneute Kopie geschrieben – der Stammsatz wird trotzdem konsistent
 *       auf {@code SUCCESS} gehoben.</li>
 *   <li>Andernfalls Quelldatei via {@link TargetFileCopyPort} unter dem aufgelösten
 *       Namen ins Zielverzeichnis kopieren.</li>
 *   <li>Dokument-Stammsatz in der Persistenz aktualisieren: Status auf
 *       {@link ProcessingStatus#SUCCESS}, {@code lastTargetPath} und
 *       {@code lastTargetFileName} setzen, {@code lastSuccessInstant} und
 *       {@code updatedAt} aktualisieren.</li>
 *   <li>Bei Persistenzfehler: Best-Effort-Rollback der geschriebenen Zieldatei.</li>
 * </ol>
 * <p>
 * Eine manuelle Kopie ist ausschließlich für Dokumente mit nicht-erfolgreichem
 * Status zulässig. Für Dokumente mit Status {@link ProcessingStatus#SUCCESS}
 * ist die manuelle Umbenennung der bestehenden Zieldatei vorgesehen.
 */
public class DefaultManualFileCopyUseCase implements ManualFileCopyUseCase {

    private final DocumentRecordRepository repository;
    private final TargetFolderPort targetFolderPort;
    private final TargetFileCopyPort targetFileCopyPort;
    private final UnitOfWorkPort unitOfWorkPort;
    private final ClockPort clock;
    private final ProcessingLogger logger;

    /** Ergebnis der Dokument-Stammsatz-Auflösung: entweder ein Record oder ein Fehler. */
    private record RecordLookupOutcome(DocumentRecord record, ManualFileCopyResult failure) {
        boolean hasFailed() { return failure != null; }
    }

    /** Ergebnis der Zieldateinamen-Auflösung: entweder Name + No-Op-Flag oder ein Fehler. */
    private record FilenameLookupOutcome(String appliedFileName, boolean noOpIdentical,
                                         ManualFileCopyResult failure) {
        boolean hasFailed() { return failure != null; }
    }

    /**
     * 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 sowie
     *                           Best-Effort-Aufräumen einer geschriebenen Zieldatei;
     *                           darf nicht null sein
     * @param targetFileCopyPort Port zum physischen Kopieren der Quelldatei in den
     *                           Zielordner; 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 DefaultManualFileCopyUseCase(
            DocumentRecordRepository repository,
            TargetFolderPort targetFolderPort,
            TargetFileCopyPort targetFileCopyPort,
            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.targetFileCopyPort = Objects.requireNonNull(targetFileCopyPort, "targetFileCopyPort 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");
    }

    /**
     * Kopiert die Quelldatei eines Dokuments mit benutzerdefiniertem Zieldateinamen ins
     * Zielverzeichnis und aktualisiert den Dokument-Stammsatz auf {@code SUCCESS}.
     * <p>
     * Der Aufruf ist atomar: Entweder werden Dateisystem und Persistenz beide
     * aktualisiert, oder beide bleiben unverändert. Bei einem Persistenzfehler nach
     * erfolgreicher Kopie wird die geschriebene Zieldatei im Rahmen eines Best-Effort-
     * Rollbacks wieder entfernt.
     *
     * @param request die Kopieranfrage mit Fingerprint und gewünschtem Basisdateinamen;
     *                darf nicht null sein
     * @return das Ergebnis der Kopieroperation; nie null
     * @throws NullPointerException wenn {@code request} null ist
     */
    @Override
    public ManualFileCopyResult copy(ManualFileCopyRequest request) {
        Objects.requireNonNull(request, "request must not be null");

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

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

        RecordLookupOutcome recordOutcome = loadAndValidateRecord(fingerprint);
        if (recordOutcome.hasFailed()) {
            return recordOutcome.failure();
        }
        DocumentRecord record = recordOutcome.record();

        FilenameLookupOutcome filenameOutcome = resolveTargetFilename(fingerprint, desiredFullName);
        if (filenameOutcome.hasFailed()) {
            return filenameOutcome.failure();
        }
        String appliedFileName = filenameOutcome.appliedFileName();
        boolean noOpIdentical = filenameOutcome.noOpIdentical();

        if (!noOpIdentical) {
            ManualFileCopyResult copyFailure = performFileCopy(fingerprint, record, appliedFileName);
            if (copyFailure != null) {
                return copyFailure;
            }
        }

        return persistAndBuildResult(fingerprint, record, appliedFileName, noOpIdentical, desiredFullName);
    }

    /**
     * Lädt den Dokument-Stammsatz und prüft, ob der aktuelle Status eine manuelle
     * Kopie erlaubt.
     *
     * @param fingerprint der Fingerprint des Dokuments
     * @return Outcome mit geladenem Record oder mit einem Fehler-Ergebnis
     */
    private RecordLookupOutcome loadAndValidateRecord(DocumentFingerprint fingerprint) {
        DocumentRecordLookupResult lookupResult = repository.findByFingerprint(fingerprint);

        if (lookupResult instanceof DocumentTerminalFinalFailure terminalFailure) {
            return new RecordLookupOutcome(terminalFailure.record(), null);
        } else if (lookupResult instanceof DocumentKnownProcessable known) {
            DocumentRecord record = known.record();
            if (record.overallStatus() == ProcessingStatus.SUCCESS) {
                logger.warn("Manuelle Dateikopie verweigert: Dokument bereits SUCCESS. Fingerprint={}",
                        fingerprint.sha256Hex());
                return new RecordLookupOutcome(null, new ManualFileCopyInvalidState(
                        "Dokument ist bereits erfolgreich verarbeitet. Bitte die Umbenennung der "
                                + "Zieldatei verwenden."));
            }
            return new RecordLookupOutcome(record, null);
        } else if (lookupResult instanceof DocumentTerminalSuccess) {
            logger.warn("Manuelle Dateikopie verweigert: Dokument bereits SUCCESS. Fingerprint={}",
                    fingerprint.sha256Hex());
            return new RecordLookupOutcome(null, new ManualFileCopyInvalidState(
                    "Dokument ist bereits erfolgreich verarbeitet. Bitte die Umbenennung der "
                            + "Zieldatei verwenden."));
        } else if (lookupResult instanceof DocumentUnknown) {
            logger.warn("Manuelle Dateikopie verweigert: Dokument unbekannt. Fingerprint={}",
                    fingerprint.sha256Hex());
            return new RecordLookupOutcome(null, new ManualFileCopyDocumentNotFound(
                    "Kein Dokument mit dem angegebenen Fingerprint gefunden."));
        } else if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) {
            logger.warn("Manuelle Dateikopie fehlgeschlagen: Lookup-Fehler. Fingerprint={}, Ursache={}",
                    fingerprint.sha256Hex(), failure.errorMessage());
            return new RecordLookupOutcome(null, new ManualFileCopyPersistenceFailure(
                    "Persistenzfehler beim Laden des Dokumentstammsatzes: " + failure.errorMessage()));
        }
        // Defensiv: nicht erreichbar dank sealed type, aber erforderlich für die Compiler-
        // Vollständigkeitsprüfung in älteren Werkzeugen.
        return new RecordLookupOutcome(null, new ManualFileCopyDocumentNotFound(
                "Unbekanntes Lookup-Ergebnis: " + lookupResult.getClass().getSimpleName()));
    }

    /**
     * Löst über {@link TargetFolderPort} einen eindeutigen Zieldateinamen auf.
     *
     * @param fingerprint     der Fingerprint des Dokuments
     * @param desiredFullName der gewünschte vollständige Dateiname
     * @return Outcome mit aufgelöstem Dateinamen und No-Op-Flag oder mit einem Fehler-Ergebnis
     */
    private FilenameLookupOutcome resolveTargetFilename(DocumentFingerprint fingerprint,
                                                         String desiredFullName) {
        TargetFilenameResolutionResult resolutionResult =
                targetFolderPort.resolveUniqueFilename(desiredFullName, fingerprint);

        if (resolutionResult instanceof ExistingIdenticalTargetFile identical) {
            logger.info("Manuelle Dateikopie: Identische Datei bereits im Zielordner vorhanden. Fingerprint={}",
                    fingerprint.sha256Hex());
            return new FilenameLookupOutcome(identical.existingFilename(), true, null);
        } else if (resolutionResult instanceof TargetFolderTechnicalFailure folderFailure) {
            logger.warn("Manuelle Dateikopie fehlgeschlagen: Zielordnerzugriff. Fingerprint={}, Ursache={}",
                    fingerprint.sha256Hex(), folderFailure.errorMessage());
            return new FilenameLookupOutcome(null, false, new ManualFileCopyFileSystemFailure(
                    "Zielordner nicht zugänglich: " + folderFailure.errorMessage()));
        } else if (resolutionResult instanceof ResolvedTargetFilename resolved) {
            return new FilenameLookupOutcome(resolved.resolvedFilename(), false, null);
        }
        return new FilenameLookupOutcome(null, false, new ManualFileCopyFileSystemFailure(
                "Unbekanntes Auflösungsergebnis: " + resolutionResult.getClass().getSimpleName()));
    }

    /**
     * Kopiert die Quelldatei physisch in den Zielordner.
     *
     * @param fingerprint     der Fingerprint des Dokuments (für Logging)
     * @param record          der aktuelle Dokument-Stammsatz
     * @param appliedFileName der aufgelöste Zieldateiname
     * @return ein Fehler-Ergebnis bei Misserfolg, {@code null} bei Erfolg
     */
    private ManualFileCopyResult performFileCopy(DocumentFingerprint fingerprint,
                                                  DocumentRecord record,
                                                  String appliedFileName) {
        var copyResult = targetFileCopyPort.copyToTarget(
                record.lastKnownSourceLocator(), appliedFileName);
        if (copyResult instanceof TargetFileCopyTechnicalFailure technicalFailure) {
            logger.warn("Manuelle Dateikopie fehlgeschlagen: Dateisystemfehler. Fingerprint={}, Ursache={}",
                    fingerprint.sha256Hex(), technicalFailure.errorMessage());
            return new ManualFileCopyFileSystemFailure(technicalFailure.errorMessage());
        }
        if (!(copyResult instanceof TargetFileCopySuccess)) {
            return new ManualFileCopyFileSystemFailure(
                    "Unbekanntes Kopier-Ergebnis: " + copyResult.getClass().getSimpleName());
        }
        return null;
    }

    /**
     * Aktualisiert den Dokument-Stammsatz in der Persistenz und gibt das finale
     * Operationsergebnis zurück. Bei einem Persistenzfehler nach erfolgter Zielkopie
     * wird ein Best-Effort-Rollback der neu geschriebenen Datei durchgeführt.
     *
     * @param fingerprint     der Fingerprint des Dokuments
     * @param record          der bisher gültige Dokument-Stammsatz
     * @param appliedFileName der tatsächlich verwendete Zieldateiname
     * @param noOpIdentical   true, wenn keine neue Kopie geschrieben wurde
     * @param desiredFullName der ursprünglich gewünschte Zieldateiname
     * @return das finale Operationsergebnis
     */
    private ManualFileCopyResult persistAndBuildResult(DocumentFingerprint fingerprint,
                                                        DocumentRecord record,
                                                        String appliedFileName,
                                                        boolean noOpIdentical,
                                                        String desiredFullName) {
        var now = clock.now();
        DocumentRecord updatedRecord = new DocumentRecord(
                record.fingerprint(),
                record.lastKnownSourceLocator(),
                record.lastKnownSourceFileName(),
                ProcessingStatus.SUCCESS,
                record.failureCounters(),
                record.lastFailureInstant(),
                now,
                record.createdAt(),
                now,
                targetFolderPort.getTargetFolderLocator(),
                appliedFileName);

        try {
            unitOfWorkPort.executeInTransaction(tx -> tx.updateDocumentRecord(updatedRecord));
        } catch (RuntimeException persistenceException) {
            String errorMessage = persistenceException.getMessage() != null
                    ? persistenceException.getMessage()
                    : persistenceException.getClass().getSimpleName();

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

            if (!noOpIdentical) {
                // Best-Effort-Rollback: nur die *neu* geschriebene Zieldatei entfernen,
                // niemals eine bereits zuvor vorhandene identische Datei.
                try {
                    targetFolderPort.tryDeleteTargetFile(appliedFileName);
                } catch (RuntimeException rollbackException) {
                    logger.error("Rollback der Zielkopie fehlgeschlagen: {}. "
                            + "Dateisystem und Persistenz sind möglicherweise inkonsistent. Fingerprint={}",
                            appliedFileName, fingerprint.sha256Hex());
                }
            }

            return new ManualFileCopyPersistenceFailure(
                    "Persistenzfehler nach Kopie: " + errorMessage);
        }

        if (noOpIdentical) {
            logger.info("Manuelle Dateikopie abgeschlossen ohne Schreibvorgang: identische Zieldatei {}.",
                    appliedFileName);
            return new ManualFileCopyNoOpIdenticalTarget(appliedFileName);
        }

        boolean conflictSuffixApplied = !appliedFileName.equals(desiredFullName);
        logger.info("Manuelle Dateikopie erfolgreich: {} (Suffix angewendet: {})",
                appliedFileName, conflictSuffixApplied);
        return new ManualFileCopySuccess(appliedFileName, conflictSuffixApplied);
    }
}
