// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using ShimResources = Microsoft.AspNetCore.Mvc.WebApiCompatShim.Resources;

namespace System.Web.Http
{
    /// <summary>
    /// Defines a serializable container for storing error information. This information is stored
    /// as key/value pairs. The dictionary keys to look up standard error information are available
    /// on the <see cref="HttpErrorKeys"/> type.
    /// </summary>
    [XmlRoot("Error")]
    public sealed class HttpError : Dictionary<string, object>, IXmlSerializable
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="HttpError"/> class.
        /// </summary>
        public HttpError()
            : base(StringComparer.OrdinalIgnoreCase)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="HttpError"/> class containing error message
        /// <paramref name="message"/>.
        /// </summary>
        /// <param name="message">The error message to associate with this instance.</param>
        public HttpError(string message)
            : this()
        {
            if (message == null)
            {
                throw new ArgumentNullException(nameof(message));
            }

            Message = message;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="HttpError"/> class for <paramref name="exception"/>.
        /// </summary>
        /// <param name="exception">The exception to use for error information.</param>
        /// <param name="includeErrorDetail">
        /// <c>true</c> to include the exception information in the error;<c>false</c> otherwise.
        /// </param>
        public HttpError(Exception exception, bool includeErrorDetail)
            : this()
        {
            if (exception == null)
            {
                throw new ArgumentNullException(nameof(exception));
            }

            Message = ShimResources.HttpError_GenericError;

            if (includeErrorDetail)
            {
                Add(HttpErrorKeys.ExceptionMessageKey, exception.Message);
                Add(HttpErrorKeys.ExceptionTypeKey, exception.GetType().FullName);
                Add(HttpErrorKeys.StackTraceKey, exception.StackTrace);
                if (exception.InnerException != null)
                {
                    Add(HttpErrorKeys.InnerExceptionKey, new HttpError(exception.InnerException, includeErrorDetail));
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="HttpError"/> class for <paramref name="modelState"/>.
        /// </summary>
        /// <param name="modelState">The invalid model state to use for error information.</param>
        /// <param name="includeErrorDetail">
        /// <c>true</c> to include exception messages in the error; <c>false</c> otherwise.
        /// </param>
        public HttpError(ModelStateDictionary modelState, bool includeErrorDetail)
            : this()
        {
            if (modelState == null)
            {
                throw new ArgumentNullException(nameof(modelState));
            }

            if (modelState.IsValid)
            {
                throw new ArgumentException(ShimResources.HttpError_ValidModelState, nameof(modelState));
            }

            Message = ShimResources.HttpError_BadRequest;

            var modelStateError = new HttpError();
            foreach (KeyValuePair<string, ModelStateEntry> keyModelStatePair in modelState)
            {
                var key = keyModelStatePair.Key;
                var errors = keyModelStatePair.Value.Errors;
                if (errors != null && errors.Count > 0)
                {
                    var errorMessages = errors.Select(error =>
                    {
                        if (includeErrorDetail && error.Exception != null)
                        {
                            return error.Exception.Message;
                        }
                        else
                        {
                            return
                                string.IsNullOrEmpty(error.ErrorMessage) ?
                                    ShimResources.HttpError_GenericError :
                                    error.ErrorMessage;
                        }
                    }).ToArray();
                    modelStateError.Add(key, errorMessages);
                }
            }

            Add(HttpErrorKeys.ModelStateKey, modelStateError);
        }

        /// <summary>
        /// The high-level, user-visible message explaining the cause of the error. Information carried in this field
        /// should be considered public in that it will go over the wire regardless of the value of error detail
        /// policy. As a result care should be taken not to disclose sensitive information about the server or the
        /// application.
        /// </summary>
        public string Message
        {
            get { return GetPropertyValue<string>(HttpErrorKeys.MessageKey); }
            set { this[HttpErrorKeys.MessageKey] = value; }
        }

        /// <summary>
        /// The <see cref="ModelState"/> containing information about the errors that occurred during model binding.
        /// </summary>
        /// <remarks>
        /// The inclusion of <see cref="System.Exception"/> information carried in the <see cref="ModelState"/> is
        /// controlled by the error detail policy. All other information in the <see cref="ModelState"/>
        /// should be considered public in that it will go over the wire. As a result care should be taken not to
        /// disclose sensitive information about the server or the application.
        /// </remarks>
        public HttpError ModelState
        {
            get { return GetPropertyValue<HttpError>(HttpErrorKeys.ModelStateKey); }
        }

        /// <summary>
        /// A detailed description of the error intended for the developer to understand exactly what failed.
        /// </summary>
        /// <remarks>
        /// The inclusion of this field is controlled by the error detail policy. The
        /// field is expected to contain information about the server or the application that should not
        /// be disclosed broadly.
        /// </remarks>
        public string MessageDetail
        {
            get { return GetPropertyValue<string>(HttpErrorKeys.MessageDetailKey); }
            set { this[HttpErrorKeys.MessageDetailKey] = value; }
        }

        /// <summary>
        /// The message of the <see cref="System.Exception"/> if available.
        /// </summary>
        /// <remarks>
        /// The inclusion of this field is controlled by the error detail policy. The
        /// field is expected to contain information about the server or the application that should not
        /// be disclosed broadly.
        /// </remarks>
        public string ExceptionMessage
        {
            get { return GetPropertyValue<string>(HttpErrorKeys.ExceptionMessageKey); }
            set { this[HttpErrorKeys.ExceptionMessageKey] = value; }
        }

        /// <summary>
        /// The type of the <see cref="System.Exception"/> if available.
        /// </summary>
        /// <remarks>
        /// The inclusion of this field is controlled by the error detail policy. The
        /// field is expected to contain information about the server or the application that should not
        /// be disclosed broadly.
        /// </remarks>
        public string ExceptionType
        {
            get { return GetPropertyValue<string>(HttpErrorKeys.ExceptionTypeKey); }
            set { this[HttpErrorKeys.ExceptionTypeKey] = value; }
        }

        /// <summary>
        /// The stack trace information associated with this instance if available.
        /// </summary>
        /// <remarks>
        /// The inclusion of this field is controlled by the error detail policy. The
        /// field is expected to contain information about the server or the application that should not
        /// be disclosed broadly.
        /// </remarks>
        public string StackTrace
        {
            get { return GetPropertyValue<string>(HttpErrorKeys.StackTraceKey); }
            set { this[HttpErrorKeys.StackTraceKey] = value; }
        }

        /// <summary>
        /// The inner <see cref="System.Exception"/> associated with this instance if available.
        /// </summary>
        /// <remarks>
        /// The inclusion of this field is controlled by the error detail policy. The
        /// field is expected to contain information about the server or the application that should not
        /// be disclosed broadly.
        /// </remarks>
        public HttpError InnerException
        {
            get { return GetPropertyValue<HttpError>(HttpErrorKeys.InnerExceptionKey); }
        }

        /// <summary>
        /// Gets a particular property value from this error instance.
        /// </summary>
        /// <typeparam name="TValue">The type of the property.</typeparam>
        /// <param name="key">The name of the error property.</param>
        /// <returns>The value of the error property.</returns>
        public TValue GetPropertyValue<TValue>(string key)
        {
            object value;
            if (TryGetValue(key, out value) && value is TValue)
            {
                return (TValue)value;
            }

            return default(TValue);
        }

        XmlSchema IXmlSerializable.GetSchema()
        {
            return null;
        }

        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            if (reader.IsEmptyElement)
            {
                reader.Read();
                return;
            }

            reader.ReadStartElement();
            while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
            {
                var key = XmlConvert.DecodeName(reader.LocalName);
                var value = reader.ReadInnerXml();

                Add(key, value);
                reader.MoveToContent();
            }
            reader.ReadEndElement();
        }

        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            foreach (var keyValuePair in this)
            {
                var key = keyValuePair.Key;
                var value = keyValuePair.Value;
                writer.WriteStartElement(XmlConvert.EncodeLocalName(key));
                if (value != null)
                {
                    var innerError = value as HttpError;
                    if (innerError == null)
                    {
                        writer.WriteValue(value);
                    }
                    else
                    {
                        ((IXmlSerializable)innerError).WriteXml(writer);
                    }
                }
                writer.WriteEndElement();
            }
        }
    }
}