using System;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Security.Cryptography;
using MaxLib.WebServer;
using MaxLib.WebServer.Post;

namespace Dacryptero.Web
{
    public class UploadService : WebService
    {
        public UploadService() 
            : base(ServerStage.CreateDocument)
        {
        }

        public override bool CanWorkWith(WebProgressTask task)
        {
            return task.Request.Location.IsUrl(new []{ "upload" });
        }

        private void CloseWithError(WebProgressTask task, string error)
        {
            task.Response.StatusCode = HttpStateCode.Forbidden;
            var m = new MemoryStream();
            var w = new Utf8JsonWriter(m);
            w.WriteStartObject();
            w.WriteBoolean("success", false);
            w.WriteString("error", error);
            w.WriteEndObject();
            w.Flush();
            task.Document.DataSources.Add(new HttpStreamDataSource(m)
            {
                MimeType = MimeType.ApplicationJson,
            });
            Serilog.Log.Error("cannot finish upload: {error}", error);
        }

        private string GetTargetName(string root, byte[] hash)
        {
            var hashName = Convert.ToHexString(hash);
            var path = Path.Combine(
                "files",
                hashName[0..4],
                hashName[4..8],
                hashName[8..]
            );
            if (!File.Exists(Path.Combine(root, path)))
                return path;
            int incr = 1;
            while (File.Exists(Path.Combine(root, $"{path}-{incr}")))
                incr++;
            return $"{path}-{incr}";
        }

        private async Task<Data.FileEntry> PerformUpload(LiteDB.ObjectId id, WebProgressTask task,
            DB.DBConfig config, UnknownPostData data
        )
        {
            var path = Path.Combine(config.DirectoryPath, "upload", id.ToString());
            if (!Directory.Exists(Path.Combine(config.DirectoryPath, "upload")))
                Directory.CreateDirectory(Path.Combine(config.DirectoryPath, "upload"));

            // upload the data in an encrypted format
            using var aes = Aes.Create();
            using var encryptor = aes.CreateEncryptor();
            using var file = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);
            using var crypto = new CryptoStream(file, encryptor, CryptoStreamMode.Write);
            await data.Data.CopyToAsync(crypto).ConfigureAwait(false);
            await crypto.FlushFinalBlockAsync().ConfigureAwait(false);
            await crypto.FlushAsync().ConfigureAwait(false);
            await file.FlushAsync().ConfigureAwait(false);

            // verify the upload and calculate the SHA512 Hash of it
            file.Position = 0;
            using var sha512 = SHA512.Create();
            var hash = await sha512.ComputeHashAsync(file).ConfigureAwait(false);

            // {
            //     Serilog.Log.Verbose("Encryption Key={key}", Convert.ToBase64String(aes.Key));
            //     Serilog.Log.Verbose("Encryption IV={keivy}", Convert.ToBase64String(aes.IV));
            // }

            crypto.Dispose();
            file.Dispose();

            // move the file to the target directory
            var targetName = GetTargetName(config.DirectoryPath, hash);
            var target = Path.Combine(config.DirectoryPath, targetName);
            var targetDir = Path.GetDirectoryName(target)!;
            if (!Directory.Exists(targetDir))
                Directory.CreateDirectory(targetDir);
            File.Move(path, target);

            // return the entry
            return new Data.FileEntry
            {
                Id = id,
                Date = DateTime.UtcNow,
                Path = targetName,
                StorageHash = hash,
                StorageIV = aes.IV,
                StorageKey = aes.Key,
            };
        }

        public override async Task ProgressTask(WebProgressTask task)
        {
            var pars = task.Request.Location.GetParameter;
            if (!pars.TryGetValue("person", out string? person))
                person = null;
            if (!pars.TryGetValue("parent", out string? parent))
                parent = null;
            if (!pars.TryGetValue("name", out string? name))
                name = null;
            if (!pars.TryGetValue("mime", out string? mime))
                mime = MimeType.ApplicationOctetStream;
            if (!pars.TryGetValue("token", out string? token))
                token = "";

            var con = await WebSocketConnection.FindFromSessionId(token).ConfigureAwait(false);
            if (con is null)
            {
                CloseWithError(task, "invalid token");
                return;
            }

            var personId = person?.ToObjectId();
            var personObj = personId is null ? null : await con.DB.GetPersonAsync(personId, CancellationToken.None);
            if (personObj is null)
            {
                CloseWithError(task, "invalid person");
                return;
            }

            var parentId = parent?.ToObjectId();
            var parentObj = parentId is null ? null : await con.DB.GetFileAsync(parentId, CancellationToken.None);
            if (parentObj is null && parent is not null)
            {
                CloseWithError(task, "invalid parent");
                return;
            }

            if (name is null)
            {
                CloseWithError(task, "invalid name");
                return;
            }

            Data.FileEntry? entry;

            if (mime != "folder")
            {
                if (task.Request.Post.DataAsync is null ||
                    await task.Request.Post.DataAsync.ConfigureAwait(false) is not UnknownPostData data
                )
                {
                    CloseWithError(task, "required upload missing");
                    return;
                }

                LiteDB.ObjectId entryId = LiteDB.ObjectId.NewObjectId();
                try
                {
                    entry = await PerformUpload(entryId, task, personObj.Value.Source.Config, data)
                        .ConfigureAwait(false);
                }
                catch (Exception e)
                {
                    Serilog.Log.Error(e, "Cannot perform upload");
                    var path = Path.Combine(personObj.Value.Source.Config.DirectoryPath,
                        "upload", entryId.ToString()
                    );
                    if (File.Exists(path))
                        File.Delete(path);
                    CloseWithError(task, "upload of data failed");
                    return;
                }
            }
            else
            {
                if (task.Request.Post.DataAsync is not null &&
                    await task.Request.Post.DataAsync.ConfigureAwait(false) is UnknownPostData data
                )
                {
                    await data.Data.DiscardAsync().ConfigureAwait(false);
                }

                entry = null;
            }

            var now = DateTime.UtcNow;
            var file = new Data.File
            {
                Created = now,
                Modified = now,
                Mime = mime,
                Name = name,
                Person = personObj.Value.Value,
                ParentId = parentObj?.Value.Id,
                // inherit the confidential setting of the parent
                Confidential = parentObj?.Value.Confidential ?? false,
            };
            if (entry is not null)
                file.Entries.Add(entry);

            try { await personObj.Value.Source.AddFile(file, CancellationToken.None); }
            catch (Exception e)
            {
                Serilog.Log.Error(e, "Cannot write added file to db");
                task.Response.StatusCode = HttpStateCode.InternalServerError;
                return;
            }

            task.Document.DataSources.Add(new HttpStringDataSource(
                "{\"success\":true}"
            ){
                MimeType = MimeType.ApplicationJson
            });
            
            await WebSocketConnection.Foreach(
                async con =>
                {
                    await con.Send(new Events.Send.FileUpdated(
                        new DB.DBValue<Data.File>(personObj.Value.Source, file)
                    )).ConfigureAwait(false);
                    await con.Send(new Events.Send.SendNotification(new Notifications.Notification
                    {
                        Close = DateTime.UtcNow.AddSeconds(15),
                        Title = entry is null ? "Folder created" : "File uploaded",
                        Description = name,
                        Image = entry is null ? "/img/svgrepo/interaction-set/folder-149450.svg" :
                            "/img/svgrepo/interaction-set/file-5230.svg"
                    })).ConfigureAwait(false);
                }
            ).ConfigureAwait(false);
        }
    }
}