package de.gecheckt.pdf.umbenenner.application.port.out;

/**
 * Sealed type representing the complete, authoritative retry decision for a document
 * after an error has been classified.
 * <p>
 * A {@code RetryDecision} is the output of the retry policy evaluation. It unambiguously
 * encodes what must happen next for the document: which status to persist, which counter
 * to increment, and whether a within-run immediate retry is still possible.
 * <p>
 * <strong>Decision cases and their semantics:</strong>
 * <ol>
 *   <li>{@link ContentErrorRetryable} — first deterministic content error. Document moves
 *       to {@code FAILED_RETRYABLE}; content-error counter is incremented by 1. One later
 *       scheduler run may retry.</li>
 *   <li>{@link ContentErrorFinal} — second (or later) deterministic content error. Document
 *       moves to {@code FAILED_FINAL}; content-error counter is incremented by 1. No further
 *       processing in any future run.</li>
 *   <li>{@link TransientErrorRetryable} — transient technical error with remaining retry budget.
 *       Document moves to {@code FAILED_RETRYABLE}; transient-error counter is incremented by 1.
 *       A later scheduler run may retry, as long as the counter stays below
 *       {@code max.retries.transient}.</li>
 *   <li>{@link TransientErrorFinal} — transient technical error that exhausts the configured
 *       {@code max.retries.transient} budget. Document moves to {@code FAILED_FINAL};
 *       transient-error counter is incremented by 1. No further processing in any future run.</li>
 *   <li>{@link TargetCopyWithImmediateRetry} — first physical copy failure within the current
 *       run. The document has not yet changed status; exactly one immediate within-run retry
 *       of the copy step is permitted. No new AI call and no new naming-proposal derivation
 *       occur. This decision does not yet modify any counter or status; the outcome of the
 *       immediate retry determines which subsequent decision applies.</li>
 * </ol>
 * <p>
 * <strong>What this type does NOT cover:</strong>
 * <ul>
 *   <li>Skip decisions ({@code SKIPPED_ALREADY_PROCESSED}, {@code SKIPPED_FINAL_FAILURE})
 *       — skips are not retry decisions; they are pure historisation events.</li>
 *   <li>Success — a successful outcome is not a retry decision.</li>
 *   <li>Pre-fingerprint failures — errors before the fingerprint is computed are not
 *       historised as attempts and therefore do not produce a {@code RetryDecision}.</li>
 * </ul>
 * <p>
 * <strong>Counter invariant:</strong> Skip decisions ({@code SKIPPED_ALREADY_PROCESSED},
 * {@code SKIPPED_FINAL_FAILURE}) never produce a {@code RetryDecision} and never change
 * any failure counter.
 * <p>
 * <strong>Single-truth rule:</strong> The retry decision is derived exclusively from the
 * document master record and the attempt history. No additional, parallel truth source
 * for retry state is introduced.
 */
public sealed interface RetryDecision {

    /**
     * Returns the failure class identifier for persistence and logging.
     * <p>
     * The failure class is a short, stable string identifying the type of failure,
     * typically the enum constant name of the original error or exception class name.
     *
     * @return failure class string; never {@code null} or blank
     */
    String failureClass();

    /**
     * Returns a human-readable failure message for persistence and logging.
     *
     * @return failure message; never {@code null} or blank
     */
    String failureMessage();

    // -------------------------------------------------------------------------
    // Deterministic content error cases
    // -------------------------------------------------------------------------

    /**
     * First historised deterministic content error for this fingerprint.
     * <p>
     * The document must be persisted with status {@code FAILED_RETRYABLE} and the
     * content-error counter incremented by 1. Exactly one later scheduler run is
     * permitted to retry.
     *
     * @param failureClass   failure class identifier; never {@code null} or blank
     * @param failureMessage human-readable failure description; never {@code null} or blank
     */
    record ContentErrorRetryable(String failureClass, String failureMessage)
            implements RetryDecision {}

    /**
     * Second (or subsequent) historised deterministic content error for this fingerprint.
     * <p>
     * The document must be persisted with status {@code FAILED_FINAL} and the
     * content-error counter incremented by 1. No further processing is allowed in
     * any future run.
     *
     * @param failureClass   failure class identifier; never {@code null} or blank
     * @param failureMessage human-readable failure description; never {@code null} or blank
     */
    record ContentErrorFinal(String failureClass, String failureMessage)
            implements RetryDecision {}

    // -------------------------------------------------------------------------
    // Transient technical error cases
    // -------------------------------------------------------------------------

    /**
     * Transient technical error with remaining retry budget.
     * <p>
     * The transient-error counter after incrementing is strictly less than
     * {@code max.retries.transient}. The document must be persisted with status
     * {@code FAILED_RETRYABLE} and the transient-error counter incremented by 1.
     * A later scheduler run may retry.
     *
     * @param failureClass   failure class identifier; never {@code null} or blank
     * @param failureMessage human-readable failure description; never {@code null} or blank
     */
    record TransientErrorRetryable(String failureClass, String failureMessage)
            implements RetryDecision {}

    /**
     * Transient technical error that exhausts the configured {@code max.retries.transient}
     * budget.
     * <p>
     * The transient-error counter after incrementing equals {@code max.retries.transient}.
     * The document must be persisted with status {@code FAILED_FINAL} and the
     * transient-error counter incremented by 1. No further processing is allowed in
     * any future run.
     * <p>
     * Example: with {@code max.retries.transient = 1}, the very first transient error
     * produces this decision immediately.
     *
     * @param failureClass   failure class identifier; never {@code null} or blank
     * @param failureMessage human-readable failure description; never {@code null} or blank
     */
    record TransientErrorFinal(String failureClass, String failureMessage)
            implements RetryDecision {}

    // -------------------------------------------------------------------------
    // Target copy immediate retry case
    // -------------------------------------------------------------------------

    /**
     * First physical target-file copy failure within the current run.
     * <p>
     * Exactly one immediate technical retry of the copy operation is permitted within
     * the same document run. This decision does not change any counter or document
     * status — it defers the final outcome until the immediate retry completes:
     * <ul>
     *   <li>If the immediate retry succeeds → document proceeds to {@code SUCCESS}.</li>
     *   <li>If the immediate retry also fails → the combined failure is classified as
     *       a transient technical error and a {@link TransientErrorRetryable} or
     *       {@link TransientErrorFinal} decision is produced for the final persistence
     *       step.</li>
     * </ul>
     * <p>
     * The immediate retry is strictly limited to the physical copy path. No new AI call
     * and no new naming-proposal derivation occur. This mechanism does not increment the
     * laufübergreifenden transient-error counter.
     *
     * @param failureMessage human-readable description of the initial copy failure;
     *                       never {@code null} or blank
     */
    record TargetCopyWithImmediateRetry(String failureMessage) implements RetryDecision {

        /**
         * Returns the constant failure class identifier for target copy failures.
         *
         * @return {@code "TARGET_COPY_TECHNICAL_ERROR"}
         */
        @Override
        public String failureClass() {
            return DocumentErrorClassification.TARGET_COPY_TECHNICAL_ERROR.name();
        }
    }
}
