using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;

using AIRuntime.Runtime.Providers.Fallback;

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

using UnityEngine.Networking;


namespace AIRuntime.Runtime.Providers
{
    /// <summary>An error carried inside a successful streaming HTTP response.</summary>
    public sealed class ProviderStreamErrorException : InvalidOperationException
    {
        public string ProviderName { get; }
        public long ErrorCode { get; }
        public string ErrorType { get; }
        public string ErrorBody { get; }

        internal ProviderStreamErrorException(string providerName, long errorCode, string errorType, string errorBody)
            : base($"{providerName} returned a streaming error (provider code {errorCode}, type {errorType}): {errorBody}")
        {
            ProviderName = providerName;
            ErrorCode = errorCode;
            ErrorType = errorType;
            ErrorBody = errorBody;
        }
    }

    /// <summary>
    /// A provider request completed without an assistant payload. The
    /// dedicated type lets retry, fallback, and durable workflow recovery
    /// distinguish this safe-to-replay condition from malformed tool calls.
    /// </summary>
    public sealed class ProviderEmptyResponseException :
        InvalidOperationException
    {
        public string ProviderName { get; }


        public ProviderEmptyResponseException(
            string providerName)
            : base(
                $"{NormalizeProviderName(providerName)} returned an invalid response containing neither assistant text nor tool calls.")
        {
            ProviderName =
                NormalizeProviderName(providerName);
        }


        private static string NormalizeProviderName(
            string providerName)
        {
            return string.IsNullOrWhiteSpace(providerName)
                ? "Provider"
                : providerName.Trim();
        }
    }


    /// <summary>
    /// Preserves a classified provider failure across the agent-run boundary
    /// so durable orchestrators never have to reverse-engineer error text.
    /// </summary>
    public sealed class ProviderExecutionException :
        InvalidOperationException
    {
        public ProviderFailureKind FailureKind { get; }


        public ProviderExecutionException(
            ProviderFailureKind failureKind,
            string message)
            : base(
                string.IsNullOrWhiteSpace(message)
                    ? "The configured provider could not complete the agent run."
                    : message)
        {
            FailureKind = failureKind;
        }
    }


    /// <summary>
    /// Tracks live UnityWebRequests so Editor domain-reload preparation can
    /// release DownloadHandlerScript native GC handles in the owning domain.
    /// </summary>
    public static class ProviderRequestLifetime
    {
        private static readonly object Gate =
            new object();

        private static readonly HashSet<UnityWebRequest> ActiveRequests =
            new HashSet<UnityWebRequest>();


        public static void Register(
            UnityWebRequest request)
        {
            if (request == null)
            {
                return;
            }

            lock (Gate)
            {
                ActiveRequests.Add(
                    request);
            }
        }


        public static void Unregister(
            UnityWebRequest request)
        {
            if (request == null)
            {
                return;
            }

            lock (Gate)
            {
                ActiveRequests.Remove(
                    request);
            }
        }


        public static void AbortAndDisposeAll()
        {
            UnityWebRequest[] requests;

            lock (Gate)
            {
                requests =
                    ActiveRequests.ToArray();

                ActiveRequests.Clear();
            }

            foreach (UnityWebRequest request in requests)
            {
                if (request == null)
                {
                    continue;
                }

                try
                {
                    request.Abort();
                }
                catch
                {
                    // Continue releasing the remaining provider transports.
                }

                try
                {
                    request.Dispose();
                }
                catch
                {
                    // Domain shutdown must remain best-effort and non-throwing.
                }
            }
        }
    }


    public sealed class ProviderTransportRecord
    {
        public string RequestId { get; internal set; }
        public string SessionId { get; internal set; }
        public int Attempt { get; internal set; }
        public JObject Metadata { get; internal set; }
        public DateTime TimestampUtc { get; internal set; }
        public string ProviderName { get; internal set; }
        public string Direction { get; internal set; }
        public string Endpoint { get; internal set; }
        public string Payload { get; internal set; }
        public bool IsStreaming { get; internal set; }
        public string StreamTerminationSignal { get; internal set; }
    }


    /// <summary>
    /// Full provider payload trace with an optional append-only local journal.
    /// Reusable authentication material is redacted before publication.
    /// </summary>
    public static class ProviderTransportDebug
    {
        private const int MaximumRecords =
            1000;

        private static readonly object Sync =
            new object();

        private static readonly List<ProviderTransportRecord> Records =
            new List<ProviderTransportRecord>();

        public static event Action<ProviderTransportRecord> RecordPublished;

        private sealed class TraceScope : IDisposable
        {
            public string RequestId = Guid.NewGuid().ToString("N");
            public string SessionId;
            public int Attempt;
            public TraceScope Previous;
            public void Dispose() => Current.Value = Previous;
        }
        private static readonly AsyncLocal<TraceScope> Current = new AsyncLocal<TraceScope>();
        private static readonly HashSet<string> Secrets = new HashSet<string>(StringComparer.Ordinal);
        private static string _journalDirectory;
        public static string LastJournalError { get; private set; }
        public static string JournalDirectory { get { lock (Sync) return _journalDirectory; } }

        public static void ConfigureFileLogging(string directory)
        {
            lock (Sync)
            {
                _journalDirectory = string.IsNullOrWhiteSpace(directory) ? null : Path.GetFullPath(directory);
                LastJournalError = null;
            }
        }

        public static IDisposable BeginAttempt(string providerName, string sessionId, int attempt)
        {
            var scope = new TraceScope { SessionId = sessionId, Attempt = attempt, Previous = Current.Value };
            Current.Value = scope;
            WriteEvent(providerName, "ATTEMPT START", new { attempt });
            return scope;
        }

        private sealed class EmptyScope : IDisposable { public void Dispose() { } }
        internal static IDisposable EnsureAttempt(string providerName) => Current.Value == null
            ? BeginAttempt(providerName, string.Empty, 1) : new EmptyScope();

        internal static void RegisterCredential(string credential)
        {
            if (!string.IsNullOrWhiteSpace(credential))
                lock (Sync) Secrets.Add(credential);
        }

        public static void WriteEvent(string providerName, string stage, object details, string sessionId = null)
        {
            if (sessionId == null || Current.Value?.SessionId == sessionId)
            {
                Publish(providerName, stage, string.Empty, JsonConvert.SerializeObject(details), false);
                return;
            }

            // Tool/lifecycle events belong to the run, outside any HTTP attempt.
            using (var scope = new TraceScope { SessionId = sessionId, RequestId = string.Empty, Previous = Current.Value })
            {
                Current.Value = scope;
                Publish(providerName, stage, string.Empty, JsonConvert.SerializeObject(details), false);
            }
        }

        internal static JObject RedactHeaders(IEnumerable<KeyValuePair<string, string>> headers)
        {
            var result = new JObject();
            foreach (var header in headers ?? Enumerable.Empty<KeyValuePair<string, string>>())
            {
                string key = header.Key.ToLowerInvariant();
                result[header.Key] = key.Contains("authorization") || key.Contains("cookie") ||
                    key.Contains("key") || key.Contains("token") || key.Contains("secret")
                    ? "[REDACTED; present]" : header.Value;
            }
            return result;
        }

        private static string Redact(string value)
        {
            string safe = value ?? string.Empty;
            lock (Sync)
                foreach (string secret in Secrets.OrderByDescending(secret => secret.Length))
                    safe = safe.Replace(secret, "[REDACTED CREDENTIAL]");
            return safe;
        }

        private static void Stamp(ProviderTransportRecord record)
        {
            record.RequestId = Current.Value?.RequestId ?? Guid.NewGuid().ToString("N");
            record.SessionId = Current.Value?.SessionId ?? string.Empty;
            record.Attempt = Current.Value?.Attempt ?? 0;
            record.Payload = Redact(record.Payload);
            record.Endpoint = Redact(record.Endpoint);
        }

        private static void Journal(ProviderTransportRecord record, string direction = null, string payload = null)
        {
            lock (Sync)
            {
                if (_journalDirectory == null) return;
                try
                {
                    Directory.CreateDirectory(_journalDirectory);
                    string path = Path.Combine(_journalDirectory,
                        $"provider-{DateTime.UtcNow:yyyy-MM-dd}-{System.Diagnostics.Process.GetCurrentProcess().Id}.jsonl");
                    File.AppendAllText(path, Redact(JsonConvert.SerializeObject(new {
                        TimestampUtc = DateTime.UtcNow, record.RequestId, record.SessionId, record.Attempt,
                        record.ProviderName, Direction = direction ?? record.Direction, record.Endpoint,
                        Payload = payload ?? record.Payload, record.IsStreaming, record.StreamTerminationSignal,
                        record.Metadata
                    })) + Environment.NewLine, new UTF8Encoding(false));
                }
                catch (Exception error)
                {
                    if (LastJournalError == null)
                        UnityEngine.Debug.LogWarning("[ProviderTrace] Could not write diagnostics: " + Redact(error.Message));
                    LastJournalError = Redact(error.Message);
                }
            }
        }


        public static IReadOnlyList<ProviderTransportRecord> Snapshot()
        {
            lock (Sync)
            {
                return Records
                    .ToArray();
            }
        }


        public static void Clear()
        {
            lock (Sync)
            {
                Records.Clear();
            }
        }


        internal static void Publish(
            string providerName,
            string direction,
            string endpoint,
            string payload,
            bool isStreaming,
            JObject metadata = null)
        {
            ProviderTransportRecord record =
                new ProviderTransportRecord
                {
                    TimestampUtc =
                        DateTime.UtcNow,
                    ProviderName =
                        providerName ??
                        "Provider",
                    Direction =
                        direction ??
                        string.Empty,
                    Endpoint =
                        endpoint ??
                        string.Empty,
                    Payload =
                        payload ??
                        string.Empty,
                    IsStreaming =
                        isStreaming,
                    Metadata = metadata
                };

            Stamp(record);
            if (string.Equals(direction, "HTTP RESPONSE", StringComparison.Ordinal))
                ProviderTokenUsage.Shared.RecordResponse(record.RequestId, record.Payload);
            Journal(record);


            lock (Sync)
            {
                Records.Add(
                    record);


                if (Records.Count >
                    MaximumRecords)
                {
                    Records.RemoveRange(
                        0,
                        Records.Count -
                        MaximumRecords);
                }
            }


            PublishRecord(
                record);
        }


        internal static ProviderTransportRecord BeginStreamingResponse(
            string providerName,
            string endpoint)
        {
            ProviderTransportRecord record =
                new ProviderTransportRecord
                {
                    TimestampUtc =
                        DateTime.UtcNow,
                    ProviderName =
                        providerName ??
                        "Provider",
                    Direction =
                        "RESPONSE STREAM",
                    Endpoint =
                        endpoint ??
                        string.Empty,
                    Payload =
                        string.Empty,
                    IsStreaming =
                        true
                };

            Stamp(record);
            Journal(record, "STREAM START", string.Empty);


            lock (Sync)
            {
                Records.Add(
                    record);


                if (Records.Count >
                    MaximumRecords)
                {
                    Records.RemoveRange(
                        0,
                        Records.Count -
                        MaximumRecords);
                }
            }


            PublishRecord(
                record);

            return record;
        }


        internal static void AppendStreamingResponse(
            ProviderTransportRecord record,
            string payload)
        {
            if (record == null ||
                string.IsNullOrEmpty(
                    payload))
            {
                return;
            }


            lock (Sync)
            {
                record.Payload = Redact(
                    (record.Payload ??
                     string.Empty) +
                    payload);
            }

            // Store only the new chunk: writing the accumulated stream on
            // every callback would make journal growth quadratic.
            Journal(record, "STREAM CHUNK", payload);


            PublishRecord(
                record);
        }


        internal static void MarkStreamingResponseCompleted(
            ProviderTransportRecord record,
            string terminationSignal)
        {
            if (record == null ||
                string.IsNullOrWhiteSpace(
                    terminationSignal))
            {
                return;
            }


            lock (Sync)
            {
                record.StreamTerminationSignal =
                    terminationSignal.Trim();
            }

            Journal(record, "STREAM END", string.Empty);


            PublishRecord(
                record);
        }


        private static void PublishRecord(
            ProviderTransportRecord record)
        {
            Delegate[] subscribers =
                RecordPublished
                    ?.GetInvocationList();


            if (subscribers ==
                null)
            {
                return;
            }


            foreach (
                Action<ProviderTransportRecord> subscriber
                in subscribers)
            {
                try
                {
                    subscriber(
                        record);
                }
                catch
                {
                    // Diagnostics must never break provider transport.
                }
            }
        }
    }


    /// <summary>
    /// Flows a text-delta observer through one asynchronous provider run.
    /// </summary>
    public static class ProviderStreamingContext
    {
        private sealed class StreamingState
        {
            private readonly Action<string> _observer;

            private long _lastActivityTicks;


            public StreamingState(
                Action<string> observer)
            {
                _observer =
                    observer;

                Touch();
            }


            public DateTime LastActivityUtc =>
                new DateTime(
                    Interlocked.Read(
                        ref _lastActivityTicks),
                    DateTimeKind.Utc);


            public void Emit(
                string text)
            {
                Touch();


                if (!string.IsNullOrEmpty(
                        text))
                {
                    _observer(
                        text);
                }
            }


            public void Touch()
            {
                Interlocked.Exchange(
                    ref _lastActivityTicks,
                    DateTime.UtcNow.Ticks);
            }
        }


        private sealed class Scope :
            IDisposable
        {
            private readonly StreamingState _previous;

            public Scope(
                Action<string> observer)
            {
                _previous =
                    State.Value;

                State.Value =
                    new StreamingState(
                        observer);
            }


            public void Dispose()
            {
                State.Value =
                    _previous;
            }
        }


        private static readonly AsyncLocal<StreamingState> State =
            new AsyncLocal<StreamingState>();

        public static bool IsActive =>
            State.Value !=
            null;

        internal static DateTime? LastActivityUtc =>
            State.Value
                ?.LastActivityUtc;


        public static IDisposable Begin(
            Action<string> observer)
        {
            if (observer == null)
            {
                throw new ArgumentNullException(
                    nameof(observer));
            }


            return new Scope(
                observer);
        }


        internal static void Emit(
            string text)
        {
            if (!string.IsNullOrEmpty(
                    text))
            {
                State.Value?.Emit(
                    text);
            }
        }


        internal static Action<string> Capture()
        {
            StreamingState state =
                State.Value;

            return state ==
                    null
                ? null
                : state.Emit;
        }


        internal static Action CaptureActivity()
        {
            StreamingState state =
                State.Value;

            return state ==
                    null
                ? null
                : state.Touch;
        }
    }


    /// <summary>
    /// Immutable declaration of the terminal frames accepted from a provider
    /// stream. The transport owns framing and cancellation; providers own the
    /// protocol markers that prove a response is complete.
    /// </summary>
    public sealed class ProviderStreamTerminationPolicy
    {
        public const string DoneSentinelSignal =
            "[DONE]";

        private static readonly IReadOnlyList<string>
            StandardBooleanTerminalPaths =
                new[]
                {
                    "done"
                };

        private static readonly IReadOnlyList<string>
            NousPortalBooleanTerminalPaths =
                new[]
                {
                    "done",
                    "lastOne"
                };

        private readonly IReadOnlyList<string>
            _booleanTerminalPaths;


        public static ProviderStreamTerminationPolicy
            OpenAICompatible
        {
            get;
        } = new ProviderStreamTerminationPolicy(
            "openai-compatible",
            StandardBooleanTerminalPaths);


        public static ProviderStreamTerminationPolicy
            NousPortal
        {
            get;
        } = new ProviderStreamTerminationPolicy(
            "nous-portal",
            NousPortalBooleanTerminalPaths);


        public string Name
        {
            get;
        }


        public IReadOnlyList<string> BooleanTerminalPaths =>
            _booleanTerminalPaths;


        public ProviderStreamTerminationPolicy(
            string name,
            IReadOnlyList<string> booleanTerminalPaths)
        {
            if (string.IsNullOrWhiteSpace(
                    name))
            {
                throw new ArgumentException(
                    "Provider stream termination policy name cannot be empty.",
                    nameof(name));
            }


            Name =
                name.Trim();

            _booleanTerminalPaths =
                CopyAndValidatePaths(
                    booleanTerminalPaths);
        }


        public bool TryGetTerminalSignal(
            string frame,
            out string terminalSignal)
        {
            terminalSignal =
                null;


            string normalized =
                NormalizeFramePayload(
                    frame);


            if (string.Equals(
                    normalized,
                    DoneSentinelSignal,
                    StringComparison.OrdinalIgnoreCase))
            {
                terminalSignal =
                    DoneSentinelSignal;

                return true;
            }


            JObject payload;


            try
            {
                payload =
                    JObject.Parse(
                        normalized);
            }
            catch (JsonException)
            {
                return false;
            }


            foreach (
                string path
                in _booleanTerminalPaths)
            {
                JToken marker =
                    payload.SelectToken(
                        path,
                        errorWhenNoMatch: false);


                if (marker?.Type ==
                        JTokenType.Boolean &&
                    marker.Value<bool>())
                {
                    terminalSignal =
                        path;

                    return true;
                }
            }


            return false;
        }


        private static IReadOnlyList<string>
            CopyAndValidatePaths(
                IReadOnlyList<string> paths)
        {
            if (paths == null ||
                paths.Count == 0)
            {
                return Array.Empty<string>();
            }


            List<string> copy =
                new List<string>(
                    paths.Count);

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


            foreach (
                string path
                in paths)
            {
                if (string.IsNullOrWhiteSpace(
                        path))
                {
                    throw new ArgumentException(
                        "Provider stream terminal paths cannot contain empty entries.",
                        nameof(paths));
                }


                string normalized =
                    path.Trim();


                if (seen.Add(
                        normalized))
                {
                    copy.Add(
                        normalized);
                }
            }


            return copy.AsReadOnly();
        }


        private static string NormalizeFramePayload(
            string frame)
        {
            string normalized =
                frame?.Trim() ??
                string.Empty;


            if (normalized.StartsWith(
                    "data:",
                    StringComparison.OrdinalIgnoreCase))
            {
                normalized =
                    normalized.Substring(
                            5)
                        .Trim();
            }


            return normalized;
        }
    }


    internal sealed class ProviderStreamingDownloadHandler :
        DownloadHandlerScript
    {
        private readonly string _providerName;
        private readonly string _endpoint;
        private readonly Action<string> _textObserver;
        private readonly Action _activityObserver;
        private readonly ProviderStreamTerminationPolicy
            _terminationPolicy;
        private readonly ProviderTransportRecord _traceRecord;
        private readonly Decoder _decoder =
            Encoding.UTF8.GetDecoder();
        private readonly StringBuilder _raw =
            new StringBuilder();
        private readonly StringBuilder _pending =
            new StringBuilder();

        private int _streamComplete;


        internal ProviderStreamingDownloadHandler(
            string providerName,
            string endpoint,
            Action<string> textObserver,
            ProviderStreamTerminationPolicy terminationPolicy)
            : base(
                new byte[8192])
        {
            _providerName =
                providerName;
            _endpoint =
                endpoint;
            _textObserver =
                textObserver;

            _terminationPolicy =
                terminationPolicy ??
                throw new ArgumentNullException(
                    nameof(terminationPolicy));

            _activityObserver =
                ProviderStreamingContext
                    .CaptureActivity();

            _traceRecord =
                ProviderTransportDebug
                    .BeginStreamingResponse(
                        providerName,
                        endpoint);
        }


        internal string RawText =>
            _raw.ToString();

        internal bool IsStreamComplete =>
            Volatile.Read(
                ref _streamComplete) !=
            0;


        protected override bool ReceiveData(
            byte[] data,
            int dataLength)
        {
            if (data == null ||
                dataLength <=
                0)
            {
                return true;
            }


            _activityObserver?.Invoke();


            char[] characters =
                new char[
                    Encoding.UTF8.GetMaxCharCount(
                        dataLength)];

            int characterCount =
                _decoder.GetChars(
                    data,
                    0,
                    dataLength,
                    characters,
                    0,
                    flush: false);

            string chunk =
                new string(
                    characters,
                    0,
                    characterCount);

            _raw.Append(
                chunk);

            _pending.Append(
                chunk);

            ProviderTransportDebug.AppendStreamingResponse(
                _traceRecord,
                chunk);

            ProcessCompleteFrames();

            return true;
        }


        protected override void CompleteContent()
        {
            char[] remainder =
                new char[4];

            int count =
                _decoder.GetChars(
                    Array.Empty<byte>(),
                    0,
                    0,
                    remainder,
                    0,
                    flush: true);


            if (count >
                0)
            {
                string text =
                    new string(
                        remainder,
                        0,
                        count);

                _raw.Append(
                    text);

                _pending.Append(
                    text);
            }


            ProcessCompleteFrames(
                flush: true);
        }


        private void ProcessCompleteFrames(
            bool flush = false)
        {
            string text =
                _pending.ToString();

            int consumed =
                0;


            for (int index = 0;
                 index <
                 text.Length;
                 index++)
            {
                if (text[index] !=
                    '\n')
                {
                    continue;
                }


                EmitFrame(
                    text.Substring(
                            consumed,
                            index -
                            consumed)
                        .TrimEnd(
                            '\r'));

                consumed =
                    index +
                    1;
            }


            if (flush &&
                consumed <
                text.Length)
            {
                EmitFrame(
                    text.Substring(
                        consumed));

                consumed =
                    text.Length;
            }


            if (consumed >
                0)
            {
                _pending.Remove(
                    0,
                    consumed);
            }
        }


        private void EmitFrame(
            string frame)
        {
            bool terminal =
                _terminationPolicy.TryGetTerminalSignal(
                    frame,
                    out string terminalSignal);


            if (terminal &&
                string.Equals(
                    terminalSignal,
                    ProviderStreamTerminationPolicy.DoneSentinelSignal,
                    StringComparison.Ordinal))
            {
                MarkStreamComplete(
                    terminalSignal);

                return;
            }


            string delta =
                ProviderStreamProtocol
                    .ExtractTextDelta(
                        frame);


            if (!string.IsNullOrEmpty(
                    delta))
            {
                _textObserver?.Invoke(
                    delta);
            }


            if (terminal)
            {
                MarkStreamComplete(
                    terminalSignal);
            }
        }


        private void MarkStreamComplete(
            string terminalSignal)
        {
            if (Interlocked.Exchange(
                    ref _streamComplete,
                    1) !=
                0)
            {
                return;
            }


            ProviderTransportDebug.MarkStreamingResponseCompleted(
                _traceRecord,
                terminalSignal);
        }
    }


    internal static class ProviderStreamProtocol
    {
        internal static string ExtractTextDelta(
            string frame)
        {
            JObject payload =
                ParseFrame(
                    frame);


            if (payload == null)
                return string.Empty;


            return payload
                       .SelectToken(
                           "choices[0].delta.content")
                       ?.Value<string>() ??
                   payload
                       .SelectToken(
                           "message.content")
                       ?.Value<string>() ??
                   payload
                       .SelectToken(
                           "response")
                       ?.Value<string>() ??
                   string.Empty;
        }


        internal static string ConvertToCompletionJson(
            string rawResponse, string providerName = "Provider")
        {
            if (string.IsNullOrWhiteSpace(
                    rawResponse))
            {
                return rawResponse;
            }


            JObject completeResponse =
                TryParseObject(
                    rawResponse);

            ThrowIfStreamError(completeResponse, providerName);


            if (completeResponse
                    ?.SelectToken(
                        "choices[0].message") !=
                null)
            {
                return rawResponse;
            }


            StringBuilder content =
                new StringBuilder();

            Dictionary<int, JObject> toolCalls =
                new Dictionary<int, JObject>();


            foreach (
                string frame
                in SplitFrames(
                    rawResponse))
            {
                JObject payload =
                    ParseFrame(
                        frame);


                if (payload == null)
                    continue;

                // Error frames can arrive after partial text or tool deltas.
                // Never turn that incomplete response into an executable call.
                ThrowIfStreamError(payload, providerName);


                string delta =
                    ExtractTextDelta(
                        frame);


                if (!string.IsNullOrEmpty(
                        delta))
                {
                    content.Append(
                        delta);
                }

                JArray calls =
                    payload
                        .SelectToken(
                            "choices[0].delta.tool_calls") as
                    JArray;


                if (calls == null)
                    continue;


                foreach (
                    JObject call
                    in calls.OfType<JObject>())
                {
                    int index =
                        call["index"]?.Value<int>() ??
                        0;


                    if (!toolCalls.TryGetValue(
                            index,
                            out JObject aggregate))
                    {
                        aggregate =
                            new JObject
                            {
                                ["id"] =
                                    call["id"] ??
                                    string.Empty,
                                ["type"] =
                                    "function",
                                ["function"] =
                                    new JObject
                                    {
                                        ["name"] =
                                            string.Empty,
                                        ["arguments"] =
                                            string.Empty
                                    }
                            };

                        toolCalls.Add(
                            index,
                            aggregate);
                    }


                    if (call["id"] !=
                        null)
                    {
                        aggregate["id"] =
                            call["id"];
                    }


                    JObject function =
                        call["function"] as
                        JObject;

                    JObject aggregateFunction =
                        (JObject)aggregate["function"];


                    if (function?["name"] !=
                        null)
                    {
                        aggregateFunction["name"] =
                            (aggregateFunction["name"]?.Value<string>() ??
                             string.Empty) +
                            function["name"].Value<string>();
                    }


                    if (function?["arguments"] !=
                        null)
                    {
                        aggregateFunction["arguments"] =
                            (aggregateFunction["arguments"]?.Value<string>() ??
                             string.Empty) +
                            function["arguments"].Value<string>();
                    }
                }
            }


            JObject message =
                new JObject
                {
                    ["role"] =
                        "assistant",
                    ["content"] =
                        content.ToString()
                };


            if (toolCalls.Count >
                0)
            {
                message["tool_calls"] =
                    new JArray(
                        toolCalls
                            .OrderBy(
                                pair =>
                                    pair.Key)
                            .Select(
                                pair =>
                                    pair.Value));
            }


            return new JObject
                {
                    ["choices"] =
                        new JArray(
                            new JObject
                            {
                                ["message"] =
                                    message
                            })
                }
                .ToString(
                    Formatting.None);
        }


        private static void ThrowIfStreamError(JObject payload, string providerName)
        {
            JToken error = payload?["error"];
            if (error == null || error.Type == JTokenType.Null) return;
            JObject details = error as JObject;
            long.TryParse(details?["code"]?.ToString() ?? details?["status"]?.ToString(),
                NumberStyles.Integer, CultureInfo.InvariantCulture, out long code);
            string errorType = details?["type"]?.ToString() ?? string.Empty;
            string body = payload.ToString(Formatting.None);
            ProviderTransportDebug.WriteEvent(providerName, "STREAM ERROR",
                new { ErrorCode = code, ErrorType = errorType, ErrorBody = body });
            throw new ProviderStreamErrorException(providerName, code, errorType, body);
        }

        private static IEnumerable<string> SplitFrames(
            string rawResponse)
        {
            return rawResponse.Split(
                new[]
                {
                    "\r\n",
                    "\n",
                    "\r"
                },
                StringSplitOptions.RemoveEmptyEntries);
        }


        private static JObject ParseFrame(
            string frame)
        {
            if (string.IsNullOrWhiteSpace(
                    frame))
            {
                return null;
            }


            string payload =
                frame.Trim();


            if (payload.StartsWith(
                    "data:",
                    StringComparison.OrdinalIgnoreCase))
            {
                payload =
                    payload.Substring(
                            5)
                        .Trim();
            }


            if (string.Equals(
                    payload,
                    "[DONE]",
                    StringComparison.OrdinalIgnoreCase))
            {
                return null;
            }


            return TryParseObject(
                payload);
        }


        private static JObject TryParseObject(
            string json)
        {
            try
            {
                return JObject.Parse(
                    json);
            }
            catch (JsonException)
            {
                return null;
            }
        }
    }


    /// <summary>
    /// Represents an HTTP or transport failure returned by a provider client.
    /// </summary>
    public sealed class ProviderHttpException :
        Exception
    {
        public string ProviderName
        {
            get;
        }


        public long StatusCode
        {
            get;
        }


        public string TransportError
        {
            get;
        }


        public string ResponseBody
        {
            get;
        }


        public TimeSpan? RetryAfter
        {
            get;
        }


        public bool HasStatusCode =>
            StatusCode > 0;


        public ProviderHttpException(
            string providerName,
            long statusCode,
            string transportError,
            string responseBody,
            TimeSpan? retryAfter = null)
            : base(
                BuildMessage(
                    providerName,
                    statusCode,
                    transportError,
                    responseBody,
                    retryAfter))
        {
            if (string.IsNullOrWhiteSpace(providerName))
            {
                throw new ArgumentException(
                    "Provider name cannot be empty.",
                    nameof(providerName));
            }


            if (statusCode < 0)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(statusCode),
                    "HTTP status code cannot be negative.");
            }


            if (retryAfter < TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(retryAfter),
                    "Provider retry delay cannot be negative.");
            }


            ProviderName =
                providerName.Trim();

            StatusCode =
                statusCode;

            TransportError =
                transportError ??
                string.Empty;

            ResponseBody =
                responseBody ??
                string.Empty;

            RetryAfter =
                retryAfter;
        }


        private static string BuildMessage(
            string providerName,
            long statusCode,
            string transportError,
            string responseBody,
            TimeSpan? retryAfter)
        {
            string normalizedProviderName =
                string.IsNullOrWhiteSpace(providerName)
                    ? "Provider"
                    : providerName.Trim();

            string status =
                statusCode > 0
                    ? $"HTTP {statusCode}"
                    : "No HTTP response";

            string message =
                $"{normalizedProviderName} request failed. {status}.";

            if (!string.IsNullOrWhiteSpace(transportError))
            {
                message +=
                    $" {transportError.Trim()}";
            }


            if (!string.IsNullOrWhiteSpace(responseBody))
            {
                message +=
                    $"\n{responseBody.Trim()}";
            }


            if (retryAfter.HasValue)
            {
                message +=
                    $"\nRetry after {retryAfter.Value.TotalSeconds.ToString("0.###", CultureInfo.InvariantCulture)} seconds.";
            }


            return message;
        }
    }
}
