using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using UnityEngine.Networking;


namespace AIRuntime.Runtime.Providers.OpenAI
{
    /// <summary>
    /// Low-level authenticated HTTP client for the OpenAI API.
    ///
    /// This class owns connection settings and HTTP transport only.
    /// Request and response mapping belongs in the provider.
    /// </summary>
    public sealed class OpenAIClient :
        IOpenAIClient
    {
        public const string DefaultBaseUrl =
            "https://api.openai.com/v1";

        public const string DefaultModel =
            "gpt-5.6-sol";


        private readonly string _apiKey;

        private readonly IReadOnlyDictionary<string, string> _defaultHeaders;


        public string BaseUrl { get; }

        public string Model { get; }

        public string ProviderName { get; }

        public string OrganizationId { get; }

        public string ProjectId { get; }

        public int TimeoutSeconds { get; }

        public ProviderStreamTerminationPolicy
            StreamTerminationPolicy
        {
            get;
        }

        public IReadOnlyDictionary<string, string> DefaultHeaders =>
            _defaultHeaders;


        public OpenAIClient(
            string apiKey,
            string model = DefaultModel,
            string baseUrl = DefaultBaseUrl,
            string organizationId = null,
            string projectId = null,
            int timeoutSeconds = 60,
            string providerName = "OpenAI",
            IReadOnlyDictionary<string, string> defaultHeaders = null,
            ProviderStreamTerminationPolicy streamTerminationPolicy = null)
        {
            if (string.IsNullOrWhiteSpace(apiKey))
            {
                throw new ArgumentException(
                    "OpenAI API key cannot be empty.",
                    nameof(apiKey));
            }


            if (string.IsNullOrWhiteSpace(model))
            {
                throw new ArgumentException(
                    "OpenAI model cannot be empty.",
                    nameof(model));
            }


            if (string.IsNullOrWhiteSpace(baseUrl))
            {
                throw new ArgumentException(
                    "OpenAI base URL cannot be empty.",
                    nameof(baseUrl));
            }


            if (!Uri.TryCreate(
                    baseUrl,
                    UriKind.Absolute,
                    out Uri parsedBaseUrl) ||
                (parsedBaseUrl.Scheme != Uri.UriSchemeHttps &&
                 parsedBaseUrl.Scheme != Uri.UriSchemeHttp))
            {
                throw new ArgumentException(
                    "OpenAI base URL must be an absolute HTTP or HTTPS URL.",
                    nameof(baseUrl));
            }


            if (timeoutSeconds <= 0)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(timeoutSeconds),
                    "OpenAI timeout must be greater than zero.");
            }

            if (string.IsNullOrWhiteSpace(providerName))
            {
                throw new ArgumentException(
                    "Provider name cannot be empty.",
                    nameof(providerName));
            }


            _apiKey =
                apiKey.Trim();
            ProviderTransportDebug.RegisterCredential(_apiKey);

            BaseUrl =
                baseUrl.TrimEnd('/');

            Model =
                model.Trim();

            ProviderName =
                providerName.Trim();

            OrganizationId =
                NormalizeOptionalValue(
                    organizationId);

            ProjectId =
                NormalizeOptionalValue(
                    projectId);

            TimeoutSeconds =
                timeoutSeconds;

            StreamTerminationPolicy =
                streamTerminationPolicy ??
                ProviderStreamTerminationPolicy.OpenAICompatible;

            _defaultHeaders =
                CopyAndValidateHeaders(
                    defaultHeaders);
        }


        /// <summary>
        /// Sends a raw JSON request to the Chat Completions endpoint.
        /// </summary>
        public async Task<string> ChatAsync(
            string jsonBody,
            CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(jsonBody))
            {
                throw new ArgumentException(
                    "OpenAI request body cannot be empty.",
                    nameof(jsonBody));
            }


            cancellationToken
                .ThrowIfCancellationRequested();

            string endpoint =
                $"{BaseUrl}/chat/completions";

            bool streaming =
                ProviderStreamingContext.IsActive;


            using IDisposable trace = ProviderTransportDebug.EnsureAttempt(ProviderName);
            using UnityWebRequest request =
                CreateChatRequest(
                    jsonBody,
                    streaming,
                    endpoint,
                    out ProviderStreamingDownloadHandler streamingHandler);

            var requestHeaders = new Dictionary<string, string>(_defaultHeaders);
            requestHeaders["Authorization"] = request.GetRequestHeader("Authorization");
            requestHeaders["Content-Type"] = request.GetRequestHeader("Content-Type");
            if (OrganizationId != null) requestHeaders["OpenAI-Organization"] = OrganizationId;
            if (ProjectId != null) requestHeaders["OpenAI-Project"] = ProjectId;
            ProviderTransportDebug.Publish(ProviderName, "REQUEST", endpoint, jsonBody, streaming,
                new Newtonsoft.Json.Linq.JObject {
                    ["Model"] = Model, ["Method"] = request.method,
                    ["Headers"] = ProviderTransportDebug.RedactHeaders(requestHeaders),
                    ["TimeoutSeconds"] = TimeoutSeconds
                });
            var elapsed = System.Diagnostics.Stopwatch.StartNew();


            bool completedByStreamMarker =
                false;

            ProviderRequestLifetime.Register(
                request);

            try
            {
                UnityWebRequestAsyncOperation operation =
                    request.SendWebRequest();

                while (!operation.isDone)
                {
                    if (streaming &&
                        streamingHandler.IsStreamComplete)
                    {
                        completedByStreamMarker =
                            true;

                        request.Abort();

                        break;
                    }


                    if (cancellationToken.IsCancellationRequested)
                    {
                        request.Abort();

                        cancellationToken
                            .ThrowIfCancellationRequested();
                    }


                    await Task.Yield();
                }


                cancellationToken
                    .ThrowIfCancellationRequested();

                string responseText =
                    streaming
                        ? streamingHandler.RawText
                        : request.downloadHandler.text;

                ProviderTransportDebug.Publish(ProviderName, "HTTP RESPONSE", endpoint, responseText, streaming,
                    new Newtonsoft.Json.Linq.JObject {
                        ["StatusCode"] = request.responseCode, ["TransportError"] = request.error,
                        ["FinalUrl"] = request.url, ["ElapsedMilliseconds"] = elapsed.ElapsedMilliseconds,
                        ["Headers"] = ProviderTransportDebug.RedactHeaders(request.GetResponseHeaders()),
                        ["CompletedByStreamMarker"] = completedByStreamMarker,
                        ["ResponseCharacters"] = responseText?.Length ?? 0
                    });


#if UNITY_2020_1_OR_NEWER

                if (!completedByStreamMarker &&
                    request.result !=
                    UnityWebRequest.Result.Success)

#else

                if (!completedByStreamMarker &&
                    (request.isNetworkError ||
                     request.isHttpError))

#endif
                {
                    throw new ProviderHttpException(
                        ProviderName,
                        request.responseCode,
                        request.error,
                        responseText,
                        ParseRetryAfter(
                            request.GetResponseHeader(
                                "Retry-After"),
                            DateTimeOffset.UtcNow));
                }


                return responseText;
            }
            catch (OperationCanceledException)
            {
                try
                {
                    request.Abort();
                }
                catch (ObjectDisposedException)
                {
                    // Domain-reload preparation may already have disposed it.
                }

                throw;
            }
            finally
            {
                try
                {
                    ProviderTransportDebug.Publish(ProviderName, "TRANSPORT END", endpoint,
                        streaming ? streamingHandler.RawText : request.downloadHandler.text, streaming,
                        new Newtonsoft.Json.Linq.JObject {
                            ["StatusCode"] = request.responseCode, ["TransportError"] = request.error,
                            ["Cancelled"] = cancellationToken.IsCancellationRequested,
                            ["ElapsedMilliseconds"] = elapsed.ElapsedMilliseconds
                        });
                }
                catch (ObjectDisposedException) { /* Reload may already have disposed the transport. */ }
                ProviderRequestLifetime.Unregister(
                    request);
            }
        }


        private UnityWebRequest CreateChatRequest(
            string jsonBody,
            bool streaming,
            string endpoint,
            out ProviderStreamingDownloadHandler streamingHandler)
        {
            UnityWebRequest request =
                new UnityWebRequest(
                    endpoint,
                    UnityWebRequest.kHttpVerbPOST);


            request.uploadHandler =
                new UploadHandlerRaw(
                    Encoding.UTF8.GetBytes(
                        jsonBody));

            streamingHandler =
                streaming
                    ? new ProviderStreamingDownloadHandler(
                        ProviderName,
                        endpoint,
                        ProviderStreamingContext.Capture(),
                        StreamTerminationPolicy)
                    : null;

            request.downloadHandler =
                streaming
                    ? streamingHandler
                    : new DownloadHandlerBuffer();

            request.timeout =
                ResolveTransportTimeoutSeconds(
                    streaming,
                    TimeoutSeconds);


            request.SetRequestHeader(
                "Authorization",
                $"Bearer {_apiKey}");

            request.SetRequestHeader(
                "Content-Type",
                "application/json");

            foreach (
                KeyValuePair<string, string> header
                in _defaultHeaders)
            {
                request.SetRequestHeader(
                    header.Key,
                    header.Value);
            }


            if (OrganizationId != null)
            {
                request.SetRequestHeader(
                    "OpenAI-Organization",
                    OrganizationId);
            }


            if (ProjectId != null)
            {
                request.SetRequestHeader(
                    "OpenAI-Project",
                    ProjectId);
            }


            return request;
        }


        internal static int ResolveTransportTimeoutSeconds(
            bool streaming,
            int configuredTimeoutSeconds)
        {
            // UnityWebRequest.timeout is an absolute request timeout. A
            // streaming provider can remain healthy for longer than this
            // while continuously returning reasoning or content chunks.
            // ConversationEngine already applies the configured timeout as
            // an inactivity timeout and cancels the request through its
            // linked token, so the transport must not impose a second,
            // absolute deadline on an active stream.
            return streaming
                ? 0
                : configuredTimeoutSeconds;
        }


        private static string NormalizeOptionalValue(
            string value)
        {
            return string.IsNullOrWhiteSpace(value)
                ? null
                : value.Trim();
        }


        internal static TimeSpan? ParseRetryAfter(
            string value,
            DateTimeOffset utcNow)
        {
            if (string.IsNullOrWhiteSpace(value))
                return null;


            string normalized =
                value.Trim();


            if (double.TryParse(
                    normalized,
                    NumberStyles.Float,
                    CultureInfo.InvariantCulture,
                    out double seconds) &&
                !double.IsNaN(seconds) &&
                !double.IsInfinity(seconds) &&
                seconds >= 0)
            {
                return TimeSpan.FromSeconds(
                    seconds);
            }


            if (DateTimeOffset.TryParse(
                    normalized,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.AssumeUniversal |
                    DateTimeStyles.AdjustToUniversal,
                    out DateTimeOffset retryAt))
            {
                TimeSpan delay =
                    retryAt -
                    utcNow;


                return delay > TimeSpan.Zero
                    ? delay
                    : TimeSpan.Zero;
            }


            return null;
        }


        private static IReadOnlyDictionary<string, string>
            CopyAndValidateHeaders(
                IReadOnlyDictionary<string, string> headers)
        {
            Dictionary<string, string> copy =
                new Dictionary<string, string>(
                    StringComparer.OrdinalIgnoreCase);


            if (headers == null)
            {
                return new ReadOnlyDictionary<string, string>(
                    copy);
            }


            foreach (
                KeyValuePair<string, string> header
                in headers)
            {
                if (string.IsNullOrWhiteSpace(header.Key))
                {
                    throw new ArgumentException(
                        "Default header names cannot be empty.",
                        nameof(headers));
                }


                if (string.IsNullOrWhiteSpace(header.Value))
                {
                    throw new ArgumentException(
                        $"Default header '{header.Key}' cannot have an empty value.",
                        nameof(headers));
                }


                string name =
                    header.Key.Trim();


                if (string.Equals(
                        name,
                        "Authorization",
                        StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(
                        name,
                        "Content-Type",
                        StringComparison.OrdinalIgnoreCase))
                {
                    throw new ArgumentException(
                        $"Default header '{name}' is managed by the client.",
                        nameof(headers));
                }


                copy.Add(
                    name,
                    header.Value.Trim());
            }


            return new ReadOnlyDictionary<string, string>(
                copy);
        }
    }
}
