using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

using AIRuntime.Runtime.Agents.Permissions;
using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Capabilities.Permissions;
using AIRuntime.Runtime.Models;
using AIRuntime.Runtime.Providers;
using AIRuntime.Runtime.Providers.Fallback;
using AIRuntime.Runtime.Providers.Health;
using AIRuntime.Runtime.Providers.Models;
using AIRuntime.Runtime.Services.Agent;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;


namespace AIRuntime.Runtime.Core
{
    /// <summary>
    /// Orchestrates conversations between the user,
    /// configured AI providers, and registered capabilities.
    /// </summary>
    public sealed class ConversationEngine
    {
        private const int MaxCapabilityResultConversationCharacters =
            8192;

        private const int MaxMalformedToolCallRepairAttempts =
            1;

        private const int MaxActionContinuationAttempts = 2;

        private const int MaxRepeatedRecoverableCapabilityFailures =
            3;

        private readonly ProviderFallbackChain _providerChain;

        private readonly CapabilityRegistry _registry;

        private readonly CapabilityDispatcher _dispatcher;

        private readonly AgentObserverCollection _observers;

        private readonly IProviderToolBuilder _toolBuilder;

        private readonly Conversation _conversation;

        private readonly IProviderFailureClassifier _failureClassifier;

        private readonly ProviderRetryPolicy _retryPolicy;

        private readonly ProviderHealthTracker _healthTracker;


        private int _maxCapabilityRounds;


        private TimeSpan _providerTimeout =
            TimeSpan.FromSeconds(
                60);


        private TimeSpan _runTimeout =
            TimeSpan.FromMinutes(
                5);


        private string _systemPrompt =
            string.Empty;


        private int _isRunning;


        //--------------------------------------------------
        // Agent State
        //--------------------------------------------------

        public AgentRunContext LastRunContext
        {
            get;
            private set;
        }


        public bool IsRunning
        {
            get
            {
                return Volatile.Read(
                    ref _isRunning) == 1;
            }
        }


        public int MessageCount
        {
            get
            {
                return _conversation.MessageCount;
            }
        }


        //--------------------------------------------------
        // Configuration
        //--------------------------------------------------

        /// <summary>
        /// Optional fixed ceiling for model-driven capability rounds.
        /// Zero, the default, allows productive progress without a fixed
        /// round limit. Duplicate-call and cancellation guards still apply.
        /// </summary>
        public int MaxCapabilityRounds
        {
            get
            {
                return _maxCapabilityRounds;
            }

            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        "Maximum capability rounds cannot be negative. Use zero for no fixed limit.");
                }


                ThrowIfRunning(
                    "Maximum capability rounds cannot be changed while an agent run is in progress.");


                _maxCapabilityRounds =
                    value;
            }
        }


        public string SystemPrompt
        {
            get
            {
                return _systemPrompt;
            }

            set
            {
                ThrowIfRunning(
                    "The system prompt cannot be changed while an agent run is in progress.");


                _systemPrompt =
                    value?.Trim() ??
                    string.Empty;
            }
        }


        /// <summary>
        /// Maximum duration allowed for each individual
        /// provider attempt.
        /// </summary>
        public TimeSpan ProviderTimeout
        {
            get
            {
                return _providerTimeout;
            }

            set
            {
                if (value <= TimeSpan.Zero)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        "Provider timeout must be greater than zero.");
                }


                ThrowIfRunning(
                    "The provider timeout cannot be changed while an agent run is in progress.");


                _providerTimeout =
                    value;
            }
        }


        public TimeSpan RunTimeout
        {
            get
            {
                return _runTimeout;
            }

            set
            {
                if (value != Timeout.InfiniteTimeSpan &&
                    value <= TimeSpan.Zero)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        "Run timeout must be greater than zero or Timeout.InfiniteTimeSpan.");
                }


                ThrowIfRunning(
                    "The run timeout cannot be changed while an agent run is in progress.");


                _runTimeout =
                    value;
            }
        }


        //--------------------------------------------------
        // Construction
        //--------------------------------------------------

        public ConversationEngine(
            ProviderFallbackChain providerChain,
            ProviderRetryPolicy retryPolicy,
            ProviderHealthTracker healthTracker,
            CapabilityRegistry registry,
            CapabilityDispatcher dispatcher,
            AgentObserverCollection observers)
            : this(
                providerChain,
                retryPolicy,
                healthTracker,
                registry,
                dispatcher,
                observers,
                new DefaultProviderFailureClassifier(),
                new ProviderToolBuilder())
        {
        }


        public ConversationEngine(
            ProviderFallbackChain providerChain,
            ProviderRetryPolicy retryPolicy,
            ProviderHealthTracker healthTracker,
            CapabilityRegistry registry,
            CapabilityDispatcher dispatcher,
            AgentObserverCollection observers,
            IProviderFailureClassifier failureClassifier,
            IProviderToolBuilder toolBuilder)
        {
            _providerChain =
                providerChain ??
                throw new ArgumentNullException(
                    nameof(providerChain));


            if (!_providerChain.HasPrimaryProvider)
            {
                throw new InvalidOperationException(
                    "A primary provider must be configured before creating the conversation engine.");
            }


            _retryPolicy =
                retryPolicy ??
                throw new ArgumentNullException(
                    nameof(retryPolicy));


            _healthTracker =
                healthTracker ??
                throw new ArgumentNullException(
                    nameof(healthTracker));


            _failureClassifier =
                failureClassifier ??
                throw new ArgumentNullException(
                    nameof(failureClassifier));


            _registry =
                registry ??
                throw new ArgumentNullException(
                    nameof(registry));


            _dispatcher =
                dispatcher ??
                throw new ArgumentNullException(
                    nameof(dispatcher));


            _observers =
                observers ??
                throw new ArgumentNullException(
                    nameof(observers));


            _toolBuilder =
                toolBuilder ??
                throw new ArgumentNullException(
                    nameof(toolBuilder));


            _conversation =
                new Conversation();
        }


        //--------------------------------------------------
        // Conversation State
        //--------------------------------------------------

        public void ResetConversation()
        {
            ThrowIfRunning(
                "The conversation cannot be reset while an agent run is in progress.");


            _conversation.Clear();
        }


        public void ClearSystemPrompt()
        {
            ThrowIfRunning(
                "The system prompt cannot be changed while an agent run is in progress.");


            _systemPrompt =
                string.Empty;
        }


        //--------------------------------------------------
        // String Response API
        //--------------------------------------------------

        public Task<string> ChatAsync(
            string prompt)
        {
            return ChatAsync(
                prompt,
                CancellationToken.None);
        }


        public Task<string> ChatAsync(
            string prompt,
            CancellationToken cancellationToken)
        {
            IReadOnlyCollection<ICapability> capabilities =
                _registry.GetAll();


            return ExecuteAsync(
                prompt,
                _conversation,
                capabilities,
                CreateDefaultCapabilityPolicy(
                    capabilities),
                ProviderRequestOptions.Default,
                runContext: null,
                cancellationToken);
        }


        //--------------------------------------------------
        // Structured Result API
        //--------------------------------------------------

        public Task<AgentRunResult> RunAsync(
            string prompt)
        {
            return RunAsync(
                prompt,
                CancellationToken.None);
        }


        public Task<AgentRunResult> RunAsync(
            string prompt,
            CancellationToken cancellationToken)
        {
            IReadOnlyCollection<ICapability> capabilities =
                _registry.GetAll();


            return ExecuteStructuredAsync(
                prompt,
                _conversation,
                capabilities,
                CreateDefaultCapabilityPolicy(
                    capabilities),
                ProviderRequestOptions.Default,
                cancellationToken);
        }


        //--------------------------------------------------
        // Scoped Agent Result API
        //--------------------------------------------------

        public Task<AgentRunResult> RunAsync(
            string prompt,
            IReadOnlyList<CapabilityDescriptor> descriptors,
            CapabilityAccessPolicy capabilityPolicy,
            ProviderRequestOptions providerOptions)
        {
            return RunAsync(
                prompt,
                descriptors,
                capabilityPolicy,
                providerOptions,
                CancellationToken.None);
        }


        public Task<AgentRunResult> RunAsync(
            string prompt,
            IReadOnlyList<CapabilityDescriptor> descriptors,
            CapabilityAccessPolicy capabilityPolicy,
            ProviderRequestOptions providerOptions,
            CancellationToken cancellationToken)
        {
            IReadOnlyCollection<ICapability> capabilities =
                ResolveRegisteredCapabilities(
                    descriptors);


            return ExecuteStructuredAsync(
                prompt,
                _conversation,
                capabilities,
                capabilityPolicy,
                providerOptions,
                cancellationToken);
        }


        public Task<AgentRunResult> RunAsync(
            string prompt,
            IReadOnlyCollection<ICapability> capabilities,
            CapabilityAccessPolicy capabilityPolicy,
            ProviderRequestOptions providerOptions)
        {
            return RunAsync(
                prompt,
                capabilities,
                capabilityPolicy,
                providerOptions,
                CancellationToken.None);
        }


        public Task<AgentRunResult> RunAsync(
            string prompt,
            IReadOnlyCollection<ICapability> capabilities,
            CapabilityAccessPolicy capabilityPolicy,
            ProviderRequestOptions providerOptions,
            CancellationToken cancellationToken)
        {
            return ExecuteStructuredAsync(
                prompt,
                _conversation,
                capabilities,
                capabilityPolicy,
                providerOptions,
                cancellationToken);
        }


        //--------------------------------------------------
        // Isolated Result API
        //--------------------------------------------------

        public Task<AgentRunResult> RunIsolatedAsync(
            string prompt)
        {
            return RunIsolatedAsync(
                prompt,
                CancellationToken.None);
        }


        public Task<AgentRunResult> RunIsolatedAsync(
            string prompt,
            CancellationToken cancellationToken)
        {
            Conversation isolatedConversation =
                new Conversation();


            IReadOnlyCollection<ICapability> capabilities =
                _registry.GetAll();


            return ExecuteStructuredAsync(
                prompt,
                isolatedConversation,
                capabilities,
                CreateDefaultCapabilityPolicy(
                    capabilities),
                ProviderRequestOptions.Default,
                cancellationToken);
        }


        //--------------------------------------------------
        // Structured Execution
        //--------------------------------------------------

        private async Task<AgentRunResult> ExecuteStructuredAsync(
            string prompt,
            Conversation conversation,
            IReadOnlyCollection<ICapability> capabilities,
            CapabilityAccessPolicy capabilityPolicy,
            ProviderRequestOptions providerOptions,
            CancellationToken cancellationToken)
        {
            AgentRunContext context =
                new AgentRunContext();


            try
            {
                string response =
                    await ExecuteAsync(
                        prompt,
                        conversation,
                        capabilities,
                        capabilityPolicy,
                        providerOptions,
                        context,
                        cancellationToken)
                        .ConfigureAwait(false);


                context.Complete();


                return CreateResult(
                    context,
                    response,
                    AgentRunStatus.Succeeded,
                    errorMessage: null);
            }
            catch (TimeoutException exception)
            {
                context.Complete();


                return CreateResult(
                    context,
                    response: string.Empty,
                    AgentRunStatus.TimedOut,
                    errorMessage: exception.Message,
                    providerFailureKind: ProviderFailureKind.Timeout);
            }
            catch (OperationCanceledException)
            {
                context.Complete();


                return CreateResult(
                    context,
                    response: string.Empty,
                    AgentRunStatus.Cancelled,
                    errorMessage: "The agent run was cancelled.",
                    providerFailureKind: ProviderFailureKind.Cancelled);
            }
            catch (Exception exception)
            {
                context.Complete();


                return CreateResult(
                    context,
                    response: string.Empty,
                    AgentRunStatus.Failed,
                    errorMessage: exception.Message,
                    providerFailureKind: ResolveProviderFailureKind(
                        exception));
            }
        }


        //--------------------------------------------------
        // Agent Execution
        //--------------------------------------------------

        private async Task<string> ExecuteAsync(
            string prompt,
            Conversation conversation,
            IReadOnlyCollection<ICapability> capabilities,
            CapabilityAccessPolicy capabilityPolicy,
            ProviderRequestOptions providerOptions,
            AgentRunContext runContext,
            CancellationToken cancellationToken)
        {
            if (string.IsNullOrWhiteSpace(prompt))
            {
                throw new ArgumentException(
                    "Prompt cannot be empty.",
                    nameof(prompt));
            }


            if (conversation == null)
            {
                throw new ArgumentNullException(
                    nameof(conversation));
            }


            if (capabilities == null)
            {
                throw new ArgumentNullException(
                    nameof(capabilities));
            }


            if (capabilityPolicy == null)
            {
                throw new ArgumentNullException(
                    nameof(capabilityPolicy));
            }


            if (providerOptions == null)
            {
                throw new ArgumentNullException(
                    nameof(providerOptions));
            }


            cancellationToken
                .ThrowIfCancellationRequested();


            if (
                Interlocked.CompareExchange(
                    ref _isRunning,
                    1,
                    0) != 0)
            {
                throw new InvalidOperationException(
                    "An agent run is already in progress.");
            }


            using CancellationTokenSource runTimeoutSource =
                new CancellationTokenSource();


            runTimeoutSource.CancelAfter(
                _runTimeout);


            using CancellationTokenSource linkedSource =
                CancellationTokenSource.CreateLinkedTokenSource(
                    cancellationToken,
                    runTimeoutSource.Token);


            try
            {
                return await ExecuteRunAsync(
                    prompt,
                    conversation,
                    capabilities,
                    capabilityPolicy,
                    providerOptions,
                    runContext,
                    linkedSource.Token,
                    cancellationToken,
                    runTimeoutSource.Token)
                    .ConfigureAwait(false);
            }
            finally
            {
                Volatile.Write(
                    ref _isRunning,
                    0);
            }
        }


        private async Task<string> ExecuteRunAsync(
            string prompt,
            Conversation conversation,
            IReadOnlyCollection<ICapability> capabilities,
            CapabilityAccessPolicy capabilityPolicy,
            ProviderRequestOptions providerOptions,
            AgentRunContext runContext,
            CancellationToken executionToken,
            CancellationToken callerToken,
            CancellationToken runTimeoutToken)
        {
            AgentRunContext context =
                runContext ??
                new AgentRunContext();


            LastRunContext =
                context;


            HashSet<string> scopedCapabilityNames =
                BuildCapabilityNameSet(
                    capabilities);


            int conversationCheckpoint =
                conversation.CreateCheckpoint();


            _observers.Publish(
                new AgentEvent(
                    AgentEventType.RunStarted,
                    context.SessionId));


            try
            {
                conversation.AddUserMessage(
                    prompt);

                Dictionary<string, string> completedCapabilityCalls =
                    new Dictionary<string, string>(
                        StringComparer.Ordinal);

                var observationProgress = new StableObservationProgress();

                HashSet<string> replayedDuplicateCalls =
                    new HashSet<string>(
                        StringComparer.Ordinal);

                HashSet<string> completedSuccessfulCalls =
                    new HashSet<string>(
                        StringComparer.Ordinal);

                Dictionary<string, int> recoverableCapabilityFailureCounts =
                    new Dictionary<string, int>(
                        StringComparer.Ordinal);

                HashSet<string> rejectedToolCallFingerprints =
                    new HashSet<string>(
                        StringComparer.Ordinal);

                int malformedToolCallRepairAttempts =
                    0;

                int actionContinuationAttempts = 0;

                for (
                    int round = 0;
                    _maxCapabilityRounds == 0 ||
                    round < _maxCapabilityRounds;
                    round++)
                {
                    executionToken
                        .ThrowIfCancellationRequested();


                    IReadOnlyList<ProviderTool> tools =
                        _toolBuilder.Build(
                            capabilities);


                    ChatCompletion completion =
                        await RequestProviderCompletionAsync(
                            conversation,
                            tools,
                            providerOptions,
                            executionToken);


                    executionToken
                        .ThrowIfCancellationRequested();


                    if (completion.HasNormalizationMessages)
                    {
                        foreach (string normalizationMessage
                                 in completion.NormalizationMessages)
                        {
                            if (string.IsNullOrWhiteSpace(
                                    normalizationMessage))
                            {
                                continue;
                            }


                            _observers.Publish(
                                new AgentEvent(
                                    AgentEventType.ModelResponseNormalized,
                                    context.SessionId,
                                    message: normalizationMessage));
                        }
                    }


                    string malformedToolCallRepairPrompt =
                        null;


                    if (completion.HasRejectedToolCalls)
                    {
                        bool repeatedRejectedCall =
                            RegisterRejectedToolCallFingerprints(
                                completion.RejectedToolCalls,
                                rejectedToolCallFingerprints);


                        if (repeatedRejectedCall)
                        {
                            throw new InvalidOperationException(
                                "The provider repeated the same malformed tool call after AIRuntime requested a schema correction. The rejected call was not executed.");
                        }


                        if (malformedToolCallRepairAttempts >=
                            MaxMalformedToolCallRepairAttempts)
                        {
                            throw new InvalidOperationException(
                                $"The provider returned another malformed tool call after {MaxMalformedToolCallRepairAttempts} schema-correction attempt. The rejected call was not executed.");
                        }


                        malformedToolCallRepairPrompt =
                            BuildMalformedToolCallRepairPrompt(
                                completion.RejectedToolCalls,
                                tools);
                    }


                    if (!completion.HasToolCalls)
                    {
                        if (!string.IsNullOrWhiteSpace(
                                malformedToolCallRepairPrompt))
                        {
                            malformedToolCallRepairAttempts++;

                            conversation.AddUserMessage(
                                malformedToolCallRepairPrompt);

                            _observers.Publish(
                                new AgentEvent(
                                    AgentEventType.ModelResponseRepair,
                                    context.SessionId,
                                    message: malformedToolCallRepairPrompt));

                            continue;
                        }


                        string content =
                            completion.Content ??
                            string.Empty;


                        conversation.AddAssistantMessage(
                            content);

                        if (tools.Count > 0 && IsUnfulfilledActionAnnouncement(content))
                        {
                            if (actionContinuationAttempts >= MaxActionContinuationAttempts)
                            {
                                throw new InvalidOperationException(
                                    "[ActionContinuationExhausted] The model repeatedly announced an action without executing it. " +
                                    "The run did not complete; inspect retained execution memory before resuming.");
                            }

                            actionContinuationAttempts++;
                            const string continuationPrompt =
                                "Your last response announced an action but did not execute it. " +
                                "Continue the existing task using the available capabilities and current evidence. " +
                                "Preserve completed work and follow the existing permissions and task scope. " +
                                "If no further action is needed, give the actual result; if blocked, explain the concrete blocker. " +
                                "Do not end with another promise to act.";
                            conversation.AddUserMessage(continuationPrompt);
                            _observers.Publish(new AgentEvent(
                                AgentEventType.ModelResponseRepair,
                                context.SessionId,
                                message: continuationPrompt));
                            continue;
                        }


                        context.Complete();


                        _observers.Publish(
                            new AgentEvent(
                                AgentEventType.RunCompleted,
                                context.SessionId));


                        return content;
                    }


                    EnforceSequentialMutationCalls(
                        completion,
                        capabilities);


                    conversation.AddAssistantToolCalls(
                        completion.ToolCalls);


                    string duplicateTerminalResult =
                        await ExecuteCapabilityCallsAsync(
                            completion,
                            conversation,
                            context,
                            scopedCapabilityNames,
                            capabilityPolicy,
                            capabilities,
                            completedCapabilityCalls,
                            replayedDuplicateCalls,
                            completedSuccessfulCalls,
                            observationProgress,
                            recoverableCapabilityFailureCounts,
                            executionToken);

                    if (!string.IsNullOrWhiteSpace(
                            duplicateTerminalResult))
                    {
                        conversation.AddAssistantMessage(
                            duplicateTerminalResult);

                        context.Complete();

                        _observers.Publish(
                            new AgentEvent(
                                AgentEventType.RunCompleted,
                                context.SessionId));

                        return duplicateTerminalResult;
                    }


                    if (!string.IsNullOrWhiteSpace(
                            malformedToolCallRepairPrompt))
                    {
                        malformedToolCallRepairAttempts++;

                        conversation.AddUserMessage(
                            malformedToolCallRepairPrompt);

                        _observers.Publish(
                            new AgentEvent(
                                AgentEventType.ModelResponseRepair,
                                context.SessionId,
                                message: malformedToolCallRepairPrompt));
                    }
                }


                throw new InvalidOperationException(
                    $"Conversation exceeded the maximum of " +
                    $"{_maxCapabilityRounds} capability rounds.");
            }
            catch (TimeoutException exception)
            {
                RollbackAndComplete(
                    conversation,
                    conversationCheckpoint,
                    context);


                _observers.Publish(
                    new AgentEvent(
                        AgentEventType.RunTimedOut,
                        context.SessionId,
                        message: exception.Message));


                throw;
            }
            catch (OperationCanceledException)
                when (
                    runTimeoutToken.IsCancellationRequested &&
                    !callerToken.IsCancellationRequested)
            {
                RollbackAndComplete(
                    conversation,
                    conversationCheckpoint,
                    context);


                string message =
                    $"The agent run exceeded the maximum duration of " +
                    $"{_runTimeout.TotalSeconds:0.###} seconds.";


                _observers.Publish(
                    new AgentEvent(
                        AgentEventType.RunTimedOut,
                        context.SessionId,
                        message: message));


                throw new TimeoutException(
                    message);
            }
            catch (OperationCanceledException)
            {
                RollbackAndComplete(
                    conversation,
                    conversationCheckpoint,
                    context);


                _observers.Publish(
                    new AgentEvent(
                        AgentEventType.RunCancelled,
                        context.SessionId));


                throw;
            }
            catch (Exception exception)
            {
                RollbackAndComplete(
                    conversation,
                    conversationCheckpoint,
                    context);


                _observers.Publish(
                    new AgentEvent(
                        AgentEventType.RunFailed,
                        context.SessionId,
                        message: exception.Message));


                throw;
            }
        }


        //--------------------------------------------------
        // Provider Execution
        //--------------------------------------------------

        private async Task<ChatCompletion> RequestProviderCompletionAsync(
            Conversation conversation,
            IReadOnlyList<ProviderTool> tools,
            ProviderRequestOptions providerOptions,
            CancellationToken cancellationToken)
        {
            IReadOnlyList<IProvider> providers =
                _providerChain.GetProvidersInPriorityOrder();


            if (providers.Count == 0)
            {
                throw new InvalidOperationException(
                    "No providers are configured.");
            }


            IReadOnlyList<ConversationMessage> providerMessages =
                BuildProviderMessages(
                    conversation);


            List<ProviderFailure> failures =
                new List<ProviderFailure>();


            List<string> skippedProviders =
                new List<string>();


            int attemptedProviderCount =
                0;


            for (
                int providerIndex = 0;
                providerIndex < providers.Count;
                providerIndex++)
            {
                cancellationToken
                    .ThrowIfCancellationRequested();


                IProvider provider =
                    providers[providerIndex];


                //--------------------------------------------------
                // Skip providers currently cooling down
                //--------------------------------------------------

                if (!_healthTracker.IsAvailable(
                        provider))
                {
                    ProviderHealthRecord health =
                        _healthTracker.GetHealth(
                            provider);


                    string cooldownDescription =
                        health.CooldownUntil.HasValue
                            ? health.CooldownUntil.Value.ToString(
                                "O")
                            : "unknown";


                    skippedProviders.Add(
                        $"'{provider.Name}' until {cooldownDescription}");
                    ProviderTransportDebug.WriteEvent(provider.Name, "PROVIDER SKIPPED",
                        new { SessionId = LastRunContext?.SessionId, Cooldown = cooldownDescription });


                    continue;
                }


                attemptedProviderCount++;


                bool hasAnotherAvailableProvider =
                    HasAvailableProviderAfter(
                        providers,
                        providerIndex);


                //--------------------------------------------------
                // Initial attempt plus configured retries
                //--------------------------------------------------

                for (
                    int attemptIndex = 0;
                    attemptIndex <= _retryPolicy.MaxRetries;
                    attemptIndex++)
                {
                    cancellationToken
                        .ThrowIfCancellationRequested();


                    _healthTracker.RecordAttempt(
                        provider);


                    Stopwatch stopwatch =
                        Stopwatch.StartNew();

                    using IDisposable trace = ProviderTransportDebug.BeginAttempt(
                        provider.Name, LastRunContext?.SessionId, attemptIndex + 1);


                    try
                    {
                        ChatCompletion completion =
                            await RequestSingleProviderAsync(
                                provider,
                                providerMessages,
                                tools,
                                providerOptions,
                                cancellationToken);


                        stopwatch.Stop();


                        if (completion == null)
                        {
                            throw new InvalidOperationException(
                                $"Provider '{provider.Name}' returned a null completion.");
                        }


                        _healthTracker.RecordSuccess(
                            provider,
                            stopwatch.Elapsed);

                        ProviderTransportDebug.WriteEvent(provider.Name, "ATTEMPT SUCCEEDED", new {
                            ElapsedMilliseconds = stopwatch.ElapsedMilliseconds,
                            Content = completion.Content, ToolCalls = completion.ToolCalls,
                            RejectedToolCalls = completion.RejectedToolCalls
                        });


                        return completion;
                    }
                    catch (OperationCanceledException)
                        when (
                            cancellationToken.IsCancellationRequested)
                    {
                        stopwatch.Stop();


                        // Caller cancellation and full-run timeout must
                        // stop retries and fallback immediately.
                        ProviderTransportDebug.WriteEvent(provider.Name, "ATTEMPT CANCELLED",
                            new { ElapsedMilliseconds = stopwatch.ElapsedMilliseconds });
                        throw;
                    }
                    catch (Exception exception)
                    {
                        stopwatch.Stop();


                        ProviderFailure failure =
                            _failureClassifier.Classify(
                                provider,
                                exception);


                        failures.Add(
                            failure);
                        ProviderTransportDebug.WriteEvent(provider.Name, "ATTEMPT FAILED", new {
                            Kind = failure.Kind.ToString(), failure.Message, failure.ShouldFallback,
                            Exception = exception.ToString(), ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
                        });


                        _healthTracker.RecordFailure(
                            provider,
                            failure,
                            stopwatch.Elapsed);


                        //--------------------------------------------------
                        // Permanent or unknown failure
                        //--------------------------------------------------

                        if (!failure.ShouldFallback)
                        {
                            throw failure.Exception;
                        }


                        bool retriesRemain =
                            attemptIndex <
                            _retryPolicy.MaxRetries;


                        bool failureIsRetryable =
                            _retryPolicy.ShouldRetry(
                                failure.Kind);


                        bool providerStillAvailable =
                            _healthTracker.IsAvailable(
                                provider);
                        ProviderTransportDebug.WriteEvent(provider.Name, "RETRY DECISION", new {
                            RetrySameProvider = retriesRemain && failureIsRetryable && providerStillAvailable,
                            HasAnotherAvailableProvider = hasAnotherAvailableProvider,
                            RetriesRemain = retriesRemain, FailureIsRetryable = failureIsRetryable,
                            ProviderStillAvailable = providerStillAvailable,
                            DelayMilliseconds = retriesRemain && failureIsRetryable && providerStillAvailable
                                ? _retryPolicy.GetDelay(attemptIndex + 1).TotalMilliseconds : 0
                        });


                        //--------------------------------------------------
                        // Retry the same provider
                        //--------------------------------------------------

                        if (
                            retriesRemain &&
                            failureIsRetryable &&
                            providerStillAvailable)
                        {
                            int retryNumber =
                                attemptIndex +
                                1;


                            TimeSpan delay =
                                _retryPolicy.GetDelay(
                                    retryNumber);


                            if (delay > TimeSpan.Zero)
                            {
                                await Task.Delay(
                                    delay,
                                    cancellationToken);
                            }


                            continue;
                        }


                        //--------------------------------------------------
                        // Move to the next available provider
                        //--------------------------------------------------

                        if (hasAnotherAvailableProvider)
                        {
                            break;
                        }


                        //--------------------------------------------------
                        // No usable provider remains
                        //--------------------------------------------------

                        throw CreateProviderChainException(
                            failures);
                    }
                }
            }


            //--------------------------------------------------
            // Every provider was cooling down
            //--------------------------------------------------

            if (attemptedProviderCount == 0)
            {
                string skippedSummary =
                    skippedProviders.Count > 0
                        ? string.Join(
                            "; ",
                            skippedProviders)
                        : "No provider health information was available.";


                throw new InvalidOperationException(
                    "All configured providers are cooling down. " +
                    $"Skipped providers: {skippedSummary}.");
            }


            throw CreateProviderChainException(
                failures);
        }


        private bool HasAvailableProviderAfter(
            IReadOnlyList<IProvider> providers,
            int currentProviderIndex)
        {
            for (
                int index = currentProviderIndex + 1;
                index < providers.Count;
                index++)
            {
                if (_healthTracker.IsAvailable(
                        providers[index]))
                {
                    return true;
                }
            }


            return false;
        }


        private async Task<ChatCompletion> RequestSingleProviderAsync(
            IProvider provider,
            IReadOnlyList<ConversationMessage> messages,
            IReadOnlyList<ProviderTool> tools,
            ProviderRequestOptions providerOptions,
            CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ProviderInputGuard.Validate(provider, messages);

            using CancellationTokenSource timeoutSource =
                new CancellationTokenSource();


            using CancellationTokenSource linkedSource =
                CancellationTokenSource.CreateLinkedTokenSource(
                    cancellationToken,
                    timeoutSource.Token);

            DateTime requestStartedUtc =
                DateTime.UtcNow;


            try
            {
                Task<ChatCompletion> providerTask;


                if (
                    provider is
                        IProviderWithRequestOptions configurableProvider)
                {
                    providerTask =
                        configurableProvider.ChatAsync(
                            messages,
                            tools,
                            providerOptions,
                            linkedSource.Token);
                }
                else
                {
                    providerTask =
                        provider.ChatAsync(
                            messages,
                            tools,
                            linkedSource.Token);
                }


                while (!providerTask.IsCompleted)
                {
                    cancellationToken
                        .ThrowIfCancellationRequested();

                    DateTime lastActivityUtc =
                        ProviderStreamingContext
                            .LastActivityUtc ??
                        requestStartedUtc;


                    if (lastActivityUtc <
                        requestStartedUtc)
                    {
                        lastActivityUtc =
                            requestStartedUtc;
                    }


                    TimeSpan remaining =
                        _providerTimeout -
                        (DateTime.UtcNow -
                         lastActivityUtc);


                    if (remaining <=
                        TimeSpan.Zero)
                    {
                        timeoutSource.Cancel();

                        throw new TimeoutException(
                            $"Provider '{provider.Name}' did not respond within " +
                            $"{_providerTimeout.TotalSeconds:0.###} seconds.");
                    }


                    Task delayTask =
                        Task.Delay(
                            remaining,
                            cancellationToken);

                    Task completedTask =
                        await Task.WhenAny(
                            providerTask,
                            delayTask);


                    if (completedTask ==
                        providerTask)
                    {
                        break;
                    }
                }


                return await providerTask;
            }
            catch (OperationCanceledException)
                when (
                    timeoutSource.IsCancellationRequested &&
                    !cancellationToken.IsCancellationRequested)
            {
                throw new TimeoutException(
                    $"Provider '{provider.Name}' did not respond within " +
                    $"{_providerTimeout.TotalSeconds:0.###} seconds.");
            }
        }


        private static Exception CreateProviderChainException(
            IReadOnlyList<ProviderFailure> failures)
        {
            if (
                failures == null ||
                failures.Count == 0)
            {
                return new InvalidOperationException(
                    "The provider chain failed without recording an error.");
            }


            if (failures.Count == 1)
            {
                return failures[0].Exception;
            }


            bool everyFailureWasTimeout =
                true;


            List<Exception> exceptions =
                new List<Exception>(
                    failures.Count);


            string attemptSummary =
                string.Empty;


            for (
                int index = 0;
                index < failures.Count;
                index++)
            {
                ProviderFailure failure =
                    failures[index];


                exceptions.Add(
                    failure.Exception);


                if (
                    failure.Kind !=
                    ProviderFailureKind.Timeout)
                {
                    everyFailureWasTimeout =
                        false;
                }


                if (index > 0)
                {
                    attemptSummary +=
                        "; ";
                }


                attemptSummary +=
                    $"'{failure.ProviderName}' " +
                    $"[{failure.Kind}]: " +
                    failure.Message;
            }


            AggregateException aggregate =
                new AggregateException(
                    exceptions);

            if (failures.All(failure => failure.Kind == ProviderFailureKind.UnsupportedInput))
                return new ProviderInputNotSupportedException(
                    "Visual verification blocked: no configured provider can accept these images. " + attemptSummary);


            if (everyFailureWasTimeout)
            {
                return new TimeoutException(
                    "All configured providers timed out. " +
                    $"Attempts: {attemptSummary}.",
                    aggregate);
            }


            return new AggregateException(
                "All eligible providers failed. " +
                $"Attempts: {attemptSummary}.",
                aggregate);
        }


        private IReadOnlyList<ConversationMessage> BuildProviderMessages(
            Conversation conversation)
        {
            bool hasSystemPrompt =
                !string.IsNullOrWhiteSpace(
                    _systemPrompt);


            int additionalMessages =
                hasSystemPrompt
                    ? 1
                    : 0;


            List<ConversationMessage> messages =
                new List<ConversationMessage>(
                    conversation.MessageCount +
                    additionalMessages);


            if (hasSystemPrompt)
            {
                messages.Add(
                    new ConversationMessage
                    {
                        Role =
                            "system",

                        Content =
                            _systemPrompt
                    });
            }


            List<ConversationAttachment> pendingArtifacts = new List<ConversationAttachment>();
            for (int messageIndex = 0; messageIndex < conversation.Messages.Count; messageIndex++)
            {
                ConversationMessage message = conversation.Messages[messageIndex];
                messages.Add(
                    message);

                if (string.Equals(message.Role, "tool", StringComparison.Ordinal) &&
                    message.Attachments != null && message.Attachments.Count > 0)
                {
                    pendingArtifacts.AddRange(message.Attachments);
                }

                bool toolBlockEnds = string.Equals(message.Role, "tool", StringComparison.Ordinal) &&
                    (messageIndex == conversation.Messages.Count - 1 ||
                     !string.Equals(conversation.Messages[messageIndex + 1].Role, "tool", StringComparison.Ordinal));
                if (toolBlockEnds && pendingArtifacts.Count > 0)
                {
                    messages.Add(new ConversationMessage
                    {
                        Role = "user",
                        Content = "Authoritative capability artifacts are attached. Inspect their actual content before making visual or media claims.",
                        Attachments = new List<ConversationAttachment>(pendingArtifacts)
                    });
                    pendingArtifacts.Clear();
                }
            }


            return messages;
        }


        //--------------------------------------------------
        // Capability Resolution
        //--------------------------------------------------

        private IReadOnlyCollection<ICapability>
            ResolveRegisteredCapabilities(
                IReadOnlyList<CapabilityDescriptor> descriptors)
        {
            if (descriptors == null)
            {
                throw new ArgumentNullException(
                    nameof(descriptors));
            }


            IReadOnlyCollection<ICapability> registered =
                _registry.GetAll();


            Dictionary<string, ICapability> registeredByName =
                new Dictionary<string, ICapability>(
                    StringComparer.Ordinal);


            foreach (
                ICapability capability
                in registered)
            {
                if (
                    capability == null ||
                    string.IsNullOrWhiteSpace(
                        capability.Name))
                {
                    continue;
                }


                registeredByName[
                    capability.Name] =
                    capability;
            }


            List<ICapability> resolved =
                new List<ICapability>(
                    descriptors.Count);


            HashSet<string> addedNames =
                new HashSet<string>(
                    StringComparer.Ordinal);


            foreach (
                CapabilityDescriptor descriptor
                in descriptors)
            {
                if (descriptor == null)
                {
                    throw new InvalidOperationException(
                        "The capability descriptor list contains a null entry.");
                }


                if (string.IsNullOrWhiteSpace(
                        descriptor.Name))
                {
                    throw new InvalidOperationException(
                        "A capability descriptor has an empty name.");
                }


                if (
                    !registeredByName.TryGetValue(
                        descriptor.Name,
                        out ICapability capability))
                {
                    throw new InvalidOperationException(
                        $"Capability '{descriptor.Name}' is not registered.");
                }


                if (addedNames.Add(
                        descriptor.Name))
                {
                    resolved.Add(
                        capability);
                }
            }


            return resolved;
        }


        private static CapabilityAccessPolicy CreateDefaultCapabilityPolicy(
            IReadOnlyCollection<ICapability> capabilities)
        {
            CapabilityAccessPolicy policy =
                new CapabilityAccessPolicy();


            foreach (
                ICapability capability
                in capabilities)
            {
                if (
                    capability != null &&
                    !string.IsNullOrWhiteSpace(
                        capability.Name))
                {
                    policy.Allow(
                        capability.Name);
                }
            }


            return policy;
        }


        private static HashSet<string> BuildCapabilityNameSet(
            IReadOnlyCollection<ICapability> capabilities)
        {
            HashSet<string> names =
                new HashSet<string>(
                    StringComparer.Ordinal);


            foreach (
                ICapability capability
                in capabilities)
            {
                if (
                    capability != null &&
                    !string.IsNullOrWhiteSpace(
                        capability.Name))
                {
                    names.Add(
                        capability.Name);
                }
            }


            return names;
        }


        private static void EnforceSequentialMutationCalls(
            ChatCompletion completion,
            IReadOnlyCollection<ICapability> capabilities)
        {
            if (completion.ToolCalls.Count <=
                1)
            {
                return;
            }

            if (AllowsSameCapabilityBatch(
                    completion.ToolCalls,
                    capabilities))
            {
                return;
            }


            ToolCall mutationCall =
                null;


            foreach (
                ToolCall call
                in completion.ToolCalls)
            {
                if (call == null)
                    continue;


                foreach (
                    ICapability capability
                    in capabilities)
                {
                    if (capability == null ||
                        !string.Equals(
                            capability.Name,
                            call.Name,
                            StringComparison.Ordinal))
                    {
                        continue;
                    }


                    if (capability.Permission !=
                        CapabilityPermission.ReadOnly)
                    {
                        mutationCall =
                            call;
                    }


                    break;
                }


                if (mutationCall != null)
                    break;
            }


            if (mutationCall == null)
                return;


            completion.ToolCalls.Clear();

            completion.ToolCalls.Add(
                mutationCall);
        }


        //--------------------------------------------------
        // Capability Execution
        //--------------------------------------------------

        private async Task<string> ExecuteCapabilityCallsAsync(
            ChatCompletion completion,
            Conversation conversation,
            AgentRunContext context,
            HashSet<string> scopedCapabilityNames,
            CapabilityAccessPolicy capabilityPolicy,
            IReadOnlyCollection<ICapability> capabilities,
            IDictionary<string, string> completedCapabilityCalls,
            ISet<string> replayedDuplicateCalls,
            ISet<string> completedSuccessfulCalls,
            StableObservationProgress observationProgress,
            IDictionary<string, int> recoverableCapabilityFailureCounts,
            CancellationToken cancellationToken)
        {
            foreach (
                ToolCall call
                in completion.ToolCalls)
            {
                cancellationToken
                    .ThrowIfCancellationRequested();


                if (call == null)
                {
                    throw new InvalidOperationException(
                        "The provider returned a null tool call.");
                }


                if (string.IsNullOrWhiteSpace(
                        call.Name))
                {
                    throw new InvalidOperationException(
                        "The provider returned a tool call with an empty capability name.");
                }


                if (!scopedCapabilityNames.Contains(
                        call.Name))
                {
                    throw new InvalidOperationException(
                        $"Capability '{call.Name}' is not available during this execution.");
                }


                if (!capabilityPolicy.CanUse(
                        call.Name))
                {
                    throw new InvalidOperationException(
                        $"Capability '{call.Name}' is not permitted for this agent.");
                }


                string callFingerprint =
                    CreateCapabilityCallFingerprint(
                        call);

                bool mutationCapability =
                    IsMutationCapability(
                        call.Name,
                        capabilities);

                bool stableObservationCapability =
                    IsStableObservationCapability(
                        call.Name,
                        capabilities);

                if ((mutationCapability ||
                     stableObservationCapability) &&
                    completedCapabilityCalls.TryGetValue(
                        callFingerprint,
                        out string previousResult))
                {
                    bool previousCallSucceeded =
                        completedSuccessfulCalls.Contains(
                            callFingerprint);

                    if (!replayedDuplicateCalls.Add(
                            callFingerprint))
                    {
                        if (mutationCapability &&
                            previousCallSucceeded &&
                            CanCompleteFromRepeatedResult(
                                call.Name,
                                capabilities))
                        {
                            return
                                $"Completed using the authoritative result from the already-finished verification capability '{call.Name}'. " +
                                "The repeated call was not executed again.\n\n" +
                                BoundCapabilityResultForConversation(
                                    call.Name,
                                    previousResult);
                        }

                        if (stableObservationCapability)
                        {
                            throw new InvalidOperationException(
                                "[CapabilityNoProgress] The provider repeated the identical capability call '" +
                                call.Name +
                                "' after AIRuntime had already replayed unchanged authoritative evidence. " +
                                "The run was stopped to prevent a no-progress tool-call loop.");
                        }

                        if (mutationCapability &&
                            !previousCallSucceeded)
                        {
                            throw new InvalidOperationException(
                                "[CapabilityNoProgress] The provider repeated the identical failed capability call '" +
                                call.Name +
                                "' after AIRuntime had already replayed its authoritative failure. " +
                                "The run was stopped to prevent a no-progress tool-call loop.");
                        }

                    }

                    string duplicateResult =
                        JsonConvert.SerializeObject(
                            new
                            {
                                Succeeded = previousCallSucceeded,
                                DuplicateIgnored = true,
                                StableObservation =
                                    stableObservationCapability,
                                StateChangedSincePreviousCall = false,
                                Message =
                                    previousCallSucceeded
                                        ? "This identical capability call already completed successfully and was not executed again. Use PreviousResult, choose a materially different action, or provide the final answer now. Repeating this unchanged observation again will stop the run as no progress."
                                        : "This identical capability call already failed and was not executed again. Use PreviousResult, correct the arguments or environment, then choose a distinct next action.",
                                PreviousResult =
                                    BoundCapabilityResultForConversation(
                                        call.Name,
                                        previousResult)
                            });

                    conversation.AddCapabilityResult(
                        call.Id,
                        call.Name,
                        BoundCapabilityResultForConversation(
                            call.Name,
                            duplicateResult));

                    continue;
                }


                _observers.Publish(
                    new AgentEvent(
                        AgentEventType.ToolStarted,
                        context.SessionId,
                        call.Name,
                        call.Arguments));


                try
                {
                    string result =
                        await _dispatcher.DispatchAsync(
                            call,
                            context.SessionId,
                            cancellationToken);


                    cancellationToken
                        .ThrowIfCancellationRequested();


                    context.RecordToolCall();


                    _observers.Publish(
                        new AgentEvent(
                            AgentEventType.ToolCompleted,
                            context.SessionId,
                            call.Name,
                            result));


                    conversation.AddCapabilityResult(
                        call.Id,
                        call.Name,
                        BoundCapabilityResultForConversation(
                            call.Name,
                            result));

                    if (mutationCapability)
                    {
                        completedCapabilityCalls.Clear();
                        replayedDuplicateCalls.Clear();
                        completedSuccessfulCalls.Clear();
                        recoverableCapabilityFailureCounts.Clear();
                        observationProgress.Clear();
                    }
                    else if (stableObservationCapability)
                    {
                        observationProgress.Record(call.Name, callFingerprint, result, replayedDuplicateCalls);
                    }

                    if (mutationCapability ||
                        stableObservationCapability)
                    {
                        completedCapabilityCalls[callFingerprint] =
                            result;

                        completedSuccessfulCalls.Add(
                            callFingerprint);
                    }
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch (Exception exception)
                {
                    _observers.Publish(
                        new AgentEvent(
                            AgentEventType.ToolFailed,
                            context.SessionId,
                            call.Name,
                            exception.Message));

                    if (!IsRecoverableCapabilityFailure(
                            exception))
                    {
                        throw;
                    }


                    string failureFingerprint =
                        CreateRecoverableCapabilityFailureFingerprint(
                            call,
                            exception);

                    int repeatedFailureCount =
                        recoverableCapabilityFailureCounts.TryGetValue(
                            failureFingerprint,
                            out int existingFailureCount)
                            ? existingFailureCount + 1
                            : 1;

                    recoverableCapabilityFailureCounts[
                        failureFingerprint] = repeatedFailureCount;

                    if (repeatedFailureCount >=
                        MaxRepeatedRecoverableCapabilityFailures)
                    {
                        throw new InvalidOperationException(
                            "[CapabilityNoProgress] Capability '" +
                            call.Name +
                            "' failed " +
                            repeatedFailureCount +
                            " times with the same actionable error while no mutation succeeded. " +
                            "The run was stopped to prevent superficial argument changes from extending a no-progress loop. " +
                            "Last failure: " +
                            (exception.Message ?? string.Empty),
                            exception);
                    }


                    string errorResult =
                        JsonConvert.SerializeObject(
                            new
                            {
                                Succeeded = false,
                                IsError = true,
                                Error = exception.Message,
                                Message =
                                    "The capability call failed. Correct the arguments or choose another available capability, then continue the task."
                            });

                    context.RecordToolCall();

                    conversation.AddCapabilityResult(
                        call.Id,
                        call.Name,
                        errorResult);

                    // Preserve mutation idempotency while allowing repeated
                    // live inspections to execute again.
                    if (mutationCapability ||
                        stableObservationCapability)
                    {
                        completedSuccessfulCalls.Remove(
                            callFingerprint);

                        completedCapabilityCalls[callFingerprint] =
                            errorResult;
                    }

                    continue;
                }
            }


            return null;
        }


        private static string CreateRecoverableCapabilityFailureFingerprint(
            ToolCall call,
            Exception exception)
        {
            return string.Join(
                "\n",
                call?.Name?.Trim() ?? string.Empty,
                exception?.GetType().FullName ?? string.Empty,
                exception?.Message?.Trim() ?? string.Empty);
        }


        private static bool IsRecoverableCapabilityFailure(
            Exception exception)
        {
            // Permission denial and explicit user rejection are authorization
            // decisions, not correctable tool arguments. Returning either to
            // the model as a recoverable result would invite it to retry an
            // operation the user or policy has already refused.
            if (ContainsCapabilityPermissionFailure(
                    exception))
            {
                return false;
            }


            return exception is ValidationException ||
                   exception is ArgumentException ||
                   exception is InvalidOperationException ||
                   exception is System.IO.IOException ||
                   exception is UnauthorizedAccessException ||
                   exception is JsonException ||
                   exception is NotSupportedException;
        }


        private static bool ContainsCapabilityPermissionFailure(
            Exception exception)
        {
            if (exception ==
                null)
            {
                return false;
            }


            if (exception is CapabilityPermissionException)
            {
                return true;
            }


            // Some Unity async execution paths preserve the dispatcher
            // message while normalizing the concrete exception type. These
            // phrases are emitted only by CapabilityPermissionException's
            // factory methods, so retain terminal authorization semantics
            // even when that type information has been lost.
            string message =
                exception.Message ??
                string.Empty;

            if (message.StartsWith(
                    "Capability '",
                    StringComparison.Ordinal) &&
                (message.Contains(
                     " was rejected during approval.") ||
                 message.Contains(
                     " requires approval for permission '") ||
                 message.Contains(
                     " was denied because permission '")))
            {
                return true;
            }


            if (exception is AggregateException aggregateException)
            {
                foreach (Exception innerException in
                         aggregateException.InnerExceptions)
                {
                    if (ContainsCapabilityPermissionFailure(
                            innerException))
                    {
                        return true;
                    }
                }
            }


            return exception.InnerException !=
                       null &&
                   ContainsCapabilityPermissionFailure(
                       exception.InnerException);
        }


        // A read-only verification can progress without a mutation. Only a
        // previously unseen authoritative result grants another cached read;
        // argument variants and A/B/A/B cycles do not reset the allowance.
        private sealed class StableObservationProgress
        {
            private readonly HashSet<string> _results = new HashSet<string>(StringComparer.Ordinal);
            private readonly HashSet<string> _calls = new HashSet<string>(StringComparer.Ordinal);
            public void Clear() { _results.Clear(); _calls.Clear(); }
            public void Record(string name, string call, string result, ISet<string> replayed)
            {
                _calls.Add(call);
                string normalized = result ?? string.Empty;
                try
                {
                    var json = JToken.Parse(normalized);
                    if (json is JObject value &&
                        (value["Succeeded"]?.Type == JTokenType.Boolean && value["Succeeded"].Value<bool>() == false ||
                         value["IsError"]?.Type == JTokenType.Boolean && value["IsError"].Value<bool>())) return;
                    normalized = json.ToString(Formatting.None);
                }
                catch (JsonException) { }
                string fingerprint;
                using (var hash = System.Security.Cryptography.SHA256.Create())
                    fingerprint = Convert.ToBase64String(hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(name + "\n" + normalized)));
                if (_results.Add(fingerprint))
                    foreach (string observedCall in _calls) replayed.Remove(observedCall);
            }
        }

        private static string CreateCapabilityCallFingerprint(
            ToolCall call)
        {
            string arguments =
                call.Arguments?.Trim() ??
                string.Empty;

            try
            {
                arguments =
                    JObject.Parse(
                            arguments)
                        .ToString(
                            Formatting.None);
            }
            catch (JsonException)
            {
                // Argument validation reports malformed JSON elsewhere. Keep
                // the original text here so duplicate detection stays safe.
            }

            return call.Name.Trim() +
                   "\n" +
                   arguments;
        }


        private static bool RegisterRejectedToolCallFingerprints(
            IReadOnlyList<RejectedToolCall> rejectedToolCalls,
            ISet<string> fingerprints)
        {
            bool repeated =
                false;

            HashSet<string> currentResponseFingerprints =
                new HashSet<string>(
                    StringComparer.Ordinal);


            foreach (RejectedToolCall rejected in rejectedToolCalls)
            {
                if (rejected == null)
                    continue;


                string fingerprint =
                    (rejected.Name ?? string.Empty).Trim() +
                    "\n" +
                    (rejected.Arguments ?? string.Empty).Trim() +
                    "\n" +
                    (rejected.Reason ?? string.Empty).Trim();


                currentResponseFingerprints.Add(
                    fingerprint);
            }


            foreach (string fingerprint in currentResponseFingerprints)
            {
                if (fingerprints.Contains(
                        fingerprint))
                {
                    repeated =
                        true;
                }
            }


            fingerprints.UnionWith(
                currentResponseFingerprints);


            return repeated;
        }


        private static string BuildMalformedToolCallRepairPrompt(
            IReadOnlyList<RejectedToolCall> rejectedToolCalls,
            IReadOnlyList<ProviderTool> offeredTools)
        {
            JArray rejected =
                new JArray();


            HashSet<string> rejectedNames =
                new HashSet<string>(
                    StringComparer.Ordinal);


            foreach (RejectedToolCall call in rejectedToolCalls)
            {
                if (call == null)
                    continue;


                rejected.Add(
                    new JObject
                    {
                        ["index"] = call.Index,
                        ["id"] = call.Id ?? string.Empty,
                        ["name"] = call.Name ?? string.Empty,
                        ["arguments"] = call.Arguments ?? string.Empty,
                        ["problem"] = call.Reason ?? string.Empty
                    });


                if (!string.IsNullOrWhiteSpace(
                        call.Name))
                {
                    rejectedNames.Add(
                        call.Name.Trim());
                }
            }


            JArray schemas =
                new JArray();


            if (offeredTools != null)
            {
                foreach (ProviderTool tool in offeredTools)
                {
                    if (tool?.Function == null ||
                        !rejectedNames.Contains(
                            tool.Function.Name ?? string.Empty))
                    {
                        continue;
                    }


                    schemas.Add(
                        new JObject
                        {
                            ["name"] = tool.Function.Name,
                            ["argumentsSchema"] =
                                tool.Function.Parameters == null
                                    ? new JObject()
                                    : JToken.FromObject(
                                        tool.Function.Parameters)
                        });
                }
            }


            return
                "AIRuntime rejected part of your previous response because its capability-call arguments did not match the tool protocol. Nothing in the rejected calls below was executed. Correct the rejected call now using the offered capability schema. The function.arguments value must be one JSON object, not a JSON-encoded string, array, primitive, prose, or markup. Do not repeat capability calls that already have tool results in the conversation. Return a normal assistant answer only if no capability call is needed.\n\n" +
                "Rejected calls:\n" +
                rejected.ToString(
                    Formatting.Indented) +
                "\n\nMatching schemas:\n" +
                schemas.ToString(
                    Formatting.Indented);
        }


        private static bool IsMutationCapability(
            string capabilityName,
            IEnumerable<ICapability> capabilities)
        {
            foreach (ICapability capability in capabilities)
            {
                if (capability != null &&
                    string.Equals(
                        capability.Name,
                        capabilityName,
                        StringComparison.Ordinal))
                {
                    return capability.Permission !=
                           CapabilityPermission.ReadOnly;
                }
            }

            return false;
        }


        private static bool IsStableObservationCapability(
            string capabilityName,
            IEnumerable<ICapability> capabilities)
        {
            ICapability capability =
                FindCapability(
                    capabilityName,
                    capabilities);

            return capability is
                       ICapabilityObservationSemantics semantics &&
                   semantics.ObservationBehavior ==
                       CapabilityObservationBehavior.StableUntilMutation;
        }


        private static bool AllowsSameCapabilityBatch(
            IReadOnlyList<ToolCall> calls,
            IEnumerable<ICapability> capabilities)
        {
            if (calls == null ||
                calls.Count <= 1 ||
                calls[0] == null ||
                string.IsNullOrWhiteSpace(
                    calls[0].Name))
            {
                return false;
            }


            string capabilityName =
                calls[0].Name;


            ICapability capability =
                FindCapability(
                    capabilityName,
                    capabilities);


            if (!(capability is
                    ICapabilityExecutionSemantics semantics) ||
                semantics.BatchBehavior !=
                    CapabilityBatchBehavior.AllowSameCapabilityBatch)
            {
                return false;
            }


            return calls.All(
                call =>
                    call != null &&
                    string.Equals(
                        call.Name,
                        capabilityName,
                        StringComparison.Ordinal));
        }


        // Deliberately narrow: ordinary answers, questions, explanations and
        // conditional offers must remain terminal. This is a recovery hint,
        // never evidence that a task has passed its acceptance criteria.
        private static bool IsUnfulfilledActionAnnouncement(string content)
        {
            if (string.IsNullOrWhiteSpace(content) || content.Length > 400 ||
                content.IndexOf('?') >= 0 || content.Contains("```") ||
                content.IndexOf("\n", StringComparison.Ordinal) >= 0)
                return false;

            return Regex.IsMatch(content.Trim(),
                @"^(?:I'll|I’ll|I will|Let me|I'm going to|I’m going to)\s+(?:now\s+)?(?:inspect|check|read|search|run|test|verify|create|write|edit|update|fix|implement|open|save)\b(?:[^.!?]|\.(?=\S))*(?:\.{1,3}|…)?$",
                RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) &&
                !Regex.IsMatch(content, @"\b(?:if|unless|would|could|permission|approval)\b",
                    RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        }


        private static bool CanCompleteFromRepeatedResult(
            string capabilityName,
            IEnumerable<ICapability> capabilities)
        {
            ICapability capability =
                FindCapability(
                    capabilityName,
                    capabilities);


            return capability is
                       ICapabilityExecutionSemantics semantics &&
                   semantics.RepeatedCallBehavior ==
                       CapabilityRepeatedCallBehavior.CompleteRunFromPreviousResult;
        }


        private static ICapability FindCapability(
            string capabilityName,
            IEnumerable<ICapability> capabilities)
        {
            if (string.IsNullOrWhiteSpace(
                    capabilityName) ||
                capabilities == null)
            {
                return null;
            }


            return capabilities.FirstOrDefault(
                capability =>
                    capability != null &&
                    string.Equals(
                        capability.Name,
                        capabilityName,
                        StringComparison.Ordinal));
        }


        //--------------------------------------------------
        // Shared Helpers
        //--------------------------------------------------

        private static string BoundCapabilityResultForConversation(
            string capabilityName,
            string result)
        {
            string value =
                result ??
                string.Empty;

            if (value.Length <=
                MaxCapabilityResultConversationCharacters)
            {
                return value;
            }

            string header =
                $"[AIRuntime bounded capability result: '{capabilityName}', original characters={value.Length}. " +
                "The full result remains available to AIRuntime verification but is not repeated in the model conversation. " +
                "Showing beginning and ending excerpts; refine the tool query if omitted details are required.]\n";

            int excerptBudget =
                MaxCapabilityResultConversationCharacters -
                header.Length -
                32;

            int beginningLength =
                excerptBudget *
                3 /
                4;

            int endingLength =
                excerptBudget -
                beginningLength;

            return header +
                   value.Substring(
                       0,
                       beginningLength) +
                   "\n...[bounded]...\n" +
                   value.Substring(
                       value.Length -
                       endingLength);
        }

        private static void RollbackAndComplete(
            Conversation conversation,
            int checkpoint,
            AgentRunContext context)
        {
            conversation.RollbackToCheckpoint(
                checkpoint);


            context.Complete();
        }


        private void ThrowIfRunning(
            string message)
        {
            if (IsRunning)
            {
                throw new InvalidOperationException(
                    message);
            }
        }


        //--------------------------------------------------
        // Result Creation
        //--------------------------------------------------

        private static AgentRunResult CreateResult(
            AgentRunContext context,
            string response,
            AgentRunStatus status,
            string errorMessage,
            ProviderFailureKind? providerFailureKind = null)
        {
            return new AgentRunResult
            {
                Response =
                    response ??
                    string.Empty,

                SessionId =
                    context?.SessionId ??
                    string.Empty,

                ToolCallsExecuted =
                    context?.ToolCallsExecuted ??
                    0,

                Duration =
                    context?.Duration ??
                    TimeSpan.Zero,

                Status =
                    status,

                ErrorMessage =
                    errorMessage,

                ProviderFailureKind =
                    providerFailureKind
            };
        }


        private static ProviderFailureKind? ResolveProviderFailureKind(
            Exception exception)
        {
            if (exception == null)
                return null;

            if (exception is ProviderExecutionException providerExecution)
                return providerExecution.FailureKind;

            if (exception is ProviderInputNotSupportedException)
                return ProviderFailureKind.UnsupportedInput;

            if (exception is ProviderEmptyResponseException)
                return ProviderFailureKind.EmptyResponse;

            if (exception is TimeoutException)
                return ProviderFailureKind.Timeout;

            if (exception is OperationCanceledException)
                return ProviderFailureKind.Cancelled;

            if (exception is AggregateException aggregate)
            {
                foreach (Exception inner in aggregate.Flatten().InnerExceptions)
                {
                    ProviderFailureKind? kind =
                        ResolveProviderFailureKind(inner);

                    if (kind.HasValue)
                        return kind;
                }
            }

            return ResolveProviderFailureKind(
                exception.InnerException);
        }
    }
}
