using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System.Collections.Generic;
using System.Linq;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Util;
using AyaNova.Biz;


namespace AyaNova.Api.Controllers
{

    //FROM DOCS HERE:
    //https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
    //https://github.com/aspnet/Docs/tree/74a44669d5e7039e2d4d2cb3f8b0c4ed742d1124/aspnetcore/mvc/models/file-uploads/sample/FileUploadSample


    /// <summary>
    /// Attachment controller
    /// </summary>
    [ApiController]
    [ApiVersion("8.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [Produces("application/json")]
    [Authorize]
    public class AttachmentController : ControllerBase
    {
        private readonly AyContext ct;
        private readonly ILogger<AttachmentController> log;
        private readonly ApiServerState serverState;
        //private static readonly FormOptions _defaultFormOptions = new FormOptions();


        /// <summary>
        /// 
        /// </summary>
        /// <param name="dbcontext"></param>
        /// <param name="logger"></param>
        /// <param name="apiServerState"></param>
        public AttachmentController(AyContext dbcontext, ILogger<AttachmentController> logger, ApiServerState apiServerState)
        {
            ct = dbcontext;
            log = logger;
            serverState = apiServerState;
        }





        //LOOKAT: Centralize this code somewhere else, it's going to be needed for backup as well
        //consider the 1 hour thing, is this legit depending on client?

        /// <summary>
        /// Get download token
        /// A download token is good for 1 hour from issue
        /// </summary>
        /// <returns>Current download token for user</returns>
        [HttpGet("DownloadToken")]
        public async Task<IActionResult> GetDownloadToken()
        {
            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));

            long lUserId = UserIdFromContext.Id(HttpContext.Items);
            var u = await ct.User.FirstOrDefaultAsync(a => a.Id == lUserId);
            if (u == null)
                return NotFound();
            else
            {

                //Generate a download token and store it with the user account
                //users who are authenticated can get their token via download route
                Guid g = Guid.NewGuid();
                string dlkey = Convert.ToBase64String(g.ToByteArray());
                dlkey = dlkey.Replace("=", "");
                dlkey = dlkey.Replace("+", "");

                //get expiry date for download token             
                var exp = new DateTimeOffset(DateTime.Now.AddHours(1).ToUniversalTime(), TimeSpan.Zero);

                u.DlKey = dlkey;
                u.DlKeyExpire = exp.DateTime;
                ct.User.Update(u);
                try
                {
                    await ct.SaveChangesAsync();//triggering concurrency exception here                          
                }
                catch (Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException)
                {
                    log.LogInformation("Auth retry dlkey");
                };

                return Ok(ApiOkResponse.Response(new { dlkey = u.DlKey, expires = u.DlKeyExpire }, true));
            }

        }



        /// <summary>
        /// Upload attachment file
        /// 
        /// Required roles: Same roles as object that file is being attached to
        /// 
        /// </summary>
        /// <returns>NameValue list of filenames and attachment id's</returns>
        [HttpPost]
        [DisableFormValueModelBinding]
        [RequestSizeLimit(10737418241)]//10737418240 = 10gb  https://github.com/aspnet/Announcements/issues/267
        public async Task<IActionResult> Upload()
        {
            //Adapted from the example found here: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming

            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));

            var returnList = new List<NameIdItem>();

            try
            {
                if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
                    return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", $"Expected a multipart request, but got {Request.ContentType}"));

                var uploadFormData = await ApiUploadProcessor.ProcessAttachmentUpload(HttpContext);

                bool badRequest = false;
                string AttachToObjectType = string.Empty;
                string AttachToObjectId = string.Empty;
                string errorMessage = string.Empty;

                if (!uploadFormData.FormFieldData.ContainsKey("AttachToObjectType") || !uploadFormData.FormFieldData.ContainsKey("AttachToObjectId"))
                {
                    badRequest = true;
                    errorMessage = "AttachToObjectType and / or AttachToObjectId are missing and are required";
                }
                if (!badRequest)
                {
                    AttachToObjectType = uploadFormData.FormFieldData["AttachToObjectType"].ToString();
                    AttachToObjectId = uploadFormData.FormFieldData["AttachToObjectId"].ToString();
                    if (string.IsNullOrWhiteSpace(AttachToObjectType) || string.IsNullOrWhiteSpace(AttachToObjectId))
                    {
                        badRequest = true;
                        errorMessage = "AttachToObjectType and / or AttachToObjectId are empty and are required";
                    }
                }


                //Get type and id object from post paramters
                AyaTypeId attachToObject = null;
                if (!badRequest)
                {
                    attachToObject = new AyaTypeId(AttachToObjectType, AttachToObjectId);
                    if (attachToObject.IsEmpty)
                    {
                        badRequest = true;
                        errorMessage = "AttachToObjectType and / or AttachToObjectId are not valid and are required";
                    }
                }

                //Is it an attachable type of object?
                if (!badRequest)
                {
                    if (!attachToObject.IsAttachable)
                    {
                        badRequest = true;
                        errorMessage = attachToObject.ObjectType.ToString() + " - AttachToObjectType  does not support attachments";
                    }
                }


                //does attach to object exist?
                if (!badRequest)
                {
                    //check if object exists
                    //Updated code: this used to check if the o wnerId was -1 to see if it didn't exist, but since ow nerId zapped this seems like the next best way to do it
                    //Not sure at all what the ow nerid check was doing before that verified it's existance, the code is long gone now and I can't be arsed to look it up in the repo history                   
                    //If the tests pass then it's fine :)                   
                    if (!BizObjectExistsInDatabase.Exists(attachToObject))
                    {
                        badRequest = true;
                        errorMessage = "Invalid attach object";
                    }
                    else
                    {
                        // User needs modify rights to the object type in question
                        if (!Authorized.HasModifyRole(HttpContext.Items, attachToObject.ObjectType))
                        {
                            //delete temp files
                            DeleteTempFileUploadDueToBadRequest(uploadFormData);
                            return StatusCode(403, new ApiNotAuthorizedResponse());
                        }

                    }
                }



                if (badRequest)
                {
                    //delete temp files
                    DeleteTempFileUploadDueToBadRequest(uploadFormData);
                    //return bad request
                    return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, errorMessage));
                }

                long UserId = UserIdFromContext.Id(HttpContext.Items);

                //We have our files and a confirmed AyObject, ready to attach and save permanently
                if (uploadFormData.UploadedFiles.Count > 0)
                {
                    foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
                    {
                        var v = FileUtil.storeFileAttachment(a.InitialUploadedPathName, a.MimeType, a.OriginalFileName, attachToObject, ct);
                        returnList.Add(new NameIdItem()
                        {
                            Name = v.DisplayFileName,
                            Id = v.Id
                        });

                        //EVENT LOG
                        EventLogProcessor.LogEventToDatabase(new Event(UserId, attachToObject.ObjectId, attachToObject.ObjectType, AyaEvent.AttachmentCreate, v.DisplayFileName), ct);

                        //SEARCH INDEXING
                        // Search.ProcessNewObjectKeywords( UserLocaleIdFromContext.Id(HttpContext.Items), v.Id, AyaType.FileAttachment, v.DisplayFileName, v.DisplayFileName, v.Notes, v.StoredFileName);
                        var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleIdFromContext.Id(HttpContext.Items), v.Id, AyaType.FileAttachment, v.DisplayFileName);
                        SearchParams.AddText(v.Notes).AddText(v.DisplayFileName).AddText(v.StoredFileName);
                        Search.ProcessNewObjectKeywords(SearchParams);

                    }
                }
            }
            catch (InvalidDataException ex)
            {
                return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", ex.Message));
            }

            //Return the list of attachment ids and filenames
            return Ok(ApiOkResponse.Response(returnList, false));
        }

        /// <summary>
        /// Utility to delete files that were uploaded but couldn't be stored for some reason, called by Attach route
        /// </summary>
        /// <param name="uploadFormData"></param>
        private static void DeleteTempFileUploadDueToBadRequest(ApiUploadProcessor.ApiUploadedFilesResult uploadFormData)
        {
            if (uploadFormData.UploadedFiles.Count > 0)
            {
                foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
                {
                    System.IO.File.Delete(a.InitialUploadedPathName);
                }
            }
        }


        /// <summary>
        /// Delete Attachment
        /// </summary>
        /// <param name="id"></param>
        /// <returns>Ok</returns>
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteAttachment([FromRoute] long id)
        {

            if (!serverState.IsOpen)
            {
                return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(new ApiErrorResponse(ModelState));
            }

            var dbObj = await ct.FileAttachment.SingleOrDefaultAsync(m => m.Id == id);
            if (dbObj == null)
            {
                return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
            }

            long UserId = UserIdFromContext.Id(HttpContext.Items);

            if (!Authorized.HasDeleteRole(HttpContext.Items, dbObj.AttachToObjectType))
            {
                return StatusCode(403, new ApiNotAuthorizedResponse());
            }

            //do the delete
            //this handles removing the file if there are no refs left and also the db record for the attachment
            FileUtil.deleteFileAttachment(dbObj, ct);

            //Event log process delete
            EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.AttachToObjectId, dbObj.AttachToObjectType, AyaEvent.AttachmentDelete, dbObj.DisplayFileName), ct);

            //Delete search index
            Search.ProcessDeletedObjectKeywords(dbObj.Id, AyaType.FileAttachment);

            return NoContent();
        }




        /// <summary>
        /// Download a file attachment
        /// </summary>
        /// <param name="id"></param>
        /// <param name="dlkey"></param>
        /// <returns></returns>
        [HttpGet("download/{id}")]
        public async Task<IActionResult> Download([FromRoute] long id, [FromQuery] string dlkey)
        {
            //copied from Rockfish
            //https://dotnetcoretutorials.com/2017/03/12/uploading-files-asp-net-core/
            //https://stackoverflow.com/questions/45763149/asp-net-core-jwt-in-uri-query-parameter/45811270#45811270


            if (!serverState.IsOpen)
            {
                return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
            }


            if (string.IsNullOrWhiteSpace(dlkey))
            {
                return NotFound();
            }

            //get user by key, if not found then reject
            //If user dlkeyexp has not expired then return file
            var dlkeyUser = await ct.User.SingleOrDefaultAsync(m => m.DlKey == dlkey);
            if (dlkeyUser == null)
            {
                return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid"));
            }

            //Make sure the token provided is for the current user
            long UserId = UserIdFromContext.Id(HttpContext.Items);
            if (UserId != dlkeyUser.Id)
            {
                return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid"));
            }


            var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
            if (dlkeyUser.DlKeyExpire < utcNow.DateTime)
            {
                return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token has expired"));
            }

            //Ok, user has a valid download key and it's not expired yet so get the attachment record 
            var dbObj = await ct.FileAttachment.SingleOrDefaultAsync(m => m.Id == id);
            if (dbObj == null)
            {
                return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
            }

            //is this allowed?
            if (!Authorized.HasReadFullRole(HttpContext.Items, dbObj.AttachToObjectType))
            {
                return StatusCode(403, new ApiNotAuthorizedResponse());
            }

            //they are allowed, let's send the file
            string mimetype = dbObj.ContentType;
            var filePath = FileUtil.GetPermanentAttachmentFilePath(dbObj.StoredFileName);
            if (!System.IO.File.Exists(filePath))
            {
                //TODO: this should trigger some kind of notification to the ops people
                //and a red light on the dashboard
                return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, null, $"Physical file {dbObj.StoredFileName} not found despite attachment record, this file is missing"));
            }

            //Log
            EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.AttachToObjectId, dbObj.AttachToObjectType, AyaEvent.AttachmentDownload, dbObj.DisplayFileName), ct);

            return PhysicalFile(filePath, mimetype, dbObj.DisplayFileName);

        }

        ////////////////////////////////////////////////////////////////////////////////////



    }//eoc
}//eons


#region sample html form to work with this
/* 
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<title></title>
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script type="text/javascript">
$(document).ready(function () {
$("#upload").click(function (evt) {
var fileUpload = $("#files").get(0);
var files = fileUpload.files;
var data = new FormData();
for (var i = 0; i < files.length; i++) {
    data.append(files[i].name, files[i]);
}

//attachment test
data.append('AttachToObjectType','2');//object 2 is widget
data.append('AttachToObjectId','200');//there should normally always be a widget with id 1

$.ajax({
    type: "POST",
    url: "http://localhost:7575/api/v8.0/Attachment",
    headers: {
        Authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNTIxNTY5ODE5IiwiZXhwIjoiMTUyNDE2MTgxOSIsImlzcyI6IkF5YU5vdmEiLCJpZCI6IjEifQ.4PzkzZNYK5mJkbfCHGQ2N2248atAvvcDgApoz65oIC0"
    },
    contentType: false,
    processData: false,
    data: data,
    success: function (message) {
        alert("upload successful!");
        console.log(message);
    },
    error: function (error) {
        console.log(error);
        alert("There was an error uploading files!");
    }
});
});
});
</script>
</head>

<body>
<form method="post" enctype="multipart/form-data">
<input type="file" id="files" name="files" multiple />
<!-- <input type="file" accept=".zip,application/zip" id="files" name="files" multiple /> -->
<input type="button" id="upload" value="Upload file(s)" />




</form>

</body>

</html>

 */
#endregion