namespace KArtSell.Modules.ModelOperations.Domain;

public enum ModelOperationExecutionState
{
    Requested,
    Running,
    Succeeded,
    BusinessHold,
    Failed,
    Quarantined
}

public sealed record ModelOperationExecutionTransition(
    ModelOperationExecutionState From,
    ModelOperationExecutionState To,
    string ReasonCode,
    string EvidenceHash,
    DateTimeOffset OccurredAt,
    DateTimeOffset? HoldUntil);

public sealed class ModelOperationExecution
{
    private static readonly Dictionary<ModelOperationExecutionState, HashSet<ModelOperationExecutionState>> Allowed =
        new()
        {
            [ModelOperationExecutionState.Requested] = new(new[] { ModelOperationExecutionState.Running, ModelOperationExecutionState.BusinessHold, ModelOperationExecutionState.Quarantined }),
            [ModelOperationExecutionState.Running] = new(new[] { ModelOperationExecutionState.Succeeded, ModelOperationExecutionState.BusinessHold, ModelOperationExecutionState.Failed, ModelOperationExecutionState.Quarantined }),
            [ModelOperationExecutionState.BusinessHold] = new(new[] { ModelOperationExecutionState.Running, ModelOperationExecutionState.Quarantined }),
            [ModelOperationExecutionState.Failed] = new(new[] { ModelOperationExecutionState.Running, ModelOperationExecutionState.Quarantined }),
            [ModelOperationExecutionState.Succeeded] = new(),
            [ModelOperationExecutionState.Quarantined] = new()
        };

    private readonly List<ModelOperationExecutionTransition> transitions = [];

    public ModelOperationExecution(Guid requestId, string idempotencyKey, DateTimeOffset requestedAt)
    {
        if (requestId == Guid.Empty) throw new ArgumentException("Request ID is required.", nameof(requestId));
        ArgumentException.ThrowIfNullOrWhiteSpace(idempotencyKey);
        RequestId = requestId;
        IdempotencyKey = idempotencyKey;
        RequestedAt = requestedAt;
        LastOccurredAt = requestedAt;
        State = ModelOperationExecutionState.Requested;
    }

    public Guid RequestId { get; }
    public string IdempotencyKey { get; }
    public DateTimeOffset RequestedAt { get; }
    public DateTimeOffset LastOccurredAt { get; private set; }
    public DateTimeOffset? LastHeartbeatAt { get; private set; }
    public ModelOperationExecutionState State { get; private set; }
    public DateTimeOffset? HoldUntil { get; private set; }
    public IReadOnlyList<ModelOperationExecutionTransition> Transitions => transitions;

    public ModelOperationExecutionTransition MoveTo(
        ModelOperationExecutionState next,
        string reasonCode,
        string evidenceHash,
        DateTimeOffset occurredAt,
        DateTimeOffset? holdUntil = null)
    {
        if (!Allowed[State].Contains(next)) throw new InvalidOperationException($"Execution transition {State} -> {next} is not allowed.");
        ArgumentException.ThrowIfNullOrWhiteSpace(reasonCode);
        ArgumentException.ThrowIfNullOrWhiteSpace(evidenceHash);
        if (occurredAt < LastOccurredAt) throw new InvalidOperationException("Execution transition time cannot move backwards.");
        if (next == ModelOperationExecutionState.BusinessHold && (!holdUntil.HasValue || holdUntil <= occurredAt))
            throw new InvalidOperationException("Business hold requires a future hold-until instant.");
        if (next != ModelOperationExecutionState.BusinessHold && holdUntil.HasValue)
            throw new InvalidOperationException("Only a business hold may have a hold-until instant.");

        var transition = new ModelOperationExecutionTransition(State, next, reasonCode, evidenceHash, occurredAt, holdUntil);
        transitions.Add(transition);
        State = next;
        LastOccurredAt = occurredAt;
        HoldUntil = holdUntil;
        if (next == ModelOperationExecutionState.Running) LastHeartbeatAt = occurredAt;
        return transition;
    }

    public void RecordHeartbeat(DateTimeOffset occurredAt)
    {
        if (State != ModelOperationExecutionState.Running)
            throw new InvalidOperationException("Only a running execution may record a heartbeat.");
        if (occurredAt < LastHeartbeatAt)
            throw new InvalidOperationException("Execution heartbeat time cannot move backwards.");

        LastHeartbeatAt = occurredAt;
    }

    public bool HasExceededHeartbeatCutoff(DateTimeOffset asOf, TimeSpan allowedSilence)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(allowedSilence, TimeSpan.Zero);
        if (asOf < LastHeartbeatAt) throw new InvalidOperationException("Heartbeat cutoff cannot precede the latest heartbeat.");
        return State == ModelOperationExecutionState.Running
            && LastHeartbeatAt.HasValue
            && asOf - LastHeartbeatAt.Value > allowedSilence;
    }

    private static HashSet<ModelOperationExecutionState> Set(params ModelOperationExecutionState[] states) => new(states);
}
