using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Script.Serialization;
using System.Xml.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Model.Xbmc;
using NzbDrone.Core.Providers.Core;
using NzbDrone.Core.Providers.Xbmc;
using NzbDrone.Core.Repository;

namespace NzbDrone.Core.Providers
{
    public class XbmcProvider
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
        private readonly ConfigProvider _configProvider;
        private readonly HttpProvider _httpProvider;
        private readonly EventClientProvider _eventClientProvider;

        public XbmcProvider(ConfigProvider configProvider, HttpProvider httpProvider, EventClientProvider eventClientProvider)
        {
            _configProvider = configProvider;
            _httpProvider = httpProvider;
            _eventClientProvider = eventClientProvider;
        }

        public XbmcProvider()
        {

        }

        public virtual void Notify(string header, string message)
        {
            //Always use EventServer, until Json has real support for it
            foreach (var host in _configProvider.XbmcHosts.Split(','))
            {
                Logger.Trace("Sending Notifcation to XBMC Host: {0}", host);
                _eventClientProvider.SendNotification(header, message, IconType.Jpeg, "NzbDrone.jpg", GetHostWithoutPort(host));
            }
        }

        public virtual void Update(Series series)
        {
            //Use Json for Eden/Nightly or depricated HTTP for 10.x (Dharma) to get the proper path
            //Perform update with EventServer (Json currently doesn't support updating a specific path only - July 2011)

            var username = _configProvider.XbmcUsername;
            var password = _configProvider.XbmcPassword;

            foreach (var host in _configProvider.XbmcHosts.Split(','))
            {
                Logger.Trace("Determining version of XBMC Host: {0}", host);
                var version = GetJsonVersion(host, username, password);

                //If Dharma
                if (version == new XbmcVersion(2))
                {
                    //Check for active player only when we should skip updates when playing
                    if (!_configProvider.XbmcUpdateWhenPlaying)
                    {
                        Logger.Trace("Determining if there are any active players on XBMC host: {0}", host);
                        var activePlayers = GetActivePlayersDharma(host, username, password);

                        //If video is currently playing, then skip update
                        if(activePlayers["video"])
                        {
                            Logger.Debug("Video is currently playing, skipping library update");
                            continue;
                        }
                    }

                    UpdateWithHttp(series, host, username, password);
                }

                //If Eden or newer (attempting to make it future compatible)
                else if (version == new XbmcVersion(3) || version == new XbmcVersion(4))
                {
                    //Check for active player only when we should skip updates when playing
                    if (!_configProvider.XbmcUpdateWhenPlaying)
                    {
                        Logger.Trace("Determining if there are any active players on XBMC host: {0}", host);
                        var activePlayers = GetActivePlayersEden(host, username, password);

                        //If video is currently playing, then skip update
                        if(activePlayers.Any(a => a.Type.Equals("video")))
                        {
                            Logger.Debug("Video is currently playing, skipping library update");
                            continue;
                        }
                    }

                    UpdateWithJsonExecBuiltIn(series, host, username, password);
                }

                else if (version >= new XbmcVersion(5))
                {
                    //Check for active player only when we should skip updates when playing
                    if (!_configProvider.XbmcUpdateWhenPlaying)
                    {
                        Logger.Trace("Determining if there are any active players on XBMC host: {0}", host);
                        var activePlayers = GetActivePlayersEden(host, username, password);

                        //If video is currently playing, then skip update
                        if (activePlayers.Any(a => a.Type.Equals("video")))
                        {
                            Logger.Debug("Video is currently playing, skipping library update");
                            continue;
                        }
                    }

                    UpdateWithJsonVideoLibraryScan(series, host, username, password);
                }

                //Log Version zero if check failed
                else
                    Logger.Trace("Unknown version: [{0}], skipping.", version);
            }
        }

        public virtual bool UpdateWithJsonExecBuiltIn(Series series, string host, string username, string password)
        {
            try
            {
                //Use Json!
                var xbmcShows = GetTvShowsJson(host, username, password);

                TvShow path = null;

                //Log if response is null, otherwise try to find XBMC's path for series
                if (xbmcShows == null)
                    Logger.Trace("Failed to get TV Shows from XBMC");

                else
                    path = xbmcShows.FirstOrDefault(s => s.ImdbNumber == series.SeriesId || s.Label == series.Title);

                //var hostOnly = GetHostWithoutPort(host);

                if (path != null)
                {
                    Logger.Trace("Updating series [{0}] (Path: {1}) on XBMC host: {2}", series.Title, path.File, host);
                    var command = String.Format("ExecBuiltIn(UpdateLibrary(video,{0}))", path.File);
                    SendCommand(host, command, username, password);
                }

                else
                {
                    Logger.Trace("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", series.Title, host);
                    SendCommand(host, "ExecBuiltIn(UpdateLibrary(video))", username, password);
                }
            }

            catch (Exception ex)
            {
                Logger.DebugException(ex.Message, ex);
                return false;
            }

            return true;
        }

        public virtual bool UpdateWithJsonVideoLibraryScan(Series series, string host, string username, string password)
        {
            try
            {
                //Use Json!
                var xbmcShows = GetTvShowsJson(host, username, password);

                TvShow path = null;

                //Log if response is null, otherwise try to find XBMC's path for series
                if (xbmcShows == null)
                    Logger.Trace("Failed to get TV Shows from XBMC");

                else
                    path = xbmcShows.FirstOrDefault(s => s.ImdbNumber == series.SeriesId || s.Label == series.Title);

                var postJson = new JObject();
                postJson.Add(new JProperty("jsonrpc", "2.0"));
                postJson.Add(new JProperty("method", "VideoLibrary.Scan"));
                postJson.Add(new JProperty("id", 55));

                if (path != null)
                {
                    Logger.Trace("Updating series [{0}] (Path: {1}) on XBMC host: {2}", series.Title, path.File, host);
                    postJson.Add(new JProperty("params", new JObject(new JObject(new JProperty("directory", path.File)))));
                }

                else
                    Logger.Trace("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", series.Title, host);

                var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());

                if (CheckForJsonError(response))
                    return false;

                Logger.Trace(" from response");
                var result = JsonConvert.DeserializeObject<XbmcJsonResult<String>>(response);

                if(!result.Result.Equals("OK", StringComparison.InvariantCultureIgnoreCase))
                    return false;
            }

            catch (Exception ex)
            {
                Logger.DebugException(ex.Message, ex);
                return false;
            }

            return true;
        }

        public virtual bool UpdateWithHttp(Series series, string host, string username, string password)
        {
            try
            {
                Logger.Trace("Sending Update DB Request to XBMC Host: {0}", host);
                var xbmcSeriesPath = GetXbmcSeriesPath(host, series.SeriesId, username, password);

                //If the path is found update it, else update the whole library
                if (!String.IsNullOrEmpty(xbmcSeriesPath))
                {
                    Logger.Trace("Updating series [{0}] on XBMC host: {1}", series.Title, host);
                    var command = String.Format("ExecBuiltIn(UpdateLibrary(video,{0}))", xbmcSeriesPath);
                    SendCommand(host, command, username, password);
                }

                else
                {
                    //Update the entire library
                    Logger.Trace("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", series.Title, host);
                    SendCommand(host, "ExecBuiltIn(UpdateLibrary(video))", username, password);
                }
            }

            catch (Exception ex)
            {
                Logger.DebugException(ex.Message, ex);
                return false;
            }
            
            return true;
        }

        public virtual void Clean()
        {
            //Use EventServer, once Dharma is extinct use Json?

            foreach (var host in _configProvider.XbmcHosts.Split(','))
            {
                Logger.Trace("Sending DB Clean Request to XBMC Host: {0}", host);
                var command = "ExecBuiltIn(CleanLibrary(video))";
                _eventClientProvider.SendAction(GetHostWithoutPort(host), ActionType.ExecBuiltin, command);
            }
        }

        public virtual string SendCommand(string host, string command, string username, string password)
        {
            var url = String.Format("http://{0}/xbmcCmds/xbmcHttp?command={1}", host, command);

            if (!String.IsNullOrEmpty(username))
            {
                return _httpProvider.DownloadString(url, username, password);
            }

            return _httpProvider.DownloadString(url);
        }

        public virtual string GetXbmcSeriesPath(string host, int seriesId, string username, string password)
        {
            var query =
                String.Format(
                    "select path.strPath from path, tvshow, tvshowlinkpath where tvshow.c12 = {0} and tvshowlinkpath.idShow = tvshow.idShow and tvshowlinkpath.idPath = path.idPath",
                    seriesId);
            var command = String.Format("QueryVideoDatabase({0})", query);

            const string setResponseCommand =
                "SetResponseFormat(webheader;false;webfooter;false;header;<xml>;footer;</xml>;opentag;<tag>;closetag;</tag>;closefinaltag;false)";
            const string resetResponseCommand = "SetResponseFormat()";

            SendCommand(host, setResponseCommand, username, password);
            var response = SendCommand(host, command, username, password);
            SendCommand(host, resetResponseCommand, username, password);

            if (String.IsNullOrEmpty(response))
                return String.Empty;

            var xDoc = XDocument.Load(new StringReader(response.Replace("&", "&amp;")));
            var xml = (from x in xDoc.Descendants("xml") select x).FirstOrDefault();

            if (xml == null)
                return String.Empty;

            var field = xml.Descendants("field").FirstOrDefault();

            if (field == null)
                return String.Empty;

            return field.Value;
        }

        public virtual XbmcVersion GetJsonVersion(string host, string username, string password)
        {
            //2 = Dharma
            //3 & 4 = Eden
            //5 & 6 = Frodo

            try
            {
                var postJson = new JObject();
                postJson.Add(new JProperty("jsonrpc", "2.0"));
                postJson.Add(new JProperty("method", "JSONRPC.Version"));
                postJson.Add(new JProperty("id", 10));

                var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());

                if (CheckForJsonError(response))
                    return new XbmcVersion();

                Logger.Trace("Getting version from response");
                var result = JsonConvert.DeserializeObject<XbmcJsonResult<JObject>>(response);

                var versionObject = result.Result.Property("version");

                if (versionObject.Value.Type == JTokenType.Integer)
                    return new XbmcVersion((int)versionObject.Value);

                if(versionObject.Value.Type == JTokenType.Object)
                    return JsonConvert.DeserializeObject<XbmcVersion>(versionObject.Value.ToString());

                throw new InvalidCastException("Unknown Version structure!: " + versionObject);
            }

            catch (Exception ex)
            {
                Logger.DebugException(ex.Message, ex);
            }

            return new XbmcVersion();
        }

        public virtual Dictionary<string, bool> GetActivePlayersDharma(string host, string username, string password)
        {
            try
            {
                var postJson = new JObject();
                postJson.Add(new JProperty("jsonrpc", "2.0"));
                postJson.Add(new JProperty("method", "Player.GetActivePlayers"));
                postJson.Add(new JProperty("id", 10));

                var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());

                if (CheckForJsonError(response))
                    return null;

                var result = JsonConvert.DeserializeObject<ActivePlayersDharmaResult>(response);

                return result.Result;
            }

            catch (Exception ex)
            {
                Logger.DebugException(ex.Message, ex);
            }

            return null;
        }

        public virtual List<ActivePlayer> GetActivePlayersEden(string host, string username, string password)
        {
            try
            {
                var postJson = new JObject();
                postJson.Add(new JProperty("jsonrpc", "2.0"));
                postJson.Add(new JProperty("method", "Player.GetActivePlayers"));
                postJson.Add(new JProperty("id", 10));

                var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());

                if (CheckForJsonError(response))
                    return null;

                var result = JsonConvert.DeserializeObject<ActivePlayersEdenResult>(response);

                return result.Result;
            }

            catch (Exception ex)
            {
                Logger.DebugException(ex.Message, ex);
            }

            return null;
        }

        public virtual List<TvShow> GetTvShowsJson(string host, string username, string password)
        {
            try
            {
                var postJson = new JObject();
                postJson.Add(new JProperty("jsonrpc", "2.0"));
                postJson.Add(new JProperty("method", "VideoLibrary.GetTvShows"));
                postJson.Add(new JProperty("params", new JObject { new JProperty("properties", new string[] { "file", "imdbnumber" }) }));
                postJson.Add(new JProperty("id", 10));

                var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());

                if (CheckForJsonError(response))
                    return null;

                var result = JsonConvert.DeserializeObject<TvShowResponse>(response);
                var shows = result.Result.TvShows;

                return shows;
            }
            catch (Exception ex)
            {
                Logger.DebugException(ex.Message, ex);
            }
            return null;
        }

        public virtual bool CheckForJsonError(string response)
        {
            Logger.Trace("Looking for error in response: {0}", response);

            if (response.StartsWith("{\"error\""))
            {
                var serializer = new JavaScriptSerializer();
                var error = serializer.Deserialize<ErrorResult>(response);
                var code = error.Error["code"];
                var message = error.Error["message"];

                Logger.Debug("XBMC Json Error. Code = {0}, Message: {1}", code, message);
                return true;
            }

            if (String.IsNullOrWhiteSpace(response))
            {
                Logger.Debug("Invalid response from XBMC, the response is not valid JSON");
                return true;
            }

            return false;
        }

        public virtual void TestNotification(string hosts)
        {
            foreach (var host in hosts.Split(','))
            {
                Logger.Trace("Sending Test Notifcation to XBMC Host: {0}", host);
                _eventClientProvider.SendNotification("Test Notification", "Success! Notifications are setup correctly", IconType.Jpeg, "NzbDrone.jpg", GetHostWithoutPort(host));
            }
        }

        public virtual void TestJsonApi(string hosts, string username, string password)
        {
            foreach (var host in hosts.Split(','))
            {
                Logger.Trace("Sending Test Notifcation to XBMC Host: {0}", host);
                var version = GetJsonVersion(host, username, password);
                if (version == new XbmcVersion())
                    throw new Exception("Failed to get JSON version in test");
            }
        }

        private string GetHostWithoutPort(string address)
        {
            return address.Split(':')[0];
        }
    }
}