using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
using Bit.Core.Models.View;
using Newtonsoft.Json;

namespace Bit.Core.Services
{
    public class SendService : ISendService
    {
        private List<SendView> _decryptedSendsCache;
        private readonly ICryptoService _cryptoService;
        private readonly IStateService _stateService;
        private readonly IApiService _apiService;
        private readonly II18nService _i18nService;
        private readonly ICryptoFunctionService _cryptoFunctionService;
        private Task<List<SendView>> _getAllDecryptedTask;
        private readonly IFileUploadService _fileUploadService;

        public SendService(
            ICryptoService cryptoService,
            IStateService stateService,
            IApiService apiService,
            IFileUploadService fileUploadService,
            II18nService i18nService,
            ICryptoFunctionService cryptoFunctionService)
        {
            _cryptoService = cryptoService;
            _stateService = stateService;
            _apiService = apiService;
            _fileUploadService = fileUploadService;
            _i18nService = i18nService;
            _cryptoFunctionService = cryptoFunctionService;
        }

        public async Task ClearAsync(string userId)
        {
            await _stateService.SetEncryptedSendsAsync(null, userId);
            ClearCache();
        }

        public void ClearCache() => _decryptedSendsCache = null;

        public async Task DeleteAsync(params string[] ids)
        {
            var sends = await _stateService.GetEncryptedSendsAsync();

            if (sends == null)
            {
                return;
            }

            foreach (var id in ids)
            {
                sends.Remove(id);
            }

            await _stateService.SetEncryptedSendsAsync(sends);
            ClearCache();
        }

        public async Task DeleteWithServerAsync(string id)
        {
            await _apiService.DeleteSendAsync(id);
            await DeleteAsync(id);
        }

        public async Task<(Send send, EncByteArray encryptedFileData)> EncryptAsync(SendView model, byte[] fileData,
            string password, SymmetricCryptoKey key = null)
        {
            if (model.Key == null)
            {
                model.Key = _cryptoFunctionService.RandomBytes(16);
                model.CryptoKey = await _cryptoService.MakeSendKeyAsync(model.Key);
            }

            var send = new Send
            {
                Id = model.Id,
                Type = model.Type,
                Disabled = model.Disabled,
                DeletionDate = model.DeletionDate,
                ExpirationDate = model.ExpirationDate,
                MaxAccessCount = model.MaxAccessCount,
                Key = await _cryptoService.EncryptAsync(model.Key, key),
                Name = await _cryptoService.EncryptAsync(model.Name, model.CryptoKey),
                Notes = await _cryptoService.EncryptAsync(model.Notes, model.CryptoKey),
                HideEmail = model.HideEmail
            };
            EncByteArray encryptedFileData = null;

            if (password != null)
            {
                var passwordHash = await _cryptoFunctionService.Pbkdf2Async(password, model.Key,
                    CryptoHashAlgorithm.Sha256, 100000);
                send.Password = Convert.ToBase64String(passwordHash);
            }

            switch (send.Type)
            {
                case SendType.Text:
                    send.Text = new SendText
                    {
                        Text = await _cryptoService.EncryptAsync(model.Text.Text, model.CryptoKey),
                        Hidden = model.Text.Hidden
                    };
                    break;
                case SendType.File:
                    send.File = new SendFile();
                    if (fileData != null)
                    {
                        send.File.FileName = await _cryptoService.EncryptAsync(model.File.FileName, model.CryptoKey);
                        encryptedFileData = await _cryptoService.EncryptToBytesAsync(fileData, model.CryptoKey);
                    }
                    break;
                default:
                    break;
            }

            return (send, encryptedFileData);
        }

        public async Task<List<Send>> GetAllAsync()
        {
            var sends = await _stateService.GetEncryptedSendsAsync();
            return sends?.Select(kvp => new Send(kvp.Value)).ToList() ?? new List<Send>();
        }

        public async Task<List<SendView>> GetAllDecryptedAsync()
        {
            if (_decryptedSendsCache != null)
            {
                return _decryptedSendsCache;
            }

            var hasKey = await _cryptoService.HasKeyAsync();
            if (!hasKey)
            {
                throw new Exception("No Key.");
            }

            if (_getAllDecryptedTask != null && !_getAllDecryptedTask.IsCompleted && !_getAllDecryptedTask.IsFaulted)
            {
                return await _getAllDecryptedTask;
            }

            async Task<List<SendView>> doTask()
            {
                var decSends = new List<SendView>();

                async Task decryptAndAddSendAsync(Send send) => decSends.Add(await send.DecryptAsync());
                await Task.WhenAll((await GetAllAsync()).Select(s => decryptAndAddSendAsync(s)));

                decSends = decSends.OrderBy(s => s, new SendLocaleComparer(_i18nService)).ToList();
                _decryptedSendsCache = decSends;
                return _decryptedSendsCache;
            }

            _getAllDecryptedTask = doTask();
            return await _getAllDecryptedTask;
        }

        public async Task<Send> GetAsync(string id)
        {
            var sends = await _stateService.GetEncryptedSendsAsync();

            if (sends == null || !sends.ContainsKey(id))
            {
                return null;
            }

            return new Send(sends[id]);
        }

        public async Task ReplaceAsync(Dictionary<string, SendData> sends)
        {
            await _stateService.SetEncryptedSendsAsync(sends);
            _decryptedSendsCache = null;
        }

        public async Task<string> SaveWithServerAsync(Send send, EncByteArray encryptedFileData)
        {
            var request = new SendRequest(send, encryptedFileData?.Buffer?.LongLength);
            SendResponse response = default;
            if (send.Id == null)
            {
                switch (send.Type)
                {
                    case SendType.Text:
                        response = await _apiService.PostSendAsync(request);
                        break;
                    case SendType.File:
                        try
                        {
                            var uploadDataResponse = await _apiService.PostFileTypeSendAsync(request);
                            response = uploadDataResponse.SendResponse;

                            await _fileUploadService.UploadSendFileAsync(uploadDataResponse, send.File.FileName, encryptedFileData);
                        }
                        catch (ApiException e) when (e.Error.StatusCode == HttpStatusCode.NotFound)
                        {
                            response = await LegacyServerSendFileUpload(request, send, encryptedFileData);
                        }
                        catch
                        {
                            if (response != default)
                            {
                                await _apiService.DeleteSendAsync(response.Id);
                            }
                            throw;
                        }
                        break;
                    default:
                        throw new NotImplementedException($"Cannot save unknown Send type {send.Type}");
                }
                send.Id = response.Id;
            }
            else
            {
                response = await _apiService.PutSendAsync(send.Id, request);
            }

            var userId = await _stateService.GetActiveUserIdAsync();
            await UpsertAsync(new SendData(response, userId));
            return response.Id;
        }

        [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")]
        private async Task<SendResponse> LegacyServerSendFileUpload(SendRequest request, Send send, EncByteArray encryptedFileData)
        {
            var fd = new MultipartFormDataContent($"--BWMobileFormBoundary{DateTime.UtcNow.Ticks}")
                        {
                            { new StringContent(JsonConvert.SerializeObject(request)), "model" },
                            { new ByteArrayContent(encryptedFileData.Buffer), "data", send.File.FileName.EncryptedString }
                        };

            return await _apiService.PostSendFileAsync(fd);
        }

        public async Task UpsertAsync(params SendData[] sends)
        {
            var knownSends = await _stateService.GetEncryptedSendsAsync() ??
                new Dictionary<string, SendData>();

            foreach (var send in sends)
            {
                knownSends[send.Id] = send;
            }

            await _stateService.SetEncryptedSendsAsync(knownSends);
            _decryptedSendsCache = null;
        }

        public async Task RemovePasswordWithServerAsync(string id)
        {
            var response = await _apiService.PutSendRemovePasswordAsync(id);
            var userId = await _stateService.GetActiveUserIdAsync();
            await UpsertAsync(new SendData(response, userId));
        }

        private class SendLocaleComparer : IComparer<SendView>
        {
            private readonly II18nService _i18nService;

            public SendLocaleComparer(II18nService i18nService)
            {
                _i18nService = i18nService;
            }

            public int Compare(SendView a, SendView b)
            {
                var aName = a?.Name;
                var bName = b?.Name;
                if (aName == null && bName != null)
                {
                    return -1;
                }
                if (aName != null && bName == null)
                {
                    return 1;
                }
                if (aName == null && bName == null)
                {
                    return 0;
                }
                return _i18nService.StringComparer.Compare(aName, bName);
            }
        }
    }
}
