package org.mockserver.model;

import java.util.Objects;

/**
 * Declarative fault/chaos injection for an {@link HttpLlmResponse}, for testing
 * agent resilience: probabilistic provider errors (e.g. 429/529 with a
 * {@code Retry-After} header), mid-stream truncation, and malformed SSE chunks.
 * <p>
 * Determinism: with {@code errorProbability} of {@code 1.0} (always) or
 * {@code 0.0}/null (never) the error decision is fully deterministic. A
 * fractional probability draws once per response; set {@code seed} to make that
 * single draw reproducible (note: a fixed seed yields the same decision every
 * time). Truncation and malformed-SSE are deterministic. Follows the model
 * field/{@code withX}/getter convention so it round-trips without a bespoke
 * (de)serializer.
 * <p>
 * It also carries an optional <em>stateful</em> request quota (a fixed-window
 * rate limit): when {@code quotaName}, {@code quotaLimit}, and
 * {@code quotaWindowMillis} are set, requests beyond {@code quotaLimit} within
 * the window are rejected with {@code quotaErrorStatus} (default 429) and the
 * {@code retryAfter} header. Unlike the probabilistic error this is
 * deterministic and counts real requests across the process (see
 * {@link org.mockserver.llm.LlmQuotaRegistry}); expectations sharing a
 * {@code quotaName} share one counter.
 */
public class LlmChaosProfile extends ObjectWithJsonToString {

    public enum TruncateMode {
        NONE,
        MID_STREAM
    }

    private int hashCode;
    private Integer errorStatus;       // e.g. 429, 529, 500
    private String retryAfter;         // value for the Retry-After header on an error (probabilistic or quota)
    private Double errorProbability;   // 0.0–1.0; null/0 = never inject an error
    private TruncateMode truncateMode; // streaming: truncate the SSE event stream
    private Double truncateAtFraction; // 0.0–1.0 fraction of events to keep before truncating
    private Boolean malformedSse;      // streaming: emit a malformed (broken-JSON) SSE chunk
    private Long seed;                 // optional, makes a fractional errorProbability reproducible
    private String quotaName;          // stateful quota: shared counter key
    private Integer quotaLimit;        // stateful quota: max requests allowed per window
    private Long quotaWindowMillis;    // stateful quota: window length in milliseconds
    private Integer quotaErrorStatus;  // stateful quota: status when exceeded (default 429)

    public static LlmChaosProfile llmChaosProfile() {
        return new LlmChaosProfile();
    }

    public LlmChaosProfile withErrorStatus(Integer errorStatus) {
        if (errorStatus != null && (errorStatus < 100 || errorStatus > 599)) {
            throw new IllegalArgumentException("errorStatus must be between 100 and 599, got " + errorStatus);
        }
        this.errorStatus = errorStatus;
        this.hashCode = 0;
        return this;
    }

    public Integer getErrorStatus() {
        return errorStatus;
    }

    public LlmChaosProfile withRetryAfter(String retryAfter) {
        this.retryAfter = retryAfter;
        this.hashCode = 0;
        return this;
    }

    public String getRetryAfter() {
        return retryAfter;
    }

    public LlmChaosProfile withErrorProbability(Double errorProbability) {
        if (errorProbability != null && (Double.isNaN(errorProbability) || errorProbability < 0.0 || errorProbability > 1.0)) {
            throw new IllegalArgumentException("errorProbability must be between 0.0 and 1.0, got " + errorProbability);
        }
        this.errorProbability = errorProbability;
        this.hashCode = 0;
        return this;
    }

    public Double getErrorProbability() {
        return errorProbability;
    }

    public LlmChaosProfile withTruncateMode(TruncateMode truncateMode) {
        this.truncateMode = truncateMode;
        this.hashCode = 0;
        return this;
    }

    public TruncateMode getTruncateMode() {
        return truncateMode;
    }

    public LlmChaosProfile withTruncateAtFraction(Double truncateAtFraction) {
        if (truncateAtFraction != null && (Double.isNaN(truncateAtFraction) || truncateAtFraction < 0.0 || truncateAtFraction > 1.0)) {
            throw new IllegalArgumentException("truncateAtFraction must be between 0.0 and 1.0, got " + truncateAtFraction);
        }
        this.truncateAtFraction = truncateAtFraction;
        this.hashCode = 0;
        return this;
    }

    public Double getTruncateAtFraction() {
        return truncateAtFraction;
    }

    public LlmChaosProfile withMalformedSse(Boolean malformedSse) {
        this.malformedSse = malformedSse;
        this.hashCode = 0;
        return this;
    }

    public Boolean getMalformedSse() {
        return malformedSse;
    }

    public LlmChaosProfile withSeed(Long seed) {
        this.seed = seed;
        this.hashCode = 0;
        return this;
    }

    public Long getSeed() {
        return seed;
    }

    public LlmChaosProfile withQuotaName(String quotaName) {
        this.quotaName = quotaName;
        this.hashCode = 0;
        return this;
    }

    public String getQuotaName() {
        return quotaName;
    }

    public LlmChaosProfile withQuotaLimit(Integer quotaLimit) {
        if (quotaLimit != null && quotaLimit < 1) {
            throw new IllegalArgumentException("quotaLimit must be >= 1, got " + quotaLimit);
        }
        this.quotaLimit = quotaLimit;
        this.hashCode = 0;
        return this;
    }

    public Integer getQuotaLimit() {
        return quotaLimit;
    }

    public LlmChaosProfile withQuotaWindowMillis(Long quotaWindowMillis) {
        if (quotaWindowMillis != null && quotaWindowMillis < 1) {
            throw new IllegalArgumentException("quotaWindowMillis must be >= 1, got " + quotaWindowMillis);
        }
        this.quotaWindowMillis = quotaWindowMillis;
        this.hashCode = 0;
        return this;
    }

    public Long getQuotaWindowMillis() {
        return quotaWindowMillis;
    }

    public LlmChaosProfile withQuotaErrorStatus(Integer quotaErrorStatus) {
        if (quotaErrorStatus != null && (quotaErrorStatus < 100 || quotaErrorStatus > 599)) {
            throw new IllegalArgumentException("quotaErrorStatus must be between 100 and 599, got " + quotaErrorStatus);
        }
        this.quotaErrorStatus = quotaErrorStatus;
        this.hashCode = 0;
        return this;
    }

    public Integer getQuotaErrorStatus() {
        return quotaErrorStatus;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        if (hashCode() != o.hashCode()) {
            return false;
        }
        LlmChaosProfile that = (LlmChaosProfile) o;
        return Objects.equals(errorStatus, that.errorStatus) &&
            Objects.equals(retryAfter, that.retryAfter) &&
            Objects.equals(errorProbability, that.errorProbability) &&
            truncateMode == that.truncateMode &&
            Objects.equals(truncateAtFraction, that.truncateAtFraction) &&
            Objects.equals(malformedSse, that.malformedSse) &&
            Objects.equals(seed, that.seed) &&
            Objects.equals(quotaName, that.quotaName) &&
            Objects.equals(quotaLimit, that.quotaLimit) &&
            Objects.equals(quotaWindowMillis, that.quotaWindowMillis) &&
            Objects.equals(quotaErrorStatus, that.quotaErrorStatus);
    }

    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = Objects.hash(errorStatus, retryAfter, errorProbability, truncateMode, truncateAtFraction, malformedSse, seed,
                quotaName, quotaLimit, quotaWindowMillis, quotaErrorStatus);
        }
        return hashCode;
    }
}
