package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;

import java.time.Duration;
import java.time.LocalDate;
import java.util.Objects;
import java.util.Optional;

import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;

/**
 * Immutable view model for a single row in the processing-run result list.
 * <p>
 * Each completed candidate becomes exactly one row. The row carries only the information
 * that is shown in the list and the side panel; it is decoupled from the persistence
 * model so later GUI layers can render it without reaching back into the application
 * layer.
 * <p>
 * The {@code fingerprint} field is the content-based identity of the document and is
 * used as a stable key for in-place row updates during a targeted mini-run.
 * <p>
 * When {@code resetPending} is {@code true} the row represents a document whose
 * persistence status has been deleted but which has not yet been reprocessed. The status
 * icon and label reflect this special state instead of the original processing outcome.
 *
 * @param originalFileName   the source filename as reported by the use case; never
 *                           {@code null} or blank
 * @param fingerprint        the content-based identity of the processed document; never
 *                           {@code null}
 * @param status             the aggregated completion status; never {@code null}
 * @param finalFileName      the final target filename when the row represents a successful
 *                           rename; empty otherwise
 * @param correctedFileName  Der manuell korrigierte Zieldateiname, falls der Benutzer den
 *                           KI-Vorschlag in der GUI bearbeitet und gespeichert hat.
 *                           Leer bei unverändertem KI-Vorschlag.
 * @param resolvedDate       the resolved document date when the row represents a successful
 *                           rename; empty otherwise
 * @param aiReasoning        the AI reasoning shown in the side panel; empty when no
 *                           reasoning is available for this row
 * @param aiFailureMessage   eine lesbare Fehlerbeschreibung, wenn der KI-Aufruf oder die
 *                           Verarbeitung fehlgeschlagen ist; leer bei Erfolg und
 *                           übersprungenen Dokumenten
 * @param processingDuration wall-clock duration spent on the candidate in this run;
 *                           never {@code null} and never negative
 * @param resetPending       {@code true} when the document's persistence status has been
 *                           reset and is awaiting the next processing run
 * @param historicalContext  historischer Verarbeitungskontext für übersprungene Dokumente;
 *                           leer bei nicht-übersprungenen Zeilen
 */
public record GuiBatchRunResultRow(
        String originalFileName,
        DocumentFingerprint fingerprint,
        DocumentCompletionStatus status,
        Optional<String> finalFileName,
        Optional<String> correctedFileName,
        Optional<LocalDate> resolvedDate,
        Optional<String> aiReasoning,
        Optional<String> aiFailureMessage,
        Duration processingDuration,
        boolean resetPending,
        Optional<HistoricalDocumentContext> historicalContext) {

    /**
     * Label shown in the status column when a document's persistence status has been
     * reset and is waiting for the next processing run.
     */
    static final String RESET_PENDING_LABEL = "Zurückgesetzt – wartet auf nächsten Lauf";

    /**
     * Icon shown in the status column when a document's persistence status has been reset.
     */
    static final String RESET_PENDING_ICON = "⟳"; // ⟳ CLOCKWISE GAPPED CIRCLE ARROW

    /**
     * Compact constructor normalising optional holders and validating mandatory fields.
     *
     * @throws NullPointerException     if {@code originalFileName}, {@code fingerprint},
     *                                  {@code status} or {@code processingDuration} is
     *                                  {@code null}
     * @throws IllegalArgumentException if {@code originalFileName} is blank or
     *                                  {@code processingDuration} is negative
     */
    public GuiBatchRunResultRow {
        Objects.requireNonNull(originalFileName, "originalFileName must not be null");
        if (originalFileName.isBlank()) {
            throw new IllegalArgumentException("originalFileName must not be blank");
        }
        Objects.requireNonNull(fingerprint, "fingerprint must not be null");
        Objects.requireNonNull(status, "status must not be null");
        finalFileName = Objects.requireNonNullElse(finalFileName, Optional.empty());
        correctedFileName = Objects.requireNonNullElse(correctedFileName, Optional.empty());
        resolvedDate = Objects.requireNonNullElse(resolvedDate, Optional.empty());
        aiReasoning = Objects.requireNonNullElse(aiReasoning, Optional.empty());
        aiFailureMessage = Objects.requireNonNullElse(aiFailureMessage, Optional.empty());
        Objects.requireNonNull(processingDuration, "processingDuration must not be null");
        if (processingDuration.isNegative()) {
            throw new IllegalArgumentException("processingDuration must not be negative");
        }
        historicalContext = Objects.requireNonNullElse(historicalContext, Optional.empty());
    }

    /**
     * Bequem-Konstruktor für Zeilen, die weder einen manuell korrigierten Dateinamen
     * tragen noch im reset-pending-Zustand stehen und keinen historischen Kontext haben.
     *
     * @param originalFileName   the source filename; never {@code null} or blank
     * @param fingerprint        the content-based document identity; never {@code null}
     * @param status             the aggregated completion status; never {@code null}
     * @param finalFileName      the final target filename; may be {@code null} (treated as
     *                           empty)
     * @param resolvedDate       the resolved document date; may be {@code null} (treated as
     *                           empty)
     * @param aiReasoning        the AI reasoning text; may be {@code null} (treated as
     *                           empty)
     * @param aiFailureMessage   eine lesbare Fehlerbeschreibung bei Fehler; may be
     *                           {@code null} (treated as empty)
     * @param processingDuration the wall-clock processing duration; never {@code null}
     */
    public GuiBatchRunResultRow(
            String originalFileName,
            DocumentFingerprint fingerprint,
            DocumentCompletionStatus status,
            Optional<String> finalFileName,
            Optional<LocalDate> resolvedDate,
            Optional<String> aiReasoning,
            Optional<String> aiFailureMessage,
            Duration processingDuration) {
        this(originalFileName, fingerprint, status, finalFileName, Optional.empty(),
                resolvedDate, aiReasoning, aiFailureMessage, processingDuration, false,
                Optional.empty());
    }

    /**
     * Bequem-Konstruktor mit explizitem {@code resetPending}-Flag, aber ohne manuell
     * korrigierten Dateinamen und ohne historischen Kontext.
     *
     * @param originalFileName   the source filename; never {@code null} or blank
     * @param fingerprint        the content-based document identity; never {@code null}
     * @param status             the aggregated completion status; never {@code null}
     * @param finalFileName      the final target filename; may be {@code null} (treated as
     *                           empty)
     * @param resolvedDate       the resolved document date; may be {@code null} (treated as
     *                           empty)
     * @param aiReasoning        the AI reasoning text; may be {@code null} (treated as
     *                           empty)
     * @param aiFailureMessage   eine lesbare Fehlerbeschreibung bei Fehler; may be
     *                           {@code null} (treated as empty)
     * @param processingDuration the wall-clock processing duration; never {@code null}
     * @param resetPending       {@code true} wenn der Stammsatz zurückgesetzt wurde
     */
    public GuiBatchRunResultRow(
            String originalFileName,
            DocumentFingerprint fingerprint,
            DocumentCompletionStatus status,
            Optional<String> finalFileName,
            Optional<LocalDate> resolvedDate,
            Optional<String> aiReasoning,
            Optional<String> aiFailureMessage,
            Duration processingDuration,
            boolean resetPending) {
        this(originalFileName, fingerprint, status, finalFileName, Optional.empty(),
                resolvedDate, aiReasoning, aiFailureMessage, processingDuration, resetPending,
                Optional.empty());
    }

    /**
     * Creates a reset-pending copy of the supplied row, preserving the original filename
     * and fingerprint while marking the row as awaiting the next processing run.
     * <p>
     * The returned row has {@code resetPending == true}. Its {@code statusIcon()} and
     * {@code statusLabel()} reflect the reset state.
     *
     * @param previousRow the row to copy; must not be {@code null}
     * @return a new row with the same filename and fingerprint, {@code resetPending == true}
     * @throws NullPointerException if {@code previousRow} is {@code null}
     */
    public static GuiBatchRunResultRow resetMarker(GuiBatchRunResultRow previousRow) {
        Objects.requireNonNull(previousRow, "previousRow must not be null");
        return new GuiBatchRunResultRow(
                previousRow.originalFileName(),
                previousRow.fingerprint(),
                previousRow.status(),
                Optional.empty(),
                Optional.empty(),
                Optional.empty(),
                Optional.empty(),
                Optional.empty(),
                Duration.ZERO,
                true,
                Optional.empty());
    }

    /**
     * Gibt das Status-Icon für diese Zeile als Unicode-Zeichen zurück, das in JavaFX
     * unter Windows zuverlässig dargestellt wird (16px, bold).
     * <p>
     * Wenn {@code resetPending} den Wert {@code true} hat, wird unabhängig vom
     * eigentlichen Status das Reset-Icon zurückgegeben.
     * <p>
     * Die Icon-Werte stammen aus {@link ProcessingStatusPresentation}.
     *
     * @return das entsprechende Status-Zeichen
     */
    public String statusIcon() {
        if (resetPending) {
            return RESET_PENDING_ICON;
        }
        return ProcessingStatusPresentation.iconFor(status);
    }

    /**
     * Gibt die CSS-Farbe für das Status-Icon dieser Zeile zurück.
     * <p>
     * Wenn {@code resetPending} den Wert {@code true} hat, wird unabhängig vom
     * eigentlichen Status die Reset-Farbe zurückgegeben.
     * <p>
     * Farbe ist niemals das einzige Unterscheidungsmerkmal – {@link #statusIcon()} und
     * {@link #statusTooltip()} beschreiben den Status auch ohne Farbwahrnehmung eindeutig.
     * Die Farbwerte stammen aus {@link ProcessingStatusPresentation}.
     *
     * @return die entsprechende CSS-Hex-Farbe (z. B. {@code "#2e7d32"})
     */
    public String statusColor() {
        if (resetPending) {
            return "#757575"; // Grau für Reset-pending
        }
        return ProcessingStatusPresentation.cssColorFor(status);
    }

    /**
     * Gibt den deutschsprachigen Tooltip-Text für den Verarbeitungsstatus dieser Zeile zurück.
     * <p>
     * Wenn {@code resetPending} den Wert {@code true} hat, wird ein Tooltip für den
     * Reset-Zustand zurückgegeben.
     * <p>
     * Der Tooltip-Text beschreibt den Status vollständig ohne Farbe. Die Texte stammen
     * aus {@link ProcessingStatusPresentation}.
     *
     * @return der Tooltip-Text; nie leer
     */
    public String statusTooltip() {
        if (resetPending) {
            return RESET_PENDING_LABEL;
        }
        return ProcessingStatusPresentation.tooltipFor(status);
    }

    /**
     * Returns the human-readable status label for this row.
     * <p>
     * When {@code resetPending} is {@code true} the reset-pending label is returned
     * regardless of the underlying status.
     *
     * @return a non-null German status label
     */
    public String statusLabel() {
        if (resetPending) {
            return RESET_PENDING_LABEL;
        }
        return switch (status) {
            case SUCCESS -> "Erfolgreich";
            case FAILED_RETRYABLE -> "Fehlgeschlagen (wiederholbar)";
            case FAILED_PERMANENT -> "Fehlgeschlagen (dauerhaft)";
            case SKIPPED_ALREADY_PROCESSED -> "Übersprungen (bereits verarbeitet)";
            case SKIPPED_FINAL_FAILURE -> "Übersprungen (endgültig fehlgeschlagen)";
        };
    }

    /**
     * Liefert den aktuell wirksamen Zieldateinamen: falls der Benutzer den KI-Vorschlag
     * manuell korrigiert und gespeichert hat, wird der korrigierte Name geliefert,
     * ansonsten der ursprüngliche KI-Vorschlag {@link #finalFileName()}.
     * <p>
     * Die Tabellenspalte „Neuer Dateiname" bindet an diesen Wert.
     *
     * @return den aktuell anzuzeigenden Zieldateinamen; leer wenn kein Name vorliegt
     */
    public Optional<String> effectiveFileName() {
        if (correctedFileName.isPresent()) {
            return correctedFileName;
        }
        return finalFileName;
    }
}
