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

import java.util.Objects;

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;

/**
 * Default implementation of the {@link RetryDecisionEvaluator} interface.
 * <p>
 * Applies the binding retry policy rules exactly as specified:
 * <ul>
 *   <li><strong>Deterministic content errors</strong>: the first historised occurrence
 *       for a fingerprint leads to {@link RetryDecision.ContentErrorRetryable} (one later
 *       scheduler run may retry); the second occurrence leads to
 *       {@link RetryDecision.ContentErrorFinal} (no further retries).</li>
 *   <li><strong>Transient technical errors</strong>: the error remains
 *       {@link RetryDecision.TransientErrorRetryable} while the counter after incrementing
 *       is strictly less than {@code maxRetriesTransient}. When the counter after
 *       incrementing reaches {@code maxRetriesTransient}, the result is
 *       {@link RetryDecision.TransientErrorFinal}.</li>
 *   <li><strong>Target copy failures</strong>: the first copy failure within a run
 *       produces {@link RetryDecision.TargetCopyWithImmediateRetry}, allowing exactly
 *       one immediate within-run retry of the physical copy step. This decision does
 *       not modify any counter.</li>
 * </ul>
 * <p>
 * <strong>Counter semantics:</strong> The {@code currentCounters} passed to
 * {@link #evaluate} reflect the state <em>before</em> the current attempt's counter
 * increment. This evaluator computes what the counter will be after incrementing and
 * applies the threshold check accordingly.
 * <p>
 * <strong>Skip events</strong> ({@code SKIPPED_ALREADY_PROCESSED},
 * {@code SKIPPED_FINAL_FAILURE}) are not routed through this evaluator and never
 * produce a {@link RetryDecision}. No failure counter is changed by skip events.
 * <p>
 * <strong>Immediate within-run retry</strong> for the target copy path is a purely
 * technical within-run mechanism. It does not increment the laufübergreifenden
 * transient-error counter regardless of outcome, and it is not part of the
 * cross-run retry budget governed by {@code max.retries.transient}.
 * <p>
 * <strong>Single-truth rule:</strong> Evaluations are derived solely from the document
 * master record's failure counters and the configured limit. No additional, parallel
 * persistence source for retry decisions is introduced.
 * <p>
 * This class is stateless and thread-safe.
 */
public final class DefaultRetryDecisionEvaluator implements RetryDecisionEvaluator {

    /**
     * Derives the authoritative retry decision for a document-level error.
     * <p>
     * Decision rules by error class:
     * <ul>
     *   <li>{@link DocumentErrorClassification#DETERMINISTIC_CONTENT_ERROR}:
     *       {@code contentErrorCount} before increment = 0 →
     *       {@link RetryDecision.ContentErrorRetryable}; else →
     *       {@link RetryDecision.ContentErrorFinal}.</li>
     *   <li>{@link DocumentErrorClassification#TRANSIENT_TECHNICAL_ERROR}:
     *       {@code transientErrorCount + 1 < maxRetriesTransient} →
     *       {@link RetryDecision.TransientErrorRetryable};
     *       {@code transientErrorCount + 1 >= maxRetriesTransient} →
     *       {@link RetryDecision.TransientErrorFinal}.</li>
     *   <li>{@link DocumentErrorClassification#TARGET_COPY_TECHNICAL_ERROR}:
     *       always → {@link RetryDecision.TargetCopyWithImmediateRetry}.
     *       No counter is modified by this decision.</li>
     * </ul>
     *
     * @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; 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
     * @throws NullPointerException     if any reference parameter is {@code null}
     */
    @Override
    public RetryDecision evaluate(
            DocumentErrorClassification errorClass,
            FailureCounters currentCounters,
            int maxRetriesTransient,
            String failureClass,
            String failureMessage) {

        Objects.requireNonNull(errorClass, "errorClass must not be null");
        Objects.requireNonNull(currentCounters, "currentCounters must not be null");
        Objects.requireNonNull(failureClass, "failureClass must not be null");
        Objects.requireNonNull(failureMessage, "failureMessage must not be null");
        if (failureClass.isBlank()) {
            throw new IllegalArgumentException("failureClass must not be blank");
        }
        if (failureMessage.isBlank()) {
            throw new IllegalArgumentException("failureMessage must not be blank");
        }
        if (maxRetriesTransient < 1) {
            throw new IllegalArgumentException(
                    "maxRetriesTransient must be >= 1, but was: " + maxRetriesTransient);
        }

        return switch (errorClass) {
            case DETERMINISTIC_CONTENT_ERROR -> evaluateContentError(
                    currentCounters, failureClass, failureMessage);
            case TRANSIENT_TECHNICAL_ERROR -> evaluateTransientError(
                    currentCounters, maxRetriesTransient, failureClass, failureMessage);
            case TARGET_COPY_TECHNICAL_ERROR ->
                    new RetryDecision.TargetCopyWithImmediateRetry(failureMessage);
        };
    }

    /**
     * Determines whether an immediate within-run retry of the target copy operation
     * is permitted.
     * <p>
     * {@link ImmediateRetryDecision#ALLOWED} is returned only when the copy has failed
     * on its first attempt within the current run. If this is the second copy attempt
     * (the immediate retry itself has failed), {@link ImmediateRetryDecision#DENIED} is
     * returned and the failure must be escalated to the cross-run retry evaluation.
     *
     * @param isFirstCopyAttemptInThisRun {@code true} if the failing copy attempt was
     *                                    the first copy attempt for this document in
     *                                    the current run
     * @return {@link ImmediateRetryDecision#ALLOWED} or {@link ImmediateRetryDecision#DENIED};
     *         never {@code null}
     */
    @Override
    public ImmediateRetryDecision evaluateImmediateRetry(boolean isFirstCopyAttemptInThisRun) {
        return isFirstCopyAttemptInThisRun
                ? ImmediateRetryDecision.ALLOWED
                : ImmediateRetryDecision.DENIED;
    }

    /**
     * Evaluates the retry decision for a deterministic content error.
     * <p>
     * The content-error counter before this attempt determines the decision:
     * <ul>
     *   <li>Count = 0 (first error) → {@link RetryDecision.ContentErrorRetryable};
     *       one later scheduler run may retry.</li>
     *   <li>Count &ge; 1 (second or subsequent error) → {@link RetryDecision.ContentErrorFinal};
     *       no further retries.</li>
     * </ul>
     *
     * @param currentCounters failure counters before incrementing
     * @param failureClass    failure class identifier
     * @param failureMessage  failure description
     * @return the appropriate content-error retry decision
     */
    private static RetryDecision evaluateContentError(
            FailureCounters currentCounters,
            String failureClass,
            String failureMessage) {

        if (currentCounters.contentErrorCount() == 0) {
            return new RetryDecision.ContentErrorRetryable(failureClass, failureMessage);
        }
        return new RetryDecision.ContentErrorFinal(failureClass, failureMessage);
    }

    /**
     * Evaluates the retry decision for a transient technical error.
     * <p>
     * The transient-error counter after incrementing determines the decision:
     * <ul>
     *   <li>Counter after increment strictly less than {@code maxRetriesTransient} →
     *       {@link RetryDecision.TransientErrorRetryable}; a later scheduler run may retry.</li>
     *   <li>Counter after increment equals or exceeds {@code maxRetriesTransient} →
     *       {@link RetryDecision.TransientErrorFinal}; no further retries.</li>
     * </ul>
     * <p>
     * Example with {@code maxRetriesTransient = 1}: counter before = 0,
     * counter after = 1 = limit → {@link RetryDecision.TransientErrorFinal} immediately.
     * <p>
     * Example with {@code maxRetriesTransient = 2}: counter before = 0,
     * counter after = 1 &lt; 2 → {@link RetryDecision.TransientErrorRetryable};
     * counter before = 1, counter after = 2 = limit → {@link RetryDecision.TransientErrorFinal}.
     *
     * @param currentCounters     failure counters before incrementing
     * @param maxRetriesTransient configured maximum historised transient errors (&ge; 1)
     * @param failureClass        failure class identifier
     * @param failureMessage      failure description
     * @return the appropriate transient-error retry decision
     */
    private static RetryDecision evaluateTransientError(
            FailureCounters currentCounters,
            int maxRetriesTransient,
            String failureClass,
            String failureMessage) {

        int counterAfterIncrement = currentCounters.transientErrorCount() + 1;
        if (counterAfterIncrement < maxRetriesTransient) {
            return new RetryDecision.TransientErrorRetryable(failureClass, failureMessage);
        }
        return new RetryDecision.TransientErrorFinal(failureClass, failureMessage);
    }
}
