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

import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
import de.gecheckt.pdf.umbenenner.domain.model.AiFunctionalFailure;
import de.gecheckt.pdf.umbenenner.domain.model.AiTechnicalFailure;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
import de.gecheckt.pdf.umbenenner.domain.model.NamingProposalReady;
import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed;
import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailureReason;
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;

/**
 * Authoritative, stateless retry decision rule for all document processing outcomes.
 * <p>
 * This class is the single production source of truth for mapping a pipeline outcome
 * (pre-check, naming proposal, or failure) to a processing status, updated failure
 * counters, and retryability flag. Both the AI pipeline path and the target-copy
 * finalization path in {@link DocumentProcessingCoordinator} delegate to this class,
 * so that no duplicate retry logic exists elsewhere.
 * <p>
 * The transition logic is independent of persistence, orchestration, or any
 * infrastructure concern. It is purely declarative and stateless.
 *
 * <h2>Transition rules</h2>
 * <ul>
 *   <li><strong>Naming proposal ready:</strong> Status becomes
 *       {@link ProcessingStatus#PROPOSAL_READY}, counters unchanged,
 *       {@code retryable=false}.</li>
 *   <li><strong>Pre-check content error {@link PreCheckFailureReason#NO_USABLE_TEXT}:</strong>
 *       Status becomes {@link ProcessingStatus#FAILED_FINAL} immediately,
 *       content error counter incremented by 1, {@code retryable=false}.
 *       Image-only PDFs without OCR text will not yield usable text on retry.</li>
 *   <li><strong>Pre-check content error (other reason, first occurrence):</strong>
 *       Status becomes {@link ProcessingStatus#FAILED_RETRYABLE},
 *       content error counter incremented by 1, {@code retryable=true}.</li>
 *   <li><strong>Pre-check content error (other reason, second or later occurrence):</strong>
 *       Status becomes {@link ProcessingStatus#FAILED_FINAL},
 *       content error counter incremented by 1, {@code retryable=false}.</li>
 *   <li><strong>AI functional failure (first occurrence):</strong>
 *       Status becomes {@link ProcessingStatus#FAILED_RETRYABLE},
 *       content error counter incremented by 1, {@code retryable=true}.</li>
 *   <li><strong>AI functional failure (second or later occurrence):</strong>
 *       Status becomes {@link ProcessingStatus#FAILED_FINAL},
 *       content error counter incremented by 1, {@code retryable=false}.</li>
 *   <li><strong>Technical error below the transient retry limit:</strong>
 *       Status becomes {@link ProcessingStatus#FAILED_RETRYABLE},
 *       transient error counter incremented by 1, {@code retryable=true}.</li>
 *   <li><strong>Technical error at or above the transient retry limit:</strong>
 *       Status becomes {@link ProcessingStatus#FAILED_FINAL},
 *       transient error counter incremented by 1, {@code retryable=false}.</li>
 * </ul>
 *
 * <h2>Transient retry limit semantics</h2>
 * <p>
 * {@code maxRetriesTransient} is interpreted as the maximum number of historised
 * transient error attempts per fingerprint. The attempt that causes the counter
 * to reach {@code maxRetriesTransient} finalises the document status to
 * {@link ProcessingStatus#FAILED_FINAL}. Valid values are integers &gt;= 1;
 * the value 0 is invalid startup configuration and must be rejected before
 * the batch run begins.
 * <p>
 * Examples:
 * <ul>
 *   <li>{@code maxRetriesTransient = 1}: the first historised transient error
 *       immediately finalises to {@code FAILED_FINAL}.</li>
 *   <li>{@code maxRetriesTransient = 2}: the first transient error yields
 *       {@code FAILED_RETRYABLE}; the second finalises to {@code FAILED_FINAL}.</li>
 * </ul>
 */
final class ProcessingOutcomeTransition {

    private ProcessingOutcomeTransition() {
        // Static utility class; no instances
    }

    /**
     * Maps a pipeline outcome to a processing outcome for a brand-new document.
     * <p>
     * For new documents, all failure counters start at zero.
     *
     * @param pipelineOutcome   the outcome from the processing pipeline
     * @param maxRetriesTransient maximum number of historised transient error attempts
     *                            before the document is finalised to {@code FAILED_FINAL};
     *                            must be &gt;= 1
     * @return the mapped outcome with status, counters, and retryability
     */
    static ProcessingOutcome forNewDocument(
            DocumentProcessingOutcome pipelineOutcome,
            int maxRetriesTransient) {
        return forKnownDocument(pipelineOutcome, FailureCounters.zero(), maxRetriesTransient);
    }

    /**
     * Maps a pipeline outcome to a processing outcome, considering the existing
     * failure counter state from a known document's history.
     *
     * @param pipelineOutcome     the outcome from the processing pipeline
     * @param existingCounters    the current failure counter values from the document's master record
     * @param maxRetriesTransient maximum number of historised transient error attempts
     *                            before the document is finalised to {@code FAILED_FINAL};
     *                            must be &gt;= 1
     * @return the mapped outcome with updated status, counters, and retryability
     */
    static ProcessingOutcome forKnownDocument(
            DocumentProcessingOutcome pipelineOutcome,
            FailureCounters existingCounters,
            int maxRetriesTransient) {

        return switch (pipelineOutcome) {
            case NamingProposalReady ignored -> {
                // AI naming proposal produced → PROPOSAL_READY (not yet SUCCESS)
                yield new ProcessingOutcome(
                        ProcessingStatus.PROPOSAL_READY,
                        existingCounters,   // counters unchanged on proposal success
                        false               // not retryable
                );
            }

            case PreCheckFailed preCheckFailed -> {
                FailureCounters updatedCounters = existingCounters.withIncrementedContentErrorCount();

                if (preCheckFailed.failureReason() == PreCheckFailureReason.NO_USABLE_TEXT) {
                    // Image-only PDFs without OCR text will not change on retry.
                    yield new ProcessingOutcome(ProcessingStatus.FAILED_FINAL, updatedCounters, false);
                }

                // Other deterministic content errors: apply the 1-retry rule
                boolean isFirstOccurrence = existingCounters.contentErrorCount() == 0;
                if (isFirstOccurrence) {
                    yield new ProcessingOutcome(ProcessingStatus.FAILED_RETRYABLE, updatedCounters, true);
                } else {
                    yield new ProcessingOutcome(ProcessingStatus.FAILED_FINAL, updatedCounters, false);
                }
            }

            case AiFunctionalFailure ignored3 -> {
                // Deterministic content error from AI validation: apply the 1-retry rule
                FailureCounters updatedCounters = existingCounters.withIncrementedContentErrorCount();
                boolean isFirstOccurrence = existingCounters.contentErrorCount() == 0;

                if (isFirstOccurrence) {
                    yield new ProcessingOutcome(ProcessingStatus.FAILED_RETRYABLE, updatedCounters, true);
                } else {
                    yield new ProcessingOutcome(ProcessingStatus.FAILED_FINAL, updatedCounters, false);
                }
            }

            case TechnicalDocumentError ignored4 -> {
                // Technical error (extraction / infrastructure): apply transient retry limit
                FailureCounters updatedCounters = existingCounters.withIncrementedTransientErrorCount();
                boolean limitReached = updatedCounters.transientErrorCount() >= maxRetriesTransient;
                yield new ProcessingOutcome(
                        limitReached ? ProcessingStatus.FAILED_FINAL : ProcessingStatus.FAILED_RETRYABLE,
                        updatedCounters,
                        !limitReached
                );
            }

            case AiTechnicalFailure ignored5 -> {
                // Technical AI error (timeout, unreachable, bad JSON): apply transient retry limit
                FailureCounters updatedCounters = existingCounters.withIncrementedTransientErrorCount();
                boolean limitReached = updatedCounters.transientErrorCount() >= maxRetriesTransient;
                yield new ProcessingOutcome(
                        limitReached ? ProcessingStatus.FAILED_FINAL : ProcessingStatus.FAILED_RETRYABLE,
                        updatedCounters,
                        !limitReached
                );
            }

            case de.gecheckt.pdf.umbenenner.domain.model.PreCheckPassed ignored6 -> {
                // Pre-check passed without AI step: in normal flow this should not appear at
                // the outcome transition level once the AI pipeline is fully wired. Treat it
                // as a technical error and apply the transient retry limit.
                FailureCounters updatedCounters = existingCounters.withIncrementedTransientErrorCount();
                boolean limitReached = updatedCounters.transientErrorCount() >= maxRetriesTransient;
                yield new ProcessingOutcome(
                        limitReached ? ProcessingStatus.FAILED_FINAL : ProcessingStatus.FAILED_RETRYABLE,
                        updatedCounters,
                        !limitReached
                );
            }
        };
    }

    /**
     * Value type carrying the status, updated counters, and retryable flag
     * after transition from a pipeline outcome.
     *
     * @param overallStatus the overall processing status to persist
     * @param counters      the updated failure counters to persist
     * @param retryable     whether a failure is retryable in a later run
     */
    record ProcessingOutcome(
            ProcessingStatus overallStatus,
            FailureCounters counters,
            boolean retryable) {
    }
}
