using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using System;
using Newtonsoft.Json;
using RothenburgAR.Common;
using System.IO;
using System.Security.Cryptography;
using System.Text.RegularExpressions;

namespace RothenburgAR.Updater
{
    public enum UpdateState
    {
        UpdatesFound,
        ConfirmationPending,
        Downloading,
        Completed,
        Failed
    }

    public class UpdateManager : Singleton<UpdateManager>
    {
        public UpdateState UpdateState { get; private set; }

        public int CurrentProgress { get; private set; }
        public int MaxProgress { get; private set; }

        private float afterDownloadWaitTime = 10f;
        private readonly int retriesUntilFailure = 4;
        private readonly string trackerMainFile = "tracker.dat";

        public ApiVersioncheckAnswer VersionAnswer { get; set; }

        Dictionary<string, List<ApiExhibit>> ExhibitMetas = new Dictionary<string, List<ApiExhibit>>();
        private LogFileHandler logFileHandler;

        void Start()
        {
            if (Application.internetReachability == NetworkReachability.NotReachable)
            {
                // just continue to app
                UpdatesCompleted();
                return;
            }

            CheckForUpdates();
        }

        private void CheckForUpdates()
        {
            // check for updates and ask for permission to download if there are any
            string versionMap = GenerateVersionMap();
            CheckForUpdates(versionMap, 0);
        }

        private void CheckForUpdates(string versionMap, int retries)
        {
            HttpHandler http = new HttpRequest(ApiInfo.VersionCheckEndpoint, HttpVerb.POST, versionMap).Send();
            http.Operation.completed += ar =>
            {
                Debug.Log("VersionAnswer: " + http.Download.text);
                var answer = JsonConvert.DeserializeObject<ApiVersioncheckAnswer>(http.Download.text);
                if (http.IsError)
                {
                    if (retries < retriesUntilFailure)
                    {
                        CheckForUpdates(versionMap, retries + 1);
                        return;
                    }
                    else
                    {
                        UpdateFailed(http);
                        return;
                    }
                }

                Debug.Log(string.Format("{1}-DONE with {0}", ApiInfo.VersionCheckEndpoint, Time.realtimeSinceStartup));
                VersionAnswer = answer;

                if (VersionAnswer.Data.TrueForAll(d => d.Meta.Status == VersionStatus.ok && d.Tracker.Status == VersionStatus.ok))
                {
                    afterDownloadWaitTime = 0f;
                    StartCoroutine(CheckFiles());
                    return;
                }

                UpdateState = UpdateState.UpdatesFound;

                if (Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork)
                {
                    TriggerUpdate();
                }
                else
                {
                    UpdateState = UpdateState.ConfirmationPending;
                }
            };
        }

        private string GenerateVersionMap()
        {
            string result = "{ ";

            var rootDir = new DirectoryInfo(PathHelper.ExhibitionPath);
            if (!rootDir.Exists)
            {
                rootDir.Create();
            }

            var exhibitionDirs = rootDir.GetDirectories().ToList();

            foreach (var dir in exhibitionDirs)
            {
                var versionFilePath = Path.Combine(dir.FullName, "version.txt");
                if (!File.Exists(versionFilePath)) continue;

                var version = long.Parse(File.ReadAllText(versionFilePath));
                result += @"""{id}"":""{version}"",".Replace("{id}", dir.Name).Replace("{version}", version.ToString());
            };

            result = result.Substring(0, result.Length - 1) + "}";
            return result;
        }

        public void TriggerUpdate()
        {
            /*
             * dir structure:
             *     data
             *         exhibition
             *             id
             *                 tracker.dat
             *                 tracker.xml
             *                 de
             *                     meta.json
             *                 en
             *                     meta.json
             *             id
             *                 ...
             *         media
             *             ...
             */

            UpdateState = UpdateState.Downloading;

            if (VersionAnswer == null)
            {
                // try to get VersionAnswer first if that's what failed
                CheckForUpdates();
                return;
            }

            //TODO write languages to file the app can read (so that the languagemanager can decide which languages the user can choose from)

            var updatedExhibitions = VersionAnswer.Data.Where(d => d.Meta.Status == VersionStatus.updated).ToList();
            var updatedTracker = VersionAnswer.Data.Where(d => d.Tracker.Status == VersionStatus.updated).ToList();
            updatedExhibitions.Union(updatedTracker).Distinct().ToList().ForEach(updatedExhibition =>
            {
                // create exhibition directories
                var path = Path.Combine(PathHelper.ExhibitionPath, updatedExhibition.Id);
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }
            });

            updatedExhibitions.ForEach(d => UpdateExhibition(d, updatedExhibitions));
            StartCoroutine(UpdateExhibitionsCoroutine(updatedExhibitions));
        }

        private IEnumerator UpdateExhibitionsCoroutine(List<ApiExhibitionVersion> updatedExhibitions)
        {
            yield return new WaitUntil(() => updatedExhibitions.Count == 0);
            StartCoroutine(CheckFiles());
        }

        private void UpdateExhibition(ApiExhibitionVersion exhibition, List<ApiExhibitionVersion> updatedExhibitions)
        {
            var exhibitionDownloads = new List<HttpHandler>();

            foreach (var lang in VersionAnswer.Languages)
            {
                var path = PathHelper.CombinePaths(PathHelper.ExhibitionPath, exhibition.Id, lang);
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }

                UpdateMetaFile(exhibition, path, lang, exhibitionDownloads);
            }

            var versionPath = PathHelper.CombinePaths(PathHelper.ExhibitionPath, exhibition.Id, "version.txt");
            var version = exhibition.Meta.UpdateVersion.ToString();
            StartCoroutine(UpdateVersionFileCoroutine(exhibitionDownloads, versionPath, version, exhibition, updatedExhibitions));
        }

        private IEnumerator UpdateVersionFileCoroutine(List<HttpHandler> exhibitionDownloads, string versionPath, string version, ApiExhibitionVersion exhibition, List<ApiExhibitionVersion> updatedExhibitions)
        {
            // updates version file only if all sub files were downloaded successfully
            yield return new WaitUntil(() => exhibitionDownloads.All(h => h.IsDone));
            if (exhibitionDownloads.Any(http => http.IsError)) yield break;

            File.WriteAllText(versionPath, version);
            updatedExhibitions.Remove(exhibition);
        }

        private void UpdateMetaFile(ApiExhibitionVersion exhibition, string path, string lang, List<HttpHandler> exhibitionDownloads, int retries = 0)
        {
            var url = exhibition.Meta.UpdateUrl.Replace("{lang}", lang);
            var http = new HttpRequest(url, HttpVerb.GET).Send();
            exhibitionDownloads.Add(http);

            http.Operation.completed += ar =>
            {
                if (http.IsError)
                {
                    if (retries < retriesUntilFailure)
                    {
                        exhibitionDownloads.Remove(http);
                        UpdateMetaFile(exhibition, path, lang, exhibitionDownloads, retries + 1);
                        return;
                    }
                    else
                    {
                        UpdateFailed(http);
                        return;
                    }
                }

                File.WriteAllText(Path.Combine(path, "meta.json"), http.Download.text, System.Text.Encoding.UTF8);
                Debug.Log(string.Format("{1}-DONE with {0}", url, Time.realtimeSinceStartup));

                var exhibits = JsonConvert.DeserializeObject<List<ApiExhibit>>(http.Download.text);
                if (exhibits == null)
                {
                    Debug.LogError(string.Format("Exhibit could not be parsed from Json:\nurl: {0}\njson: {1}", url, http.Download.text));
                    http.IsError = true;
                    UpdateFailed(http);
                }
            };
        }

        private IEnumerator CheckFiles()
        {
            var usedFileList = new Dictionary<string, FileDownloadInfo>();
            GenerateUsedFileList(usedFileList);

            MaxProgress = usedFileList.Count;
            foreach (var file in usedFileList)
            {
                var fileInfo = file.Value;
                Dictionary<string, byte[]> data = new Dictionary<string, byte[]>();
                yield return DownloadFile(fileInfo, data);

                foreach (var item in data)
                {
                    if (!Directory.Exists(fileInfo.directory)) Directory.CreateDirectory(fileInfo.directory);
                    File.WriteAllBytes(item.Key, item.Value);
                }
            }

            var deletedData = VersionAnswer.Data.Where(d =>
                d.Meta.Status == VersionStatus.deleted
                || d.Tracker.Status == VersionStatus.deleted).ToList();
            deletedData.ForEach(d => DeleteExhibition(d));

            CleanupMediaFiles();
            UpdatesCompleted();
        }

        private void GenerateUsedFileList(Dictionary<string, FileDownloadInfo> downloadList)
        {
            ExhibitMetas.Clear();
            var rootDir = new DirectoryInfo(PathHelper.ExhibitionPath);
            var exhibitionDirs = rootDir.GetDirectories().ToList();

            foreach (var dir in exhibitionDirs)
            {
                var versionFilePath = Path.Combine(dir.FullName, "version.txt");
                if (!File.Exists(versionFilePath)) continue;

                var exhibitionId = dir.Name;
                foreach (var lang in VersionAnswer.Languages)
                {
                    var metaFilePath = PathHelper.CombinePaths(PathHelper.ExhibitionPath, exhibitionId, lang, "meta.json");
                    var metaFile = File.ReadAllText(metaFilePath);
                    var exhibits = JsonConvert.DeserializeObject<List<ApiExhibit>>(metaFile);
                    ExhibitMetas.Add(exhibitionId + "/" + lang, exhibits);
                }
            }

            // would check for   || d.Tracker.Status == VersionStatus.ok   as well but we don't get an updateUrl for those so it's no use
            var updatedTracker = VersionAnswer.Data.Where(d => d.Tracker.Status == VersionStatus.updated).ToList();
            foreach (var exhibition in updatedTracker)
            {
                var url = exhibition.Tracker.UpdateUrl;
                var directory = PathHelper.CombinePaths(PathHelper.ExhibitionPath, exhibition.Id);
                var filepath = PathHelper.CombinePaths(directory, trackerMainFile);

                downloadList.Add(url, new FileDownloadInfo(url, filepath, directory));
            }

            var mediaList = ExhibitMetas.Values.ToList().SelectMany(m => m.SelectMany(i => i.Pois.Select(p => p.MediaId))).Where(m => m != null).Distinct().ToList();
            foreach (var mediaId in mediaList)
            {
                var url = ApiInfo.FileEndpoint.Replace("{id}", mediaId);
                var directory = PathHelper.CombinePaths(PathHelper.MediaPath, mediaId);
                var filepath = PathHelper.CombinePaths(directory, mediaId + ".obj");

                downloadList.Add(url, new FileDownloadInfo(url, filepath, directory));
            }
        }

        private class FileDownloadInfo
        {
            public string url;
            public string filepath;
            public string directory;

            public FileDownloadInfo(string url, string filepath, string directory)
            {
                this.url = url;
                this.filepath = filepath;
                this.directory = directory;
            }
        }

        private IEnumerator DownloadFile(FileDownloadInfo info, Dictionary<string, byte[]> data)
        {
            if (!Directory.Exists(info.directory)) Directory.CreateDirectory(info.directory);

            var eTag = GenerateETag(info.filepath);
            yield return DownloadFile(info, eTag, data);
        }

        private IEnumerator DownloadFile(FileDownloadInfo info, string eTag, Dictionary<string, byte[]> data, int retries = 0)
        {
            var url = info.url;
            var http = new HttpRequest(url, HttpVerb.GET)
                .WithHeader("If-None-Match", eTag)
                .WithHeader("Accept-Encoding", "gzip,deflate")
                .Send();

            yield return new WaitUntil(() => http.IsDone);

            if (http.IsError)
            {
                if (retries < retriesUntilFailure)
                {
                    yield return DownloadFile(info, eTag, data, retries + 1);
                }
                else
                {
                    UpdateFailed(http);
                    yield return null;
                }
            }

            if (http.Request.responseCode != 304)
            {
                if (!data.ContainsKey(info.filepath))
                {
                    // gzipped files are un-gzipped automagically
                    data.Add(info.filepath, http.Download.data);
                }
            }

            var subfilesHeader = http.Request.GetResponseHeader("subfiles");
            var test = http.Request.GetResponseHeaders();
            if (subfilesHeader != null)
            {
                var subfiles = subfilesHeader.Split(new string[1] { ";;" }, StringSplitOptions.RemoveEmptyEntries).ToList();

                MaxProgress += subfiles.Count(subfileName => !data.Keys.Contains(Path.Combine(info.directory, subfileName)));

                foreach (var subfileName in subfiles)
                {
                    var subfileInfo = new FileDownloadInfo(
                        info.url + "/" + subfileName,
                        Path.Combine(info.directory, subfileName),
                        info.directory);

                    var subfileETag = GenerateETag(info.filepath);
                    //                                              HACK to prevent infinite subfile loop
                    if (!data.ContainsKey(subfileInfo.filepath) && subfileName != "brick.png")
                    {
                        yield return DownloadFile(subfileInfo, subfileETag, data);
                    }
                }
            }

            Debug.Log(string.Format("{1}-DONE with {0}", url, Time.realtimeSinceStartup));
            CurrentProgress += 1;
        }

        private string GenerateETag(string filepath)
        {
            string eTag = "";
            if (File.Exists(filepath))
            {
                var bytes = File.ReadAllBytes(filepath);
                SHA256Managed hasher = new SHA256Managed();
                byte[] hash = hasher.ComputeHash(bytes);
                eTag = "sha256-";
                foreach (byte x in hash)
                {
                    eTag += String.Format("{0:x2}", x);
                }
            }
            return eTag;
        }

        private void DeleteExhibition(ApiExhibitionVersion exhibition)
        {
            var path = Path.Combine(PathHelper.ExhibitionPath, exhibition.Id);
            Directory.Delete(path, true);
        }

        private void CleanupMediaFiles()
        {
            var requiredMedia = ExhibitMetas.Values.ToList().SelectMany(m => m.SelectMany(i => i.Pois.Select(p => p.MediaId))).Where(m => m != null).Distinct().ToList();

            var mediaDir = new DirectoryInfo(PathHelper.MediaPath);
            var mediaDirs = mediaDir.GetDirectories().ToList();
            foreach (var dir in mediaDirs)
            {
                //                                    HACK to preserve passion media files
                if (!requiredMedia.Contains(dir.Name) && Regex.IsMatch(dir.Name, "[0-9]"))
                {
                    Directory.Delete(dir.FullName, true);
                }
            }
        }

        private void UpdateFailed(HttpHandler http)
        {
            Debug.LogError(String.Format("Error while downloading\nurl: {0}\nNetwork Error: {1}\nHttp Error: {2}\nHttp Response Code: {3}",
                    http.Request.url,
                    http.Request.isNetworkError,
                    http.Request.isHttpError,
                    http.Request.responseCode));

            UpdateState = UpdateState.Failed;
        }

        public void UpdatesCompleted()
        {
            //TODO force switch to UpdaterView when update needs to be applied
            UpdateState = UpdateState.Completed;
        }
    }
}