// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Globalization;
using System.IO.Pipelines;
using System.Net;
using System.Net.WebSockets;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;

namespace Microsoft.AspNetCore.Owin;

using SendFileFunc = Func<string, long, long?, CancellationToken, Task>;

/// <summary>
/// OWIN feature collection.
/// </summary>
public class OwinFeatureCollection :
    IFeatureCollection,
    IHttpRequestFeature,
    IHttpResponseFeature,
    IHttpResponseBodyFeature,
    IHttpConnectionFeature,
    ITlsConnectionFeature,
    IHttpRequestIdentifierFeature,
    IHttpRequestLifetimeFeature,
    IHttpAuthenticationFeature,
    IHttpWebSocketFeature,
    IOwinEnvironmentFeature
{
    /// <summary>
    /// Gets or sets OWIN environment values.
    /// </summary>
    public IDictionary<string, object> Environment { get; set; }
    private PipeWriter _responseBodyWrapper;
    private bool _headersSent;

    /// <summary>
    /// Initializes a new instance of <see cref="OwinFeatureCollection"/>.
    /// </summary>
    /// <param name="environment">The environment values.</param>
    public OwinFeatureCollection(IDictionary<string, object> environment)
    {
        Environment = environment;
        SupportsWebSockets = true;

        var register = Prop<Action<Action<object>, object>>(OwinConstants.CommonKeys.OnSendingHeaders);
        register?.Invoke(state =>
        {
            var collection = (OwinFeatureCollection)state;
            collection._headersSent = true;
        }, this);
    }

    T Prop<T>(string key)
    {
        object value;
        if (Environment.TryGetValue(key, out value) && value is T)
        {
            return (T)value;
        }
        return default(T);
    }

    void Prop(string key, object value)
    {
        Environment[key] = value;
    }

    string IHttpRequestFeature.Protocol
    {
        get { return Prop<string>(OwinConstants.RequestProtocol); }
        set { Prop(OwinConstants.RequestProtocol, value); }
    }

    string IHttpRequestFeature.Scheme
    {
        get { return Prop<string>(OwinConstants.RequestScheme); }
        set { Prop(OwinConstants.RequestScheme, value); }
    }

    string IHttpRequestFeature.Method
    {
        get { return Prop<string>(OwinConstants.RequestMethod); }
        set { Prop(OwinConstants.RequestMethod, value); }
    }

    string IHttpRequestFeature.PathBase
    {
        get { return Prop<string>(OwinConstants.RequestPathBase); }
        set { Prop(OwinConstants.RequestPathBase, value); }
    }

    string IHttpRequestFeature.Path
    {
        get { return Prop<string>(OwinConstants.RequestPath); }
        set { Prop(OwinConstants.RequestPath, value); }
    }

    string IHttpRequestFeature.QueryString
    {
        get { return Utilities.AddQuestionMark(Prop<string>(OwinConstants.RequestQueryString)); }
        set { Prop(OwinConstants.RequestQueryString, Utilities.RemoveQuestionMark(value)); }
    }

    string IHttpRequestFeature.RawTarget
    {
        get { return string.Empty; }
        set { throw new NotSupportedException(); }
    }

    IHeaderDictionary IHttpRequestFeature.Headers
    {
        get { return Utilities.MakeHeaderDictionary(Prop<IDictionary<string, string[]>>(OwinConstants.RequestHeaders)); }
        set { Prop(OwinConstants.RequestHeaders, Utilities.MakeDictionaryStringArray(value)); }
    }

    string IHttpRequestIdentifierFeature.TraceIdentifier
    {
        get { return Prop<string>(OwinConstants.RequestId); }
        set { Prop(OwinConstants.RequestId, value); }
    }

    Stream IHttpRequestFeature.Body
    {
        get { return Prop<Stream>(OwinConstants.RequestBody); }
        set { Prop(OwinConstants.RequestBody, value); }
    }

    int IHttpResponseFeature.StatusCode
    {
        get { return Prop<int>(OwinConstants.ResponseStatusCode); }
        set { Prop(OwinConstants.ResponseStatusCode, value); }
    }

    string IHttpResponseFeature.ReasonPhrase
    {
        get { return Prop<string>(OwinConstants.ResponseReasonPhrase); }
        set { Prop(OwinConstants.ResponseReasonPhrase, value); }
    }

    IHeaderDictionary IHttpResponseFeature.Headers
    {
        get { return Utilities.MakeHeaderDictionary(Prop<IDictionary<string, string[]>>(OwinConstants.ResponseHeaders)); }
        set { Prop(OwinConstants.ResponseHeaders, Utilities.MakeDictionaryStringArray(value)); }
    }

    Stream IHttpResponseFeature.Body
    {
        get { return Prop<Stream>(OwinConstants.ResponseBody); }
        set { Prop(OwinConstants.ResponseBody, value); }
    }

    Stream IHttpResponseBodyFeature.Stream
    {
        get { return Prop<Stream>(OwinConstants.ResponseBody); }
    }

    PipeWriter IHttpResponseBodyFeature.Writer
    {
        get
        {
            if (_responseBodyWrapper == null)
            {
                _responseBodyWrapper = PipeWriter.Create(Prop<Stream>(OwinConstants.ResponseBody), new StreamPipeWriterOptions(leaveOpen: true));
            }

            return _responseBodyWrapper;
        }
    }

    bool IHttpResponseFeature.HasStarted
    {
        get { return _headersSent; }
    }

    void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)
    {
        var register = Prop<Action<Action<object>, object>>(OwinConstants.CommonKeys.OnSendingHeaders);
        if (register == null)
        {
            throw new NotSupportedException(OwinConstants.CommonKeys.OnSendingHeaders);
        }

        // Need to block on the callback since we can't change the OWIN signature to be async
        register(s => callback(s).GetAwaiter().GetResult(), state);
    }

    void IHttpResponseFeature.OnCompleted(Func<object, Task> callback, object state)
    {
        throw new NotSupportedException();
    }

    IPAddress IHttpConnectionFeature.RemoteIpAddress
    {
        get { return IPAddress.Parse(Prop<string>(OwinConstants.CommonKeys.RemoteIpAddress)); }
        set { Prop(OwinConstants.CommonKeys.RemoteIpAddress, value.ToString()); }
    }

    IPAddress IHttpConnectionFeature.LocalIpAddress
    {
        get { return IPAddress.Parse(Prop<string>(OwinConstants.CommonKeys.LocalIpAddress)); }
        set { Prop(OwinConstants.CommonKeys.LocalIpAddress, value.ToString()); }
    }

    int IHttpConnectionFeature.RemotePort
    {
        get { return int.Parse(Prop<string>(OwinConstants.CommonKeys.RemotePort), CultureInfo.InvariantCulture); }
        set { Prop(OwinConstants.CommonKeys.RemotePort, value.ToString(CultureInfo.InvariantCulture)); }
    }

    int IHttpConnectionFeature.LocalPort
    {
        get { return int.Parse(Prop<string>(OwinConstants.CommonKeys.LocalPort), CultureInfo.InvariantCulture); }
        set { Prop(OwinConstants.CommonKeys.LocalPort, value.ToString(CultureInfo.InvariantCulture)); }
    }

    string IHttpConnectionFeature.ConnectionId
    {
        get { return Prop<string>(OwinConstants.CommonKeys.ConnectionId); }
        set { Prop(OwinConstants.CommonKeys.ConnectionId, value); }
    }

    Task IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
    {
        object obj;
        if (Environment.TryGetValue(OwinConstants.SendFiles.SendAsync, out obj))
        {
            var func = (SendFileFunc)obj;
            return func(path, offset, length, cancellation);
        }
        throw new NotSupportedException(OwinConstants.SendFiles.SendAsync);
    }

    private bool SupportsClientCerts
    {
        get
        {
            object obj;
            if (string.Equals("https", ((IHttpRequestFeature)this).Scheme, StringComparison.OrdinalIgnoreCase)
                && (Environment.TryGetValue(OwinConstants.CommonKeys.LoadClientCertAsync, out obj)
                    || Environment.TryGetValue(OwinConstants.CommonKeys.ClientCertificate, out obj))
                && obj != null)
            {
                return true;
            }
            return false;
        }
    }

    X509Certificate2 ITlsConnectionFeature.ClientCertificate
    {
        get { return Prop<X509Certificate2>(OwinConstants.CommonKeys.ClientCertificate); }
        set { Prop(OwinConstants.CommonKeys.ClientCertificate, value); }
    }

    async Task<X509Certificate2> ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken)
    {
        var loadAsync = Prop<Func<Task>>(OwinConstants.CommonKeys.LoadClientCertAsync);
        if (loadAsync != null)
        {
            await loadAsync();
        }
        return Prop<X509Certificate2>(OwinConstants.CommonKeys.ClientCertificate);
    }

    CancellationToken IHttpRequestLifetimeFeature.RequestAborted
    {
        get { return Prop<CancellationToken>(OwinConstants.CallCancelled); }
        set { Prop(OwinConstants.CallCancelled, value); }
    }

    void IHttpRequestLifetimeFeature.Abort()
    {
        throw new NotImplementedException();
    }

    ClaimsPrincipal IHttpAuthenticationFeature.User
    {
        get
        {
            return Prop<ClaimsPrincipal>(OwinConstants.RequestUser)
                ?? Utilities.MakeClaimsPrincipal(Prop<IPrincipal>(OwinConstants.Security.User));
        }
        set
        {
            Prop(OwinConstants.RequestUser, value);
            Prop(OwinConstants.Security.User, value);
        }
    }

    /// <summary>
    /// Gets or sets if the underlying server supports WebSockets. This is enabled by default.
    /// The value should be consistent across requests.
    /// </summary>
    public bool SupportsWebSockets { get; set; }

    bool IHttpWebSocketFeature.IsWebSocketRequest
    {
        get
        {
            return Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out _);
        }
    }

    Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context)
    {
        object obj;
        if (!Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj))
        {
            throw new NotSupportedException("WebSockets are not supported"); // TODO: LOC
        }
        var accept = (Func<WebSocketAcceptContext, Task<WebSocket>>)obj;
        return accept(context);
    }

    // IFeatureCollection

    /// <inheritdoc/>
    public int Revision
    {
        get { return 0; } // Not modifiable
    }

    /// <inheritdoc/>
    public bool IsReadOnly
    {
        get { return true; }
    }

    /// <inheritdoc/>
    public object this[Type key]
    {
        get { return Get(key); }
        set { throw new NotSupportedException(); }
    }

    private bool SupportsInterface(Type key)
    {
        // Does this type implement the requested interface?
        if (key.IsAssignableFrom(GetType()))
        {
            // Check for conditional features
            if (key == typeof(ITlsConnectionFeature))
            {
                return SupportsClientCerts;
            }
            else if (key == typeof(IHttpWebSocketFeature))
            {
                return SupportsWebSockets;
            }

            // The rest of the features are always supported.
            return true;
        }
        return false;
    }

    /// <inheritdoc/>
    public object Get(Type key)
    {
        if (SupportsInterface(key))
        {
            return this;
        }
        return null;
    }

    /// <inheritdoc/>
    public void Set(Type key, object value)
    {
        throw new NotSupportedException();
    }

    /// <inheritdoc/>
    public TFeature Get<TFeature>()
    {
        return (TFeature)this[typeof(TFeature)];
    }

    /// <inheritdoc/>
    public void Set<TFeature>(TFeature instance)
    {
        this[typeof(TFeature)] = instance;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    /// <inheritdoc/>
    public IEnumerator<KeyValuePair<Type, object>> GetEnumerator()
    {
        yield return new KeyValuePair<Type, object>(typeof(IHttpRequestFeature), this);
        yield return new KeyValuePair<Type, object>(typeof(IHttpResponseFeature), this);
        yield return new KeyValuePair<Type, object>(typeof(IHttpResponseBodyFeature), this);
        yield return new KeyValuePair<Type, object>(typeof(IHttpConnectionFeature), this);
        yield return new KeyValuePair<Type, object>(typeof(IHttpRequestIdentifierFeature), this);
        yield return new KeyValuePair<Type, object>(typeof(IHttpRequestLifetimeFeature), this);
        yield return new KeyValuePair<Type, object>(typeof(IHttpAuthenticationFeature), this);
        yield return new KeyValuePair<Type, object>(typeof(IOwinEnvironmentFeature), this);

        // Check for conditional features
        if (SupportsClientCerts)
        {
            yield return new KeyValuePair<Type, object>(typeof(ITlsConnectionFeature), this);
        }
        if (SupportsWebSockets)
        {
            yield return new KeyValuePair<Type, object>(typeof(IHttpWebSocketFeature), this);
        }
    }

    void IHttpResponseBodyFeature.DisableBuffering()
    {
    }

    async Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellationToken)
    {
        if (_responseBodyWrapper != null)
        {
            await _responseBodyWrapper.FlushAsync(cancellationToken);
        }

        // The pipe may or may not have flushed the stream. Make sure the stream gets flushed to trigger response start.
        await Prop<Stream>(OwinConstants.ResponseBody).FlushAsync(cancellationToken);
    }

    Task IHttpResponseBodyFeature.CompleteAsync()
    {
        if (_responseBodyWrapper != null)
        {
            return _responseBodyWrapper.FlushAsync().AsTask();
        }

        return Task.CompletedTask;
    }

    /// <inheritdoc/>
    public void Dispose()
    {
    }
}

