using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;

namespace NzbDrone.Core.Download.Clients.Aria2
{
    public interface IAria2Proxy
    {
        string GetVersion(Aria2Settings settings);
        string AddUri(Aria2Settings settings, string magnet);
        string AddTorrent(Aria2Settings settings, byte[] torrent);
        Dictionary<string, string> GetGlobals(Aria2Settings settings);
        List<Aria2Status> GetTorrents(Aria2Settings settings);
        Aria2Status GetFromGID(Aria2Settings settings, string gid);
    }

    public class Aria2Proxy : IAria2Proxy
    {
        private readonly IHttpClient _httpClient;

        public Aria2Proxy(IHttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public string GetVersion(Aria2Settings settings)
        {
            var response = ExecuteRequest(settings, "aria2.getVersion", GetToken(settings));

            var element = response.XPathSelectElement("./methodResponse/params/param/value");

            var version = new Aria2Version(element);

            return version.Version;
        }

        public Aria2Status GetFromGID(Aria2Settings settings, string gid)
        {
            var response = ExecuteRequest(settings, "aria2.tellStatus", GetToken(settings), gid);

            var element = response.XPathSelectElement("./methodResponse/params/param/value");

            return new Aria2Status(element);
        }

        private List<Aria2Status> GetTorrentsMethod(Aria2Settings settings, string method, params object[] args)
        {
            var allArgs = new List<object> { GetToken(settings) };
            if (args.Any())
            {
                allArgs.AddRange(args);
            }

            var response = ExecuteRequest(settings, method, allArgs.ToArray());

            var element = response.XPathSelectElement("./methodResponse/params/param/value/array/data");

            var torrents = element?.Elements()
                               .Select(x => new Aria2Status(x))
                               .ToList()
                           ?? new List<Aria2Status>();
            return torrents;
        }

        public List<Aria2Status> GetTorrents(Aria2Settings settings)
        {
            var active = GetTorrentsMethod(settings, "aria2.tellActive");

            var waiting = GetTorrentsMethod(settings, "aria2.tellWaiting", 0, 10 * 1024);

            var stopped = GetTorrentsMethod(settings, "aria2.tellStopped", 0, 10 * 1024);

            var items = new List<Aria2Status>();

            items.AddRange(active);
            items.AddRange(waiting);
            items.AddRange(stopped);

            return items;
        }

        public Dictionary<string, string> GetGlobals(Aria2Settings settings)
        {
            var response = ExecuteRequest(settings, "aria2.getGlobalOption", GetToken(settings));

            var element = response.XPathSelectElement("./methodResponse/params/param/value");

            var result = new Aria2Dict(element);

            return result.Dict;
        }

        public string AddUri(Aria2Settings settings, string magnet)
        {
            var options = new Dictionary<string, string>();

            if (settings.Directory.IsNotNullOrWhiteSpace())
            {
                options.Add("dir", settings.Directory);
            }

            var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet }, options);
            var gid = response.GetStringResponse();

            return gid;
        }

        public string AddTorrent(Aria2Settings settings, byte[] torrent)
        {
            // Aria2's second parameter is an array of URIs and needs to be sent if options are provided, this satisfies that requirement.
            var emptyListOfUris = new List<string>();
            var options = new Dictionary<string, string>();

            if (settings.Directory.IsNotNullOrWhiteSpace())
            {
                options.Add("dir", settings.Directory);
            }

            var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent, emptyListOfUris, options);
            var gid = response.GetStringResponse();

            return gid;
        }

        private string GetToken(Aria2Settings settings)
        {
            return $"token:{settings?.SecretToken}";
        }

        private XDocument ExecuteRequest(Aria2Settings settings, string methodName, params object[] args)
        {
            var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.RpcPath)
            {
                LogResponseContent = true,
            };

            var request = requestBuilder.Call(methodName, args).Build();

            var response = _httpClient.Execute(request);

            var doc = XDocument.Parse(response.Content);

            var faultElement = doc.XPathSelectElement("./methodResponse/fault");

            if (faultElement != null)
            {
                var fault = new Aria2Fault(faultElement);

                throw new DownloadClientException($"Aria2 returned error code {fault.FaultCode}: {fault.FaultString}");
            }

            return doc;
        }
    }
}
