using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser.Model;

namespace NzbDrone.Core.Indexers
{
    public class RssParser : IParseIndexerResponse
    {
        private static readonly Regex ReplaceEntities = new Regex("&[a-z]+;", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        public const string NzbEnclosureMimeType = "application/x-nzb";
        public const string TorrentEnclosureMimeType = "application/x-bittorrent";
        public const string MagnetEnclosureMimeType = "application/x-bittorrent;x-scheme-handler/magnet";
        public static readonly string[] UsenetEnclosureMimeTypes = new[] { NzbEnclosureMimeType };
        public static readonly string[] TorrentEnclosureMimeTypes = new[] { TorrentEnclosureMimeType, MagnetEnclosureMimeType };

        protected readonly Logger _logger;

        // Use the 'guid' element content as InfoUrl.
        public bool UseGuidInfoUrl { get; set; }

        // Use the enclosure as download url and/or length.
        public bool UseEnclosureUrl { get; set; }
        public bool UseEnclosureLength { get; set; }

        // Parse "Size: 1.3 GB" or "1.3 GB" parts in the description element and use that as Size.
        public bool ParseSizeInDescription { get; set; }

        public string[] PreferredEnclosureMimeTypes { get; set; }

        private IndexerResponse _indexerResponse;

        public RssParser()
        {
            _logger = NzbDroneLogger.GetLogger(this);
        }

        public virtual IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
        {
            _indexerResponse = indexerResponse;

            var releases = new List<ReleaseInfo>();

            if (!PreProcess(indexerResponse))
            {
                return releases;
            }

            var document = LoadXmlDocument(indexerResponse);
            var items = GetItems(document).ToList();

            foreach (var item in items)
            {
                try
                {
                    var reportInfo = ProcessItem(item);

                    releases.AddIfNotNull(reportInfo);
                }
                catch (UnsupportedFeedException itemEx)
                {
                    itemEx.WithData("FeedUrl", indexerResponse.Request.Url);
                    itemEx.WithData("ItemTitle", item.Title());
                    throw;
                }
                catch (Exception itemEx)
                {
                    itemEx.WithData("FeedUrl", indexerResponse.Request.Url);
                    itemEx.WithData("ItemTitle", item.Title());
                    _logger.Error(itemEx, "An error occurred while processing feed item from {0}", indexerResponse.Request.Url);
                }
            }

            if (!PostProcess(indexerResponse, items, releases))
            {
                return new List<ReleaseInfo>();
            }

            return releases;
        }

        protected virtual XDocument LoadXmlDocument(IndexerResponse indexerResponse)
        {
            try
            {
                var content = XmlCleaner.ReplaceEntities(indexerResponse.Content);
                content = XmlCleaner.ReplaceUnicode(content);

                using (var xmlTextReader = XmlReader.Create(new StringReader(content), new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, IgnoreComments = true }))
                {
                    return XDocument.Load(xmlTextReader);
                }
            }
            catch (XmlException ex)
            {
                var contentSample = indexerResponse.Content.Substring(0, Math.Min(indexerResponse.Content.Length, 512));
                _logger.Debug("Truncated response content (originally {0} characters): {1}", indexerResponse.Content.Length, contentSample);

                ex.WithData(indexerResponse.HttpResponse);

                throw;
            }
        }

        protected virtual ReleaseInfo CreateNewReleaseInfo()
        {
            return new ReleaseInfo();
        }

        protected virtual bool PreProcess(IndexerResponse indexerResponse)
        {
            if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
            {
                throw new IndexerException(indexerResponse, "Indexer API call resulted in an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode);
            }

            if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/html") &&
                indexerResponse.HttpRequest.Headers.Accept != null && !indexerResponse.HttpRequest.Headers.Accept.Contains("text/html"))
            {
                throw new IndexerException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable.");
            }

            return true;
        }

        protected virtual bool PostProcess(IndexerResponse indexerResponse, List<XElement> elements, List<ReleaseInfo> releases)
        {
            return true;
        }

        protected ReleaseInfo ProcessItem(XElement item)
        {
            var releaseInfo = CreateNewReleaseInfo();

            releaseInfo = ProcessItem(item, releaseInfo);

            _logger.Trace("Parsed: {0}", releaseInfo.Title);

            return PostProcessItem(item, releaseInfo);
        }

        protected virtual ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo)
        {
            releaseInfo.Guid = GetGuid(item);
            releaseInfo.Title = GetTitle(item);
            releaseInfo.PublishDate = GetPublishDate(item);
            releaseInfo.DownloadUrl = GetDownloadUrl(item);
            releaseInfo.BasicAuthString = GetBasicAuth();
            releaseInfo.InfoUrl = GetInfoUrl(item);
            releaseInfo.CommentUrl = GetCommentUrl(item);

            try
            {
                releaseInfo.Size = GetSize(item);
            }
            catch (Exception)
            {
                throw new SizeParsingException("Unable to parse size from: {0}", releaseInfo.Title);
            }

            return releaseInfo;
        }

        protected virtual ReleaseInfo PostProcessItem(XElement item, ReleaseInfo releaseInfo)
        {
            return releaseInfo;
        }

        protected virtual string GetGuid(XElement item)
        {
            return item.TryGetValue("guid", Guid.NewGuid().ToString());
        }

        protected virtual string GetTitle(XElement item)
        {
            return item.TryGetValue("title", "Unknown");
        }

        protected virtual DateTime GetPublishDate(XElement item)
        {
            var dateString = item.TryGetValue("pubDate");

            if (dateString.IsNullOrWhiteSpace())
            {
                throw new UnsupportedFeedException("Rss feed must have a pubDate element with a valid publish date.");
            }

            return XElementExtensions.ParseDate(dateString);
        }

        protected virtual string GetBasicAuth()
        {
            return null;
        }

        protected virtual string GetDownloadUrl(XElement item)
        {
            if (UseEnclosureUrl)
            {
                var enclosure = GetEnclosure(item);
                return enclosure != null ? ParseUrl(enclosure.Url) : null;
            }

            return ParseUrl((string)item.Element("link"));
        }

        protected virtual string GetInfoUrl(XElement item)
        {
            if (UseGuidInfoUrl)
            {
                return ParseUrl((string)item.Element("guid"));
            }

            return null;
        }

        protected virtual string GetCommentUrl(XElement item)
        {
            return ParseUrl((string)item.Element("comments"));
        }

        protected virtual long GetSize(XElement item)
        {
            if (UseEnclosureLength)
            {
                return GetEnclosureLength(item);
            }
            if (ParseSizeInDescription && item.Element("description") != null)
            {
                return ParseSize(item.Element("description").Value, true);
            }

            return 0;
        }

        protected virtual long GetEnclosureLength(XElement item)
        {
            var enclosure = GetEnclosure(item);

            if (enclosure != null)
            {
                return enclosure.Length;
            }

            return 0;
        }

        protected virtual RssEnclosure[] GetEnclosures(XElement item)
        {
            var enclosures = item.Elements("enclosure")
                                 .Select(v =>
                                 {
                                     try
                                     {
                                         return new RssEnclosure
                                         {
                                             Url = v.Attribute("url").Value,
                                             Type = v.Attribute("type").Value,
                                             Length = (long)v.Attribute("length")
                                         };
                                     }
                                     catch (Exception e)
                                     {
                                         _logger.Warn(e, "Failed to get enclosure for: {0}", item.Title());
                                     }

                                     return null;
                                 })
                                 .Where(v => v != null)
                                 .ToArray();

            return enclosures;
        }

        protected RssEnclosure GetEnclosure(XElement item, bool enforceMimeType = true)
        {
            var enclosures = GetEnclosures(item);

            return GetEnclosure(enclosures, enforceMimeType);
        }

        protected virtual RssEnclosure GetEnclosure(RssEnclosure[] enclosures, bool enforceMimeType = true)
        {

            if (enclosures.Length == 0)
            {
                return null;
            }

            if (PreferredEnclosureMimeTypes != null)
            {
                foreach (var preferredEnclosureType in PreferredEnclosureMimeTypes)
                {
                    var preferredEnclosure = enclosures.FirstOrDefault(v => v.Type == preferredEnclosureType);

                    if (preferredEnclosure != null)
                    {
                        return preferredEnclosure;
                    }
                }

                if (enforceMimeType)
                {
                    return null;
                }
            }

            return enclosures.SingleOrDefault();
        }

        protected IEnumerable<XElement> GetItems(XDocument document)
        {
            var root = document.Root;

            if (root == null)
            {
                return Enumerable.Empty<XElement>();
            }

            var channel = root.Element("channel");

            if (channel == null)
            {
                return Enumerable.Empty<XElement>();
            }

            return channel.Elements("item");
        }

        protected virtual string ParseUrl(string value)
        {
            if (value.IsNullOrWhiteSpace())
            {
                return null;
            }

            try
            {
                var url = _indexerResponse.HttpRequest.Url + new HttpUri(value);

                return url.FullUri;
            }
            catch (Exception ex)
            {
                _logger.Debug(ex, string.Format("Failed to parse Url {0}, ignoring.", value));
                return null;
            }
        }

        private static readonly Regex ParseSizeRegex = new Regex(@"(?<value>(?<!\.\d*)(?:\d+,)*\d+(?:\.\d{1,3})?)\W?(?<unit>[KMG]i?B)(?![\w/])",
            RegexOptions.IgnoreCase | RegexOptions.Compiled);

        public static long ParseSize(string sizeString, bool defaultToBinaryPrefix)
        {
            if (sizeString.IsNullOrWhiteSpace())
            {
                return 0;
            }

            if (sizeString.All(char.IsDigit))
            {
                return long.Parse(sizeString);
            }

            var match = ParseSizeRegex.Matches(sizeString);

            if (match.Count != 0)
            {
                var value = decimal.Parse(Regex.Replace(match[0].Groups["value"].Value, "\\,", ""), CultureInfo.InvariantCulture);

                var unit = match[0].Groups["unit"].Value.ToLower();

                switch (unit)
                {
                    case "kb":
                        return ConvertToBytes(Convert.ToDouble(value), 1, defaultToBinaryPrefix);
                    case "mb":
                        return ConvertToBytes(Convert.ToDouble(value), 2, defaultToBinaryPrefix);
                    case "gb":
                        return ConvertToBytes(Convert.ToDouble(value), 3, defaultToBinaryPrefix);
                    case "kib":
                        return ConvertToBytes(Convert.ToDouble(value), 1, true);
                    case "mib":
                        return ConvertToBytes(Convert.ToDouble(value), 2, true);
                    case "gib":
                        return ConvertToBytes(Convert.ToDouble(value), 3, true);
                    default:
                        return (long)value;
                }
            }
            return 0;
        }

        private static long ConvertToBytes(double value, int power, bool binaryPrefix)
        {
            var prefix = binaryPrefix ? 1024 : 1000;
            var multiplier = Math.Pow(prefix, power);
            var result = value * multiplier;

            return Convert.ToInt64(result);
        }
    }
}
