using System;
using System.Net.Http;
using System.Threading;


namespace AIRuntime.Runtime.Providers.Fallback
{
    /// <summary>
    /// Provides conservative provider failure classification.
    ///
    /// Known temporary or provider-specific failures allow
    /// fallback. Unknown failures stop the chain by default.
    /// </summary>
    public sealed class DefaultProviderFailureClassifier :
        IProviderFailureClassifier
    {
        public ProviderFailure Classify(
            IProvider provider,
            Exception exception)
        {
            if (provider == null)
            {
                throw new ArgumentNullException(
                    nameof(provider));
            }


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


            Exception actualException =
                Unwrap(
                    exception);


            ProviderFailureKind kind =
                DetermineKind(
                    actualException);


            bool shouldFallback =
                ShouldFallback(
                    kind);


            return new ProviderFailure(
                provider.Name,
                kind,
                actualException.Message,
                actualException,
                shouldFallback);
        }


        private static ProviderFailureKind DetermineKind(
            Exception exception)
        {
            if (exception is ProviderInputNotSupportedException)
                return ProviderFailureKind.UnsupportedInput;

            if (exception is OperationCanceledException)
            {
                return ProviderFailureKind.Cancelled;
            }


            if (exception is TimeoutException)
            {
                return ProviderFailureKind.Timeout;
            }


            if (exception is ProviderStreamErrorException streamError)
            {
                if (streamError.ErrorCode >= 400 && streamError.ErrorCode <= 599)
                    return ClassifyStatusCode(streamError.ErrorCode);
                switch (streamError.ErrorType.ToLowerInvariant())
                {
                    case "service_unavailable": return ProviderFailureKind.Unavailable;
                    case "rate_limit_error": return ProviderFailureKind.RateLimited;
                    case "authentication_error": return ProviderFailureKind.Authentication;
                    case "invalid_request_error": return ProviderFailureKind.InvalidRequest;
                    default: return ProviderFailureKind.InvalidResponse;
                }
            }

            if (exception is ProviderEmptyResponseException)
            {
                return ProviderFailureKind.EmptyResponse;
            }


            if (exception is ProviderHttpException providerHttpException)
            {
                return ClassifyProviderHttpFailure(
                    providerHttpException);
            }


            if (exception is HttpRequestException)
            {
                return ClassifyHttpFailure(
                    exception);
            }


            string message =
                exception.Message ??
                string.Empty;


            string normalisedMessage =
                message.ToLowerInvariant();


            if (
                ContainsAny(
                    normalisedMessage,
                    "rate limit",
                    "rate_limit",
                    "too many requests",
                    "quota exceeded",
                    "resource exhausted",
                    "capacity"))
            {
                return ProviderFailureKind.RateLimited;
            }


            if (
                ContainsAny(
                    normalisedMessage,
                    "model not found",
                    "model does not exist",
                    "unknown model",
                    "model unavailable",
                    "model is unavailable",
                    "model has been disabled",
                    "no such model"))
            {
                return ProviderFailureKind.ModelUnavailable;
            }


            if (
                ContainsAny(
                    normalisedMessage,
                    "unauthorized",
                    "unauthorised",
                    "authentication",
                    "invalid api key",
                    "invalid_api_key",
                    "forbidden",
                    "permission denied"))
            {
                return ProviderFailureKind.Authentication;
            }


            if (
                ContainsAny(
                    normalisedMessage,
                    "connection refused",
                    "connection reset",
                    "connection failed",
                    "could not connect",
                    "unable to connect",
                    "host unreachable",
                    "network unreachable",
                    "service unavailable",
                    "temporarily unavailable",
                    "bad gateway",
                    "gateway timeout"))
            {
                return ProviderFailureKind.Unavailable;
            }


            if (
                ContainsAny(
                    normalisedMessage,
                    "invalid request",
                    "bad request",
                    "unsupported parameter",
                    "invalid parameter",
                    "context length",
                    "maximum context",
                    "token limit exceeded"))
            {
                return ProviderFailureKind.InvalidRequest;
            }


            if (
                ContainsAny(
                    normalisedMessage,
                    "invalid response",
                    "malformed response",
                    "failed to parse",
                    "deserialization",
                    "unexpected response",
                    "null completion"))
            {
                return ProviderFailureKind.InvalidResponse;
            }


            return ProviderFailureKind.Unknown;
        }


        private static ProviderFailureKind ClassifyProviderHttpFailure(
            ProviderHttpException exception)
        {
            long statusCode =
                exception.StatusCode;


            if (!exception.HasStatusCode)
            {
                return ProviderFailureKind.Unavailable;
            }


            return ClassifyStatusCode(statusCode);
        }

        private static ProviderFailureKind ClassifyStatusCode(long statusCode)
        {
            switch (statusCode)
            {
                case 401:
                case 403:
                    return ProviderFailureKind.Authentication;

                case 404:
                    return ProviderFailureKind.ModelUnavailable;

                case 408:
                case 504:
                    return ProviderFailureKind.Timeout;

                case 429:
                    return ProviderFailureKind.RateLimited;
            }


            if (statusCode >= 500 &&
                statusCode <= 599)
            {
                return ProviderFailureKind.Unavailable;
            }


            if (statusCode >= 400 &&
                statusCode <= 499)
            {
                return ProviderFailureKind.InvalidRequest;
            }


            if (statusCode >= 200 &&
                statusCode <= 399)
            {
                return ProviderFailureKind.InvalidResponse;
            }


            return ProviderFailureKind.Unknown;
        }


        private static ProviderFailureKind ClassifyHttpFailure(
            Exception exception)
        {
            string message =
                exception.Message?.ToLowerInvariant() ??
                string.Empty;


            if (
                ContainsAny(
                    message,
                    "401",
                    "403",
                    "unauthorized",
                    "unauthorised",
                    "forbidden"))
            {
                return ProviderFailureKind.Authentication;
            }


            if (
                ContainsAny(
                    message,
                    "429",
                    "too many requests",
                    "rate limit"))
            {
                return ProviderFailureKind.RateLimited;
            }


            if (
                ContainsAny(
                    message,
                    "400",
                    "422",
                    "bad request",
                    "unprocessable"))
            {
                return ProviderFailureKind.InvalidRequest;
            }


            if (
                ContainsAny(
                    message,
                    "404",
                    "model not found",
                    "not found"))
            {
                return ProviderFailureKind.ModelUnavailable;
            }


            if (
                ContainsAny(
                    message,
                    "408",
                    "504",
                    "gateway timeout"))
            {
                return ProviderFailureKind.Timeout;
            }


            return ProviderFailureKind.Unavailable;
        }


        private static bool ShouldFallback(
            ProviderFailureKind kind)
        {
            switch (kind)
            {
                case ProviderFailureKind.Timeout:
                case ProviderFailureKind.Unavailable:
                case ProviderFailureKind.RateLimited:
                case ProviderFailureKind.UnsupportedInput:
                case ProviderFailureKind.ModelUnavailable:
                case ProviderFailureKind.Authentication:
                case ProviderFailureKind.InvalidResponse:
                case ProviderFailureKind.EmptyResponse:
                    return true;

                case ProviderFailureKind.InvalidRequest:
                case ProviderFailureKind.Cancelled:
                case ProviderFailureKind.Unknown:
                default:
                    return false;
            }
        }


        private static bool ContainsAny(
            string value,
            params string[] terms)
        {
            foreach (string term in terms)
            {
                if (
                    value.IndexOf(
                        term,
                        StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    return true;
                }
            }


            return false;
        }


        private static Exception Unwrap(
            Exception exception)
        {
            if (!(exception is AggregateException aggregate))
            {
                return exception;
            }


            AggregateException flattened =
                aggregate.Flatten();


            if (flattened.InnerExceptions.Count == 1)
            {
                return flattened.InnerExceptions[0];
            }


            return flattened;
        }
    }
}
