package de.gecheckt.pdf.umbenenner.domain.model;

import java.time.Instant;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * Technical context representing a single batch processing run.
 * <p>
 * Each batch run is assigned a unique {@link RunId} and has associated timestamp information.
 * The context flows through the entire execution from Bootstrap through Use Case,
 * enabling correlation of all activities and results within a single run.
 * <p>
 * Responsibilities:
 * <ul>
 *   <li>Track the unique identity of the batch run</li>
 *   <li>Record when the run started (and eventually when it ends)</li>
 *   <li>Carry an optional fingerprint filter for targeted (mini) runs that restrict
 *       processing to a specific set of documents</li>
 *   <li>Provide run context to persistence, logging, and result tracking</li>
 * </ul>
 * <p>
 * This context is independent of individual document processing and contains
 * no business logic. It is purely a technical container for run identity, timing,
 * and optional candidate restriction.
 * <p>
 * When no fingerprint filter is set ({@link #fingerprintFilter()} returns empty),
 * the run processes all candidates found in the source folder (regular batch run).
 * When a non-empty filter is present, only candidates whose fingerprint is contained
 * in that set are processed.
 */
public final class BatchRunContext {

    private final RunId runId;
    private final Instant startInstant;
    private Instant endInstant;
    private final Set<DocumentFingerprint> fingerprintFilter;

    /**
     * Creates a new BatchRunContext for a regular (unfiltered) batch run.
     * <p>
     * No fingerprint filter is applied; all candidates from the source folder are
     * eligible for processing.
     * <p>
     * The end instant is initially null and may be set later via {@link #setEndInstant(Instant)}.
     *
     * @param runId        the unique identifier for this run; must not be null
     * @param startInstant the moment when the run started; must not be null
     * @throws NullPointerException if {@code runId} or {@code startInstant} is null
     */
    public BatchRunContext(RunId runId, Instant startInstant) {
        this(runId, startInstant, null);
    }

    /**
     * Creates a new BatchRunContext, optionally restricting the run to a specific
     * set of document fingerprints.
     * <p>
     * When {@code fingerprintFilter} is non-null and non-empty, the batch run
     * processes only candidates whose computed fingerprint is contained in the
     * supplied set. An empty set results in a run that processes nothing.
     * A {@code null} value is treated identically to a regular unfiltered run.
     * <p>
     * The supplied set is defensively copied; modifications to the original set
     * after construction have no effect on this context.
     * <p>
     * The end instant is initially null and may be set later via {@link #setEndInstant(Instant)}.
     *
     * @param runId             the unique identifier for this run; must not be null
     * @param startInstant      the moment when the run started; must not be null
     * @param fingerprintFilter the set of fingerprints to restrict processing to,
     *                          or {@code null} for an unfiltered run
     * @throws NullPointerException if {@code runId} or {@code startInstant} is null
     */
    public BatchRunContext(RunId runId, Instant startInstant, Set<DocumentFingerprint> fingerprintFilter) {
        this.runId = Objects.requireNonNull(runId, "RunId must not be null");
        this.startInstant = Objects.requireNonNull(startInstant, "Start instant must not be null");
        this.endInstant = null;
        this.fingerprintFilter = fingerprintFilter != null
                ? Collections.unmodifiableSet(Set.copyOf(fingerprintFilter))
                : null;
    }

    /**
     * Returns a new {@link BatchRunContext} with the same run ID and start instant,
     * but restricted to the given fingerprint filter.
     * <p>
     * The returned context is independent; changes to the supplied set after this
     * call have no effect on the new context.
     *
     * @param filter the set of fingerprints to restrict processing to;
     *               {@code null} is treated as no filter (regular run)
     * @return a new context with the supplied filter applied; never null
     */
    public BatchRunContext withFingerprintFilter(Set<DocumentFingerprint> filter) {
        return new BatchRunContext(runId, startInstant, filter);
    }

    /**
     * Returns the optional fingerprint filter for this run.
     * <p>
     * When the returned optional is empty, no restriction applies and all source-folder
     * candidates are eligible (regular batch run). When present, only candidates whose
     * computed fingerprint is contained in the returned set are processed.
     *
     * @return an {@link Optional} containing the immutable fingerprint filter set,
     *         or an empty optional for a regular unfiltered run
     */
    public Optional<Set<DocumentFingerprint>> fingerprintFilter() {
        return Optional.ofNullable(fingerprintFilter);
    }

    /**
     * Returns the unique identifier of this run.
     *
     * @return the run ID, never null
     */
    public RunId runId() {
        return runId;
    }

    /**
     * Returns the instant when this run started.
     *
     * @return the start instant, never null
     */
    public Instant startInstant() {
        return startInstant;
    }

    /**
     * Returns the instant when this run ended, or null if the run has not yet completed.
     * <p>
     * The end instant is set by {@link #setEndInstant(Instant)} during run completion.
     *
     * @return the end instant, or null if the run is still in progress
     */
    public Instant endInstant() {
        return endInstant;
    }

    /**
     * Sets the instant when this run ended.
     * <p>
     * This should be called once at the end of the batch run to mark completion.
     * Typically called by the orchestration layer (Bootstrap) after the use case completes.
     *
     * @param endInstant the moment when the run completed, must not be null
     * @throws NullPointerException if endInstant is null
     * @throws IllegalStateException if end instant has already been set
     */
    public void setEndInstant(Instant endInstant) {
        Objects.requireNonNull(endInstant, "End instant must not be null");
        if (this.endInstant != null) {
            throw new IllegalStateException("End instant has already been set");
        }
        this.endInstant = endInstant;
    }

    /**
     * Returns true if this run has completed (end instant is set).
     *
     * @return true if end instant is not null, false otherwise
     */
    public boolean isCompleted() {
        return endInstant != null;
    }

    @Override
    public String toString() {
        return "BatchRunContext{" +
                "runId=" + runId +
                ", startInstant=" + startInstant +
                ", endInstant=" + endInstant +
                ", fingerprintFilter=" + (fingerprintFilter == null
                        ? "none" : fingerprintFilter.size() + " fingerprints") +
                '}';
    }
}
