using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using iiie.CacheControl.Business.HttpExtensions;
using iiie.CacheControl.Business.OutputCache;
using iiie.CacheControl.DBO;

namespace iiie.CacheControl.Attributes
{
    [AttributeUsage(AttributeTargets.Method)]
    public abstract class CacheControlAttribute : FilterAttribute, IActionFilter
    {
        protected static MediaTypeHeaderValue DefaultMediaType = new MediaTypeHeaderValue("application/json");

        /// <summary>
        /// Indicates if the client can reuse cached data without asking origin server
        /// </summary>
        protected bool MustRevalidate { get; set; }

        /// <summary>
        /// Indicates if the query string must be used to control cache
        /// </summary>
        protected bool ExcludeQueryStringFromCacheKey { get; set; }

        /// <summary>
        /// Indicates if the post data must be used to control cache
        /// </summary>
        protected bool ExcludePostFromCacheKey { get; set; }

        /// <summary>
        /// Optionnal data used by ouput cache backend
        /// </summary>
        public object OutputCacheData { get; set; }

        /// <summary>
        /// Define the cache type used to store cache
        /// </summary>
        protected OutputCacheType CacheType { get; set; }

        protected Type CacheKeyGenerator { get; set; }

        private MediaTypeHeaderValue _responseMediaType;

        private IOutputCache _webCache;

        protected abstract bool IsValid(CacheDbo data);

        protected virtual CacheDbo CreateCacheUser()
        {
            return new CacheDbo();
        }

        protected virtual MediaTypeHeaderValue GetExpectedMediaType(HttpConfiguration config, HttpActionContext actionContext)
        {
            MediaTypeHeaderValue responseMediaType = null;

            var negotiator = config.Services.GetService(typeof(IContentNegotiator)) as IContentNegotiator;
            var returnType = actionContext.ActionDescriptor.ReturnType;

            if (negotiator != null && returnType != typeof(HttpResponseMessage))
            {
                var negotiatedResult = negotiator.Negotiate(returnType, actionContext.Request, config.Formatters);
                responseMediaType = negotiatedResult.MediaType;
                responseMediaType.CharSet = Encoding.UTF8.HeaderName;
            }
            else
            {
                if (actionContext.Request.Headers.Accept != null)
                {
                    responseMediaType = actionContext.Request.Headers.Accept.FirstOrDefault();
                    if (responseMediaType == null ||
                        !config.Formatters.Any(x => x.SupportedMediaTypes.Contains(responseMediaType)))
                    {
                        DefaultMediaType.CharSet = Encoding.UTF8.HeaderName;
                        return DefaultMediaType;
                    }
                }
            }

            return responseMediaType;
        }

        private void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext == null) throw new ArgumentNullException("actionContext");

            var config = actionContext.Request.GetConfiguration();

            _webCache = config.CacheOutputConfiguration(CacheType).GetCacheOutputProvider(actionContext.Request, OutputCacheData);

            var cacheKeyGenerator = config.CacheOutputConfiguration(CacheType).GetCacheKeyGenerator(actionContext.Request, CacheKeyGenerator);

            _responseMediaType = GetExpectedMediaType(config, actionContext);

            var cachekey = cacheKeyGenerator.MakeCacheKey(actionContext, _responseMediaType, CacheType,
                ExcludeQueryStringFromCacheKey, ExcludePostFromCacheKey);

            var data = _webCache.Get(cachekey);
            if (data == null)
                return;

            if (!IsValid(data))
            {
                _webCache.Remove(cachekey);
                return;
            }

            if (actionContext.Request.Headers.IfNoneMatch != null)
            {
                if (data.ETag != null)
                {
                    if (actionContext.Request.Headers.IfNoneMatch.Any(x => x.Tag == data.ETag))
                    {
                        var quickResponse = actionContext.Request.CreateResponse(HttpStatusCode.NotModified);
                        ApplyCacheHeaders(quickResponse);
                        actionContext.Response = quickResponse;
                        return;
                    }
                }
            }

            if (data.Content == null) return;

            actionContext.Response = actionContext.Request.CreateResponse();
            actionContext.Response.Content = new ByteArrayContent(data.Content);

            actionContext.Response.Content.Headers.ContentType = new MediaTypeHeaderValue(data.ContentType);
            if (data.ETag != null) SetEtag(actionContext.Response, data.ETag);

            ApplyCacheHeaders(actionContext.Response);
        }

        private async Task OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            if (actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode) return;

            var config = actionExecutedContext.Request.GetConfiguration().CacheOutputConfiguration(CacheType);
            var cacheKeyGenerator = config.GetCacheKeyGenerator(actionExecutedContext.Request, CacheKeyGenerator);

            var cachekey = cacheKeyGenerator.MakeCacheKey(actionExecutedContext.ActionContext, _responseMediaType,
                CacheType, ExcludeQueryStringFromCacheKey, ExcludePostFromCacheKey);

            if (!string.IsNullOrWhiteSpace(cachekey) && !(_webCache.Contains(cachekey)))
            {
                SetEtag(actionExecutedContext.Response, Guid.NewGuid().ToString());

                if (actionExecutedContext.Response.Content != null)
                {
                    var data = CreateCacheUser();
                    data.Content = await actionExecutedContext.Response.Content.ReadAsByteArrayAsync();
                    data.ContentType = actionExecutedContext.Response.Content.Headers.ContentType.MediaType;
                    data.ETag = actionExecutedContext.Response.Headers.ETag.Tag;
                    data.Date = DateTime.Now;

                    _webCache.Add(cachekey, data);
                }
            }

            ApplyCacheHeaders(actionExecutedContext.ActionContext.Response);
        }

        private void ApplyCacheHeaders(HttpResponseMessage response)
        {
            if (MustRevalidate)
            {
                response.Headers.CacheControl = new CacheControlHeaderValue
                {
                    MustRevalidate = MustRevalidate
                };
            }
        }

        private static void SetEtag(HttpResponseMessage message, string etag)
        {
            if (etag != null)
            {
                message.Headers.ETag = new EntityTagHeaderValue(@"""" + etag.Replace("\"", string.Empty) + @"""");
            }
        }

        Task<HttpResponseMessage> IActionFilter.ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException("actionContext");
            }

            if (continuation == null)
            {
                throw new ArgumentNullException("continuation");
            }

            OnActionExecuting(actionContext);

            if (actionContext.Response != null)
            {
                return Task.FromResult(actionContext.Response);
            }

            return CallOnActionExecutedAsync(actionContext, cancellationToken, continuation);
        }

        private async Task<HttpResponseMessage> CallOnActionExecutedAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            cancellationToken.ThrowIfCancellationRequested();

            HttpResponseMessage response = null;
            Exception exception = null;
            try
            {
                response = await continuation();
            }
            catch (Exception e)
            {
                exception = e;
            }

            try
            {
                var executedContext = new HttpActionExecutedContext(actionContext, exception) { Response = response };
                await OnActionExecuted(executedContext);

                if (executedContext.Response != null)
                {
                    return executedContext.Response;
                }

                if (executedContext.Exception != null)
                {
                    ExceptionDispatchInfo.Capture(executedContext.Exception).Throw();
                }
            }
            catch (Exception e)
            {
                actionContext.Response = null;
                ExceptionDispatchInfo.Capture(e).Throw();
            }

            throw new InvalidOperationException(GetType().Name);
        }

    }
}