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

import de.gecheckt.pdf.umbenenner.application.port.out.DocumentErrorClassification;
import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
import de.gecheckt.pdf.umbenenner.application.port.out.ImmediateRetryDecision;
import de.gecheckt.pdf.umbenenner.application.port.out.RetryDecision;

/**
 * Application service contract for deriving authoritative retry decisions from
 * document error state and configuration.
 * <p>
 * This interface defines the single, testable entry point for all retry policy
 * evaluations. Implementations must apply the verbindlichen retry rules exactly
 * as specified:
 * <ul>
 *   <li><strong>Deterministic content errors</strong> ({@link DocumentErrorClassification#DETERMINISTIC_CONTENT_ERROR}):
 *       the <em>first</em> historised content error for a fingerprint results in
 *       {@link RetryDecision.ContentErrorRetryable}; the <em>second</em> results in
 *       {@link RetryDecision.ContentErrorFinal}.</li>
 *   <li><strong>Transient technical errors</strong> ({@link DocumentErrorClassification#TRANSIENT_TECHNICAL_ERROR}):
 *       the error remains retryable while the transient-error counter after incrementing
 *       stays strictly below {@code maxRetriesTransient}. When the counter after
 *       incrementing reaches {@code maxRetriesTransient}, the result is
 *       {@link RetryDecision.TransientErrorFinal}.</li>
 *   <li><strong>Target copy failures</strong> ({@link DocumentErrorClassification#TARGET_COPY_TECHNICAL_ERROR})
 *       on the <em>first</em> copy attempt within a run: result is
 *       {@link RetryDecision.TargetCopyWithImmediateRetry}. After the immediate retry
 *       has itself failed, the failure is re-evaluated as a
 *       {@link DocumentErrorClassification#TRANSIENT_TECHNICAL_ERROR}.</li>
 * </ul>
 * <p>
 * <strong>Counter semantics:</strong>
 * <ul>
 *   <li>The {@code currentCounters} passed to {@link #evaluate} reflect the state
 *       <em>before</em> the current attempt's counter increment. The evaluator is
 *       responsible for determining what the counter will be after incrementing and
 *       applying the threshold check accordingly.</li>
 *   <li>Skip events ({@code SKIPPED_ALREADY_PROCESSED}, {@code SKIPPED_FINAL_FAILURE})
 *       are not routed through this evaluator and never produce a
 *       {@link RetryDecision}.</li>
 * </ul>
 * <p>
 * <strong>{@code maxRetriesTransient} invariant:</strong>
 * The value must be an Integer &ge; 1. A value of {@code 0} is invalid configuration
 * and must be rejected at startup before any batch run begins. Implementations of
 * this interface may assume the value is always &ge; 1 when called.
 * <p>
 * Example for {@code maxRetriesTransient = 1}:
 * <ul>
 *   <li>transient-error counter before = 0 → after increment = 1 = limit → {@link RetryDecision.TransientErrorFinal}</li>
 * </ul>
 * Example for {@code maxRetriesTransient = 2}:
 * <ul>
 *   <li>transient-error counter before = 0 → after increment = 1 &lt; 2 → {@link RetryDecision.TransientErrorRetryable}</li>
 *   <li>transient-error counter before = 1 → after increment = 2 = limit → {@link RetryDecision.TransientErrorFinal}</li>
 * </ul>
 * <p>
 * <strong>Single-truth rule:</strong> No parallel persistence source for retry
 * decisions is introduced. Evaluations are derived solely from the document master
 * record's failure counters and the configured limit.
 */
public interface RetryDecisionEvaluator {

    /**
     * Derives the authoritative retry decision for a document-level error.
     * <p>
     * The decision is determined by the error classification, the existing failure
     * counters (before any increment for the current attempt), and the configured
     * transient-retry limit.
     *
     * @param errorClass          classification of the error that occurred; never {@code null}
     * @param currentCounters     failure counters <em>before</em> incrementing for this
     *                            attempt; never {@code null}
     * @param maxRetriesTransient configured maximum number of historised transient errors
     *                            allowed per fingerprint; must be &ge; 1
     * @param failureClass        short, stable failure class identifier for persistence
     *                            and logging; never {@code null} or blank
     * @param failureMessage      human-readable description of the error; never {@code null}
     *                            or blank
     * @return the authoritative {@link RetryDecision}; never {@code null}
     * @throws IllegalArgumentException if {@code maxRetriesTransient} is less than 1
     */
    RetryDecision evaluate(
            DocumentErrorClassification errorClass,
            FailureCounters currentCounters,
            int maxRetriesTransient,
            String failureClass,
            String failureMessage);

    /**
     * Determines whether an immediate within-run retry of the target copy operation
     * is permitted.
     * <p>
     * An immediate retry is {@link ImmediateRetryDecision#ALLOWED} only when the copy
     * has failed on its first attempt within the current run. If this is the second
     * copy attempt within the same run (i.e. the immediate retry itself has failed),
     * the result is {@link ImmediateRetryDecision#DENIED}.
     *
     * @param isFirstCopyAttemptInThisRun {@code true} if the failing copy attempt was
     *                                    the first copy attempt for this document in
     *                                    the current run; {@code false} if it was the
     *                                    immediate retry attempt
     * @return {@link ImmediateRetryDecision#ALLOWED} or {@link ImmediateRetryDecision#DENIED};
     *         never {@code null}
     */
    ImmediateRetryDecision evaluateImmediateRetry(boolean isFirstCopyAttemptInThisRun);
}
