using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Authorization;
using Bit.Core.Models.Api;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Settings;
using Bit.Core.Models.Api.Response;
using Bit.Core.Enums;
using Bit.Core.Context;
using Microsoft.Azure.EventGrid.Models;
using Bit.Api.Utilities;
using System.Collections.Generic;
using Bit.Core.Models.Table;
using Newtonsoft.Json;
using Bit.Core.Models.Data;
using Microsoft.Extensions.Logging;

namespace Bit.Api.Controllers
{
    [Route("sends")]
    [Authorize("Application")]
    public class SendsController : Controller
    {
        private readonly ISendRepository _sendRepository;
        private readonly IUserService _userService;
        private readonly ISendService _sendService;
        private readonly ISendFileStorageService _sendFileStorageService;
        private readonly ILogger<SendsController> _logger;
        private readonly GlobalSettings _globalSettings;
        private readonly ICurrentContext _currentContext;

        public SendsController(
            ISendRepository sendRepository,
            IUserService userService,
            ISendService sendService,
            ISendFileStorageService sendFileStorageService,
            ILogger<SendsController> logger,
            GlobalSettings globalSettings,
            ICurrentContext currentContext)
        {
            _sendRepository = sendRepository;
            _userService = userService;
            _sendService = sendService;
            _sendFileStorageService = sendFileStorageService;
            _logger = logger;
            _globalSettings = globalSettings;
            _currentContext = currentContext;
        }

        [AllowAnonymous]
        [HttpPost("access/{id}")]
        public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestModel model)
        {
            // Uncomment whenever we want to require the `send-id` header
            //if (!_currentContext.HttpContext.Request.Headers.ContainsKey("Send-Id") ||
            //    _currentContext.HttpContext.Request.Headers["Send-Id"] != id)
            //{
            //    throw new BadRequestException("Invalid Send-Id header.");
            //}

            var guid = new Guid(CoreHelpers.Base64UrlDecode(id));
            var (send, passwordRequired, passwordInvalid) =
                await _sendService.AccessAsync(guid, model.Password);
            if (passwordRequired)
            {
                return new UnauthorizedResult();
            }
            if (passwordInvalid)
            {
                await Task.Delay(2000);
                throw new BadRequestException("Invalid password.");
            }
            if (send == null)
            {
                throw new NotFoundException();
            }

            var sendResponse = new SendAccessResponseModel(send, _globalSettings);
            if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault())
            {
                var creator = await _userService.GetUserByIdAsync(send.UserId.Value);
                sendResponse.CreatorIdentifier = creator.Email;
            }
            return new ObjectResult(sendResponse);
        }

        [AllowAnonymous]
        [HttpPost("{encodedSendId}/access/file/{fileId}")]
        public async Task<IActionResult> GetSendFileDownloadData(string encodedSendId,
            string fileId, [FromBody] SendAccessRequestModel model)
        {
            // Uncomment whenever we want to require the `send-id` header
            //if (!_currentContext.HttpContext.Request.Headers.ContainsKey("Send-Id") ||
            //    _currentContext.HttpContext.Request.Headers["Send-Id"] != encodedSendId)
            //{
            //    throw new BadRequestException("Invalid Send-Id header.");
            //}

            var sendId = new Guid(CoreHelpers.Base64UrlDecode(encodedSendId));
            var send = await _sendRepository.GetByIdAsync(sendId);

            if (send == null)
            {
                throw new BadRequestException("Could not locate send");
            }

            var (url, passwordRequired, passwordInvalid) = await _sendService.GetSendFileDownloadUrlAsync(send, fileId,
                model.Password);

            if (passwordRequired)
            {
                return new UnauthorizedResult();
            }
            if (passwordInvalid)
            {
                await Task.Delay(2000);
                throw new BadRequestException("Invalid password.");
            }
            if (send == null)
            {
                throw new NotFoundException();
            }

            return new ObjectResult(new SendFileDownloadDataResponseModel()
            {
                Id = fileId,
                Url = url,
            });
        }

        [HttpGet("{id}")]
        public async Task<SendResponseModel> Get(string id)
        {
            var userId = _userService.GetProperUserId(User).Value;
            var send = await _sendRepository.GetByIdAsync(new Guid(id));
            if (send == null || send.UserId != userId)
            {
                throw new NotFoundException();
            }

            return new SendResponseModel(send, _globalSettings);
        }

        [HttpGet("")]
        public async Task<ListResponseModel<SendResponseModel>> Get()
        {
            var userId = _userService.GetProperUserId(User).Value;
            var sends = await _sendRepository.GetManyByUserIdAsync(userId);
            var responses = sends.Select(s => new SendResponseModel(s, _globalSettings));
            return new ListResponseModel<SendResponseModel>(responses);
        }

        [HttpPost("")]
        public async Task<SendResponseModel> Post([FromBody] SendRequestModel model)
        {
            model.ValidateCreation();
            var userId = _userService.GetProperUserId(User).Value;
            var send = model.ToSend(userId, _sendService);
            await _sendService.SaveSendAsync(send);
            return new SendResponseModel(send, _globalSettings);
        }

        [HttpPost("file")]
        [RequestSizeLimit(105_906_176)]
        [DisableFormValueModelBinding]
        public async Task<SendResponseModel> PostFile()
        {
            if (!Request?.ContentType.Contains("multipart/") ?? true)
            {
                throw new BadRequestException("Invalid content.");
            }

            if (Request.ContentLength > 105906176) // 101 MB, give em' 1 extra MB for cushion
            {
                throw new BadRequestException("Max file size is 100 MB.");
            }

            Send send = null;
            await Request.GetSendFileAsync(async (stream, fileName, model) =>
            {
                model.ValidateCreation();
                var userId = _userService.GetProperUserId(User).Value;
                var (madeSend, madeData) = model.ToSend(userId, fileName, _sendService);
                send = madeSend;
                await _sendService.SaveFileSendAsync(send, madeData, model.FileLength.GetValueOrDefault(0));
                await _sendService.UploadFileToExistingSendAsync(stream, send);
            });

            return new SendResponseModel(send, _globalSettings);
        }


        [HttpPost("file/v2")]
        public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendRequestModel model)
        {
            if (model.Type != SendType.File)
            {
                throw new BadRequestException("Invalid content.");
            }

            if (!model.FileLength.HasValue)
            {
                throw new BadRequestException("Invalid content. File size hint is required.");
            }

            if (model.FileLength.Value > SendService.MAX_FILE_SIZE)
            {
                throw new BadRequestException($"Max file size is {SendService.MAX_FILE_SIZE_READABLE}.");
            }

            var userId = _userService.GetProperUserId(User).Value;
            var (send, data) = model.ToSend(userId, model.File.FileName, _sendService);
            var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value);
            return new SendFileUploadDataResponseModel
            {
                Url = uploadUrl,
                FileUploadType = _sendFileStorageService.FileUploadType,
                SendResponse = new SendResponseModel(send, _globalSettings)
            };
        }

        [HttpGet("{id}/file/{fileId}")]
        public async Task<SendFileUploadDataResponseModel> RenewFileUpload(string id, string fileId)
        {
            var userId = _userService.GetProperUserId(User).Value;
            var sendId = new Guid(id);
            var send = await _sendRepository.GetByIdAsync(sendId);
            var fileData = JsonConvert.DeserializeObject<SendFileData>(send?.Data);

            if (send == null || send.Type != SendType.File || (send.UserId.HasValue && send.UserId.Value != userId) ||
                !send.UserId.HasValue || fileData.Id != fileId || fileData.Validated)
            {
                // Not found if Send isn't found, user doesn't have access, request is faulty,
                // or we've already validated the file. This last is to emulate create-only blob permissions for Azure
                throw new NotFoundException();
            }

            return new SendFileUploadDataResponseModel
            {
                Url = await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId),
                FileUploadType = _sendFileStorageService.FileUploadType,
                SendResponse = new SendResponseModel(send, _globalSettings),
            };
        }

        [HttpPost("{id}/file/{fileId}")]
        [DisableFormValueModelBinding]
        public async Task PostFileForExistingSend(string id, string fileId)
        {
            if (!Request?.ContentType.Contains("multipart/") ?? true)
            {
                throw new BadRequestException("Invalid content.");
            }

            if (Request.ContentLength > 105906176 && !_globalSettings.SelfHosted) // 101 MB, give em' 1 extra MB for cushion
            {
                throw new BadRequestException("Max file size for direct upload is 100 MB.");
            }

            var send = await _sendRepository.GetByIdAsync(new Guid(id));
            await Request.GetFileAsync(async (stream) =>
            {
                await _sendService.UploadFileToExistingSendAsync(stream, send);
            });
        }

        [AllowAnonymous]
        [HttpPost("file/validate/azure")]
        public async Task<ObjectResult> AzureValidateFile()
        {
            return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
            {
                {
                    "Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
                    {
                        try
                        {
                            var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
                            var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
                            var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
                            if (send == null)
                            {
                                if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService)
                                {
                                    await azureSendFileStorageService.DeleteBlobAsync(blobName);
                                }
                                return;
                            }
                            await _sendService.ValidateSendFile(send);
                        }
                        catch (Exception e)
                        {
                            _logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonConvert.SerializeObject(eventGridEvent)}");
                            return;
                        }
                    }
                }
            });
        }

        [HttpPut("{id}")]
        public async Task<SendResponseModel> Put(string id, [FromBody] SendRequestModel model)
        {
            model.ValidateEdit();
            var userId = _userService.GetProperUserId(User).Value;
            var send = await _sendRepository.GetByIdAsync(new Guid(id));
            if (send == null || send.UserId != userId)
            {
                throw new NotFoundException();
            }

            await _sendService.SaveSendAsync(model.ToSend(send, _sendService));
            return new SendResponseModel(send, _globalSettings);
        }

        [HttpPut("{id}/remove-password")]
        public async Task<SendResponseModel> PutRemovePassword(string id)
        {
            var userId = _userService.GetProperUserId(User).Value;
            var send = await _sendRepository.GetByIdAsync(new Guid(id));
            if (send == null || send.UserId != userId)
            {
                throw new NotFoundException();
            }

            send.Password = null;
            await _sendService.SaveSendAsync(send);
            return new SendResponseModel(send, _globalSettings);
        }

        [HttpDelete("{id}")]
        public async Task Delete(string id)
        {
            var userId = _userService.GetProperUserId(User).Value;
            var send = await _sendRepository.GetByIdAsync(new Guid(id));
            if (send == null || send.UserId != userId)
            {
                throw new NotFoundException();
            }

            await _sendService.DeleteSendAsync(send);
        }
    }
}