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

import java.time.Instant;
import java.time.LocalDate;
import java.util.Objects;

import de.gecheckt.pdf.umbenenner.domain.model.DateSource;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
import de.gecheckt.pdf.umbenenner.domain.model.RunId;

/**
 * Application-facing representation of exactly one historised processing attempt
 * (Versuchshistorie-Eintrag) for an identified document.
 * <p>
 * <strong>Historisation boundary:</strong> Only attempts for documents whose
 * {@link DocumentFingerprint} was successfully computed are historised. Failures that
 * occur <em>before</em> the fingerprint is available (e.g. the source file is
 * unreadable before hashing) are <em>not</em> represented by a {@code ProcessingAttempt}
 * and are <em>not</em> written to SQLite.
 * <p>
 * <strong>Attempt number semantics:</strong> The attempt number starts at&nbsp;1 for the
 * first historised attempt per fingerprint and increases monotonically by 1 for every
 * subsequent attempt, including skip attempts
 * ({@link ProcessingStatus#SKIPPED_ALREADY_PROCESSED},
 * {@link ProcessingStatus#SKIPPED_FINAL_FAILURE}).
 * <p>
 * <strong>Field semantics:</strong>
 * <ul>
 *   <li>{@link #fingerprint()} — foreign key to the document master record.</li>
 *   <li>{@link #runId()} — identifies the batch run during which this attempt occurred.</li>
 *   <li>{@link #attemptNumber()} — monotonically increasing per fingerprint; assigned
 *       before the attempt is recorded.</li>
 *   <li>{@link #startedAt()} — wall-clock timestamp when processing of this candidate
 *       began in this run.</li>
 *   <li>{@link #endedAt()} — wall-clock timestamp when processing completed (success,
 *       failure, or skip).</li>
 *   <li>{@link #status()} — outcome status of this specific attempt.</li>
 *   <li>{@link #failureClass()} — short classification of the failure (e.g. enum constant
 *       name or exception class name); {@code null} for successful or skip attempts.</li>
 *   <li>{@link #failureMessage()} — human-readable failure description; {@code null} for
 *       successful or skip attempts.</li>
 *   <li>{@link #retryable()} — {@code true} if the failure is considered retryable in a
 *       later run; {@code false} for final failures, successes, and skip attempts.</li>
 *   <li>{@link #aiProvider()} — opaque identifier of the AI provider that was active
 *       during this attempt (e.g. {@code "openai-compatible"} or {@code "claude"});
 *       {@code null} for attempts that did not involve an AI call (skip, pre-check
 *       failure) or for historical attempts recorded before this field was introduced.</li>
 *   <li>{@link #modelName()} — the AI model name used in this attempt; {@code null} if
 *       no AI call was made (e.g. pre-check failures or skip attempts).</li>
 *   <li>{@link #promptIdentifier()} — stable identifier of the prompt template used;
 *       {@code null} if no AI call was made.</li>
 *   <li>{@link #processedPageCount()} — number of PDF pages processed; {@code null} if
 *       pages were not extracted (e.g. pre-fingerprint or skip attempts).</li>
 *   <li>{@link #sentCharacterCount()} — number of characters sent to the AI; {@code null}
 *       if no AI call was made.</li>
 *   <li>{@link #aiRawResponse()} — the complete raw AI response body; {@code null} if no
 *       AI call was made. Stored in SQLite but not written to log files by default.</li>
 *   <li>{@link #aiReasoning()} — the reasoning extracted from the AI response; {@code null}
 *       if no valid AI response was obtained.</li>
 *   <li>{@link #resolvedDate()} — the date resolved for the naming proposal; {@code null}
 *       if no naming proposal was produced.</li>
 *   <li>{@link #dateSource()} — the origin of the resolved date; {@code null} if no
 *       naming proposal was produced.</li>
 *   <li>{@link #validatedTitle()} — the validated title from the naming proposal;
 *       {@code null} if no naming proposal was produced.</li>
 *   <li>{@link #finalTargetFileName()} — the final filename written to the target folder
 *       (including any duplicate suffix); set only for
 *       {@link ProcessingStatus#SUCCESS} attempts, {@code null} otherwise.</li>
 * </ul>
 *
 * @param fingerprint         content-based document identity; never null
 * @param runId               identifier of the batch run; never null
 * @param attemptNumber       monotonic sequence number per fingerprint; must be &gt;= 1
 * @param startedAt           start of this processing attempt; never null
 * @param endedAt             end of this processing attempt; never null
 * @param status              outcome status of this attempt; never null
 * @param failureClass        failure classification, or {@code null} for non-failure statuses
 * @param failureMessage      failure description, or {@code null} for non-failure statuses
 * @param retryable           whether this failure should be retried in a later run
 * @param aiProvider          opaque AI provider identifier for this attempt, or {@code null}
 * @param modelName           AI model name, or {@code null} if no AI call was made
 * @param promptIdentifier    prompt identifier, or {@code null} if no AI call was made
 * @param processedPageCount  number of PDF pages processed, or {@code null}
 * @param sentCharacterCount  number of characters sent to AI, or {@code null}
 * @param aiRawResponse       full raw AI response, or {@code null}
 * @param aiReasoning         AI reasoning text, or {@code null}
 * @param resolvedDate        resolved date for naming proposal, or {@code null}
 * @param dateSource          origin of resolved date, or {@code null}
 * @param validatedTitle      validated title, or {@code null}
 * @param finalTargetFileName filename written to the target folder for SUCCESS attempts,
 *                            or {@code null}
 */
public record ProcessingAttempt(
        DocumentFingerprint fingerprint,
        RunId runId,
        int attemptNumber,
        Instant startedAt,
        Instant endedAt,
        ProcessingStatus status,
        String failureClass,
        String failureMessage,
        boolean retryable,
        // AI traceability fields (null for non-AI attempts)
        String aiProvider,
        String modelName,
        String promptIdentifier,
        Integer processedPageCount,
        Integer sentCharacterCount,
        String aiRawResponse,
        String aiReasoning,
        LocalDate resolvedDate,
        DateSource dateSource,
        String validatedTitle,
        // Target copy traceability (null for non-SUCCESS attempts)
        String finalTargetFileName) {

    /**
     * Compact constructor validating mandatory non-null fields and numeric constraints.
     *
     * @throws NullPointerException     if any mandatory field is null
     * @throws IllegalArgumentException if {@code attemptNumber} is less than 1
     */
    public ProcessingAttempt {
        Objects.requireNonNull(fingerprint, "fingerprint must not be null");
        Objects.requireNonNull(runId, "runId must not be null");
        if (attemptNumber < 1) {
            throw new IllegalArgumentException(
                    "attemptNumber must be >= 1, but was: " + attemptNumber);
        }
        Objects.requireNonNull(startedAt, "startedAt must not be null");
        Objects.requireNonNull(endedAt, "endedAt must not be null");
        Objects.requireNonNull(status, "status must not be null");
    }

    /**
     * Creates a {@link ProcessingAttempt} with no AI traceability fields set.
     * <p>
     * Convenience factory for pre-check failures, skip events, and any attempt
     * that does not involve an AI call. The {@link #aiProvider()} field is set
     * to {@code null}.
     *
     * @param fingerprint    document identity; must not be null
     * @param runId          batch run identifier; must not be null
     * @param attemptNumber  monotonic attempt number; must be &gt;= 1
     * @param startedAt      start instant; must not be null
     * @param endedAt        end instant; must not be null
     * @param status         outcome status; must not be null
     * @param failureClass   failure class name, or {@code null}
     * @param failureMessage failure description, or {@code null}
     * @param retryable      whether retryable in a later run
     * @return a new attempt with all AI fields set to {@code null}
     */
    public static ProcessingAttempt withoutAiFields(
            DocumentFingerprint fingerprint,
            RunId runId,
            int attemptNumber,
            Instant startedAt,
            Instant endedAt,
            ProcessingStatus status,
            String failureClass,
            String failureMessage,
            boolean retryable) {
        return new ProcessingAttempt(
                fingerprint, runId, attemptNumber, startedAt, endedAt,
                status, failureClass, failureMessage, retryable,
                null, null, null, null, null, null, null, null, null, null, null);
    }
}
