using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core;
using Umbraco.Core.Logging;

namespace Umbraco.Web.Routing
{
    internal static class UrlProviderExtensions
    {
        /// <summary>
        /// Gets the Urls of the content item.
        /// </summary>
        /// <remarks>
        /// <para>Use when displaying Urls. If errors occur when generating the Urls, they will show in the list.</para>
        /// <para>Contains all the Urls that we can figure out (based upon domains, etc).</para>
        /// </remarks>
        public static IEnumerable<UrlInfo> GetContentUrls(this IContent content,
            IPublishedRouter publishedRouter,
            UmbracoContext umbracoContext,
            ILocalizationService localizationService,
            ILocalizedTextService textService,
            IContentService contentService,
            ILogger logger)
        {
            if (content == null) throw new ArgumentNullException(nameof(content));
            if (publishedRouter == null) throw new ArgumentNullException(nameof(publishedRouter));
            if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext));
            if (localizationService == null) throw new ArgumentNullException(nameof(localizationService));
            if (textService == null) throw new ArgumentNullException(nameof(textService));
            if (contentService == null) throw new ArgumentNullException(nameof(contentService));
            if (logger == null) throw new ArgumentNullException(nameof(logger));

            if (content.Published == false)
            {
                yield return UrlInfo.Message(textService.Localize("content/itemNotPublished"));
                yield break;
            }

            // build a list of urls, for the back-office
            // which will contain
            // - the 'main' urls, which is what .Url would return, for each culture
            // - the 'other' urls we know (based upon domains, etc)
            //
            // need to work through each installed culture:
            // on invariant nodes, each culture returns the same url segment but,
            // we don't know if the branch to this content is invariant, so we need to ask
            // for URLs for all cultures.
            // and, not only for those assigned to domains in the branch, because we want
            // to show what GetUrl() would return, for every culture.

            var urls = new HashSet<UrlInfo>();
            var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList();

            //get all URLs for all cultures
            //in a HashSet, so de-duplicates too
            foreach (var cultureUrl in GetContentUrlsByCulture(content, cultures, publishedRouter, umbracoContext, contentService, textService, logger))
            {
                urls.Add(cultureUrl);
            }

            //return the real urls first, then the messages
            foreach (var urlGroup in urls.GroupBy(x => x.IsUrl).OrderByDescending(x => x.Key))
            {
                //in some cases there will be the same URL for multiple cultures:
                // * The entire branch is invariant
                // * If there are less domain/cultures assigned to the branch than the number of cultures/languages installed

                foreach (var dUrl in urlGroup.DistinctBy(x => x.Text.ToUpperInvariant()).OrderBy(x => x.Text).ThenBy(x => x.Culture))
                    yield return dUrl;
            }

            // get the 'other' urls - ie not what you'd get with GetUrl() but urls that would route to the document, nevertheless.
            // for these 'other' urls, we don't check whether they are routable, collide, anything - we just report them.
            foreach (var otherUrl in umbracoContext.UrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture))
                if (urls.Add(otherUrl)) //avoid duplicates
                    yield return otherUrl;
        }
        
        /// <summary>
        /// Tries to return a <see cref="UrlInfo"/> for each culture for the content while detecting collisions/errors
        /// </summary>
        /// <param name="content"></param>
        /// <param name="cultures"></param>
        /// <param name="publishedRouter"></param>
        /// <param name="umbracoContext"></param>
        /// <param name="contentService"></param>
        /// <param name="textService"></param>
        /// <param name="logger"></param>
        /// <returns></returns>
        private static IEnumerable<UrlInfo> GetContentUrlsByCulture(IContent content,
            IEnumerable<string> cultures,
            IPublishedRouter publishedRouter,
            UmbracoContext umbracoContext,
            IContentService contentService,
            ILocalizedTextService textService,
            ILogger logger)
        {
            foreach (var culture in cultures)
            {
                // if content is variant, and culture is not published, skip
                if (content.ContentType.VariesByCulture() && !content.IsCulturePublished(culture))
                    continue;

                // if it's variant and culture is published, or if it's invariant, proceed

                string url;
                try
                {
                    url = umbracoContext.UrlProvider.GetUrl(content.Id, culture);
                }
                catch (Exception ex)
                {
                    logger.Error<UrlProvider>(ex, "GetUrl exception.");
                    url = "#ex";
                }

                switch (url)
                {
                    // deal with 'could not get the url'
                    case "#":
                        yield return HandleCouldNotGetUrl(content, culture, contentService, textService);
                        break;

                    // deal with exceptions
                    case "#ex":
                        yield return UrlInfo.Message(textService.Localize("content/getUrlException"), culture);
                        break;

                    // got a url, deal with collisions, add url
                    default:
                        if (DetectCollision(content, url, culture, umbracoContext, publishedRouter, textService, out var urlInfo)) // detect collisions, etc
                            yield return urlInfo;
                        else
                            yield return UrlInfo.Url(url, culture);
                        break;
                }
            }
        }

        private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService)
        {
            // document has a published version yet its url is "#" => a parent must be
            // unpublished, walk up the tree until we find it, and report.
            var parent = content;
            do
            {
                parent = parent.ParentId > 0 ? contentService.GetParent(parent) : null;
            }
            while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture)));

            if (parent == null) // oops, internal error
                return UrlInfo.Message(textService.Localize("content/parentNotPublishedAnomaly"), culture);

            else if (!parent.Published) // totally not published
                return UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] {parent.Name}), culture);

            else // culture not published
                return UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] {parent.Name}), culture);
        }

        private static bool DetectCollision(IContent content, string url, string culture, UmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, out UrlInfo urlInfo)
        {
            // test for collisions on the 'main' url
            var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute);
            if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(umbracoContext.CleanedUmbracoUrl);
            uri = UriUtility.UriToUmbraco(uri);
            var pcr = publishedRouter.CreateRequest(umbracoContext, uri);
            publishedRouter.TryRouteRequest(pcr);

            urlInfo = null;

            if (pcr.HasPublishedContent == false)
            {
                urlInfo = UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture);
                return true;
            }

            if (pcr.IgnorePublishedContentCollisions)
                return false;

            if (pcr.PublishedContent.Id != content.Id)
            {
                var o = pcr.PublishedContent;
                var l = new List<string>();
                while (o != null)
                {
                    l.Add(o.Name);
                    o = o.Parent;
                }
                l.Reverse();
                var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")";

                 urlInfo = UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture);
                return true;
            }

            // no collision
            return false;
        }
    }
}
