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.IO;
using System.ComponentModel.DataAnnotations;
using Sockeye.Models;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Util;
using Sockeye.Biz;
using System.Linq;
using System.Collections.Generic;

namespace Sockeye.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}/attachment")]
    [Produces("application/json")]
    public class AttachmentController : ControllerBase
    {
        private readonly AyContext ct;
        private readonly ILogger<AttachmentController> log;
        private readonly ApiServerState serverState;

        /// <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;
        }



        public class UpdateAttachmentInfo
        {
            [Required]
            public uint Concurrency { get; set; }
            [Required]
            public string DisplayFileName { get; set; }
            public string Notes { get; set; }

        }

        /// <summary>
        /// Update FileAttachment
        /// (FileName and notes only)
        /// </summary>
        /// <param name="id"></param>
        /// <param name="inObj"></param>
        /// <returns>list</returns>
        [Authorize]
        [HttpPut("{id}")]
        public async Task<IActionResult> PutAttachment([FromRoute] long id, [FromBody] UpdateAttachmentInfo inObj)
        {
            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));

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

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

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

            if (!Authorized.HasModifyRole(HttpContext.Items, dbObject.AttachToAType))
            {
                return StatusCode(403, new ApiNotAuthorizedResponse());
            }


            try
            {
                string ChangeTextra = string.Empty;
                if (dbObject.DisplayFileName != inObj.DisplayFileName)
                {
                    ChangeTextra = $"\"{dbObject.DisplayFileName}\" => \"{inObj.DisplayFileName}\"";
                }
                if (dbObject.Notes != inObj.Notes)
                {
                    if (!string.IsNullOrWhiteSpace(ChangeTextra))
                        ChangeTextra += ", ";
                    ChangeTextra += "Notes";
                }
                dbObject.DisplayFileName = inObj.DisplayFileName;
                dbObject.Notes = inObj.Notes;



                //Set "original" value of concurrency token to input token
                //this will allow EF to check it out
                ct.Entry(dbObject).OriginalValues["Concurrency"] = inObj.Concurrency;

                //Log event and save context             
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.AttachToObjectId, dbObject.AttachToAType, SockEvent.AttachmentModified, ChangeTextra), ct);



                //SEARCH INDEXING                     
                var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationIdFromContext.Id(HttpContext.Items), id, SockType.FileAttachment);
                SearchParams.AddText(inObj.Notes).AddText(inObj.DisplayFileName);
                await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);

                //--------------

            }
            catch (DbUpdateConcurrencyException)
            {
                return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
            }

            //Normallyh wouldn't return a whole list but in this case the UI demands it because of reactivity issues
            var ret = await GetFileListForObjectAsync(dbObject.AttachToAType, dbObject.AttachToObjectId);
            return Ok(ApiOkResponse.Response(ret));
        }


        /// <summary>
        /// Get attachments for object type and id specified
        /// 
        /// Required Role: Read full object properties rights to object type specified
        /// 
        /// </summary>        
        /// <returns>file attachment list for object</returns>
        [Authorize]
        [HttpGet("list")]
        public async Task<IActionResult> GetList([FromQuery] SockType sockType, [FromQuery] long sockId)
        {
            if (serverState.IsClosed)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));

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

            //Is this a customer user attempting to view a wo attachments??
           // var userType = UserTypeFromContext.Type(HttpContext.Items);

            if (!Authorized.HasReadFullRole(HttpContext.Items, sockType))
            {
                return StatusCode(403, new ApiNotAuthorizedResponse());
            }


            var ret = await GetFileListForObjectAsync(sockType, sockId);
            return Ok(ApiOkResponse.Response(ret));
        }


        /// <summary>
        /// Get parent object type and id 
        /// for specified attachment id
        /// 
        /// </summary>        
        /// <returns></returns>
        [Authorize]
        [HttpGet("parent/{id}")]
        public async Task<IActionResult> GetParent([FromRoute] long id)
        {
            if (serverState.IsClosed)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));

            if (!Authorized.HasReadFullRole(HttpContext.Items, SockType.FileAttachment))
                return StatusCode(403, new ApiNotAuthorizedResponse());


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

            var at = await ct.FileAttachment.AsNoTracking().Where(z => z.Id == id).FirstOrDefaultAsync();
            if (at == null)
                return NotFound();


            return Ok(ApiOkResponse.Response(new { id = at.AttachToObjectId, type = at.AttachToAType }));
        }




        /// <summary>
        /// Upload attachment file
        /// Max 10GiB total
        /// Requires same Authorization roles as object that file is being attached to
        /// 
        /// </summary>
        /// <returns>NameValue list of filenames and attachment id's</returns>
        [Authorize]
        [HttpPost]
        [DisableFormValueModelBinding]
        [RequestSizeLimit(ServerBootConfig.MAX_ATTACHMENT_UPLOAD_BYTES)]
        public async Task<IActionResult> UploadAsync()
        {
            //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(serverState.ApiErrorCode, null, serverState.Reason));

            //  var returnList = new List<NameIdItem>();
            object ret = null;
            SockTypeId attachToObject = null;
            try
            {
                if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
                    return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, $"Expected a multipart request, but got {Request.ContentType}"));



                bool badRequest = false;
                string AttachToAType = string.Empty;
                string AttachToObjectId = string.Empty;
                string errorMessage = string.Empty;
                string Notes = string.Empty;
                long? OverrideUserId = null;
                List<UploadFileData> FileData = new List<UploadFileData>();

                var uploadFormData = await ApiUploadProcessor.ProcessUploadAsync(HttpContext);
                if (!string.IsNullOrWhiteSpace(uploadFormData.Error))
                {
                    badRequest = true;
                    errorMessage = uploadFormData.Error;
                }

                if (!badRequest
                    && (!uploadFormData.FormFieldData.ContainsKey("FileData")
                    || !uploadFormData.FormFieldData.ContainsKey("AttachToAType")
                    || !uploadFormData.FormFieldData.ContainsKey("AttachToObjectId")))
                {
                    badRequest = true;
                    errorMessage = "Missing one or more required FormFieldData values: AttachToAType, AttachToObjectId, FileData";
                }
                if (!badRequest)
                {
                    AttachToAType = uploadFormData.FormFieldData["AttachToAType"].ToString();
                    //for v8 migrate purposes
                    if (uploadFormData.FormFieldData.ContainsKey("OverrideUserId"))
                        OverrideUserId = long.Parse(uploadFormData.FormFieldData["OverrideUserId"].ToString());

                    AttachToObjectId = uploadFormData.FormFieldData["AttachToObjectId"].ToString();
                    if (uploadFormData.FormFieldData.ContainsKey("Notes"))
                        Notes = uploadFormData.FormFieldData["Notes"].ToString();
                    //fileData in JSON stringify format which contains the actual last modified dates etc
                    //"[{\"name\":\"Client.csv\",\"lastModified\":1582822079618},{\"name\":\"wmi4fu06nrs41.jpg\",\"lastModified\":1586900220990}]"
                    FileData = Newtonsoft.Json.JsonConvert.DeserializeObject<List<UploadFileData>>(uploadFormData.FormFieldData["FileData"].ToString());

                    if (string.IsNullOrWhiteSpace(AttachToAType) || string.IsNullOrWhiteSpace(AttachToObjectId))
                    {
                        badRequest = true;
                        errorMessage = "AttachToAType and / or AttachToObjectId are empty and are required";
                    }
                }


                //Get type and id object from post paramters

                if (!badRequest)
                {
                    attachToObject = new SockTypeId(AttachToAType, AttachToObjectId);
                    if (attachToObject.IsEmpty)
                    {
                        badRequest = true;
                        errorMessage = "AttachToAType and / or AttachToObjectId are not valid and are required";
                    }
                }

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


                //does attach to object exist?
                if (!badRequest)
                {
                    //check if object exists                                    
                    if (!await BizObjectExistsInDatabase.ExistsAsync(attachToObject.SockType, attachToObject.ObjectId, ct))
                    {
                        badRequest = true;
                        errorMessage = "Invalid attach object";
                    }
                    else
                    {
                        // User needs modify rights to the object type in question
                        if (!Authorized.HasModifyRole(HttpContext.Items, attachToObject.SockType))
                        {
                            //delete temp files
                            ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
                            return StatusCode(403, new ApiNotAuthorizedResponse());
                        }

                    }
                }



                if (badRequest)
                {
                    //delete temp files
                    ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
                    //file too large is most likely issue so in that case return this localized properly
                    if (errorMessage.Contains("413"))
                    {
                        var TransId = UserTranslationIdFromContext.Id(HttpContext.Items);
                        return BadRequest(new ApiErrorResponse(
                            ApiErrorCode.VALIDATION_LENGTH_EXCEEDED,
                            null,
                            "HTTP ERROR CODE 413 " + String.Format(await TranslationBiz.GetTranslationStaticAsync("AyaFileFileTooLarge", TransId, ct), FileUtil.GetBytesReadable(ServerBootConfig.MAX_ATTACHMENT_UPLOAD_BYTES))));
                    }
                    else//not too big, something else                    
                        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)
                    {
                        //Get the actual date from the separate filedata
                        //this is because the lastModified date is always empty in the form data files
                        DateTime theDate = DateTime.MinValue;
                        foreach (UploadFileData f in FileData)
                        {
                            if (f.name == a.OriginalFileName)
                            {
                                if (f.lastModified > 0)
                                {
                                    theDate = DateTimeOffset.FromUnixTimeMilliseconds(f.lastModified).UtcDateTime;
                                }
                            }
                        }
                        if (theDate == DateTime.MinValue)
                            theDate = DateTime.UtcNow;

                        var v = await FileUtil.StoreFileAttachmentAsync(a.InitialUploadedPathName, a.MimeType, a.OriginalFileName, theDate, attachToObject, Notes, OverrideUserId ?? UserId, ct);

                        //EVENT LOG
                        await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, attachToObject.ObjectId, attachToObject.SockType, SockEvent.AttachmentCreate, v.DisplayFileName), ct);

                        //SEARCH INDEXING                     
                        var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationIdFromContext.Id(HttpContext.Items), v.Id, SockType.FileAttachment);
                        SearchParams.AddText(v.Notes).AddText(v.DisplayFileName);
                        await Search.ProcessNewObjectKeywordsAsync(SearchParams);

                    }
                }
                ret = await GetFileListForObjectAsync(attachToObject.SockType, attachToObject.ObjectId);
            }
            catch (InvalidDataException ex)
            {
                return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message));
            }

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

        // /// <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>
        [Authorize]
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteAttachmentAsync([FromRoute] long id)
        {
            if (!serverState.IsOpen)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));

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

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

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

            if (!Authorized.HasDeleteRole(HttpContext.Items, dbObject.AttachToAType))
            {
                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
            await FileUtil.DeleteFileAttachmentAsync(dbObject, ct);

            //Event log process delete
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.AttachToObjectId, dbObject.AttachToAType, SockEvent.AttachmentDelete, dbObject.DisplayFileName), ct);

            //Delete search index
            await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, SockType.FileAttachment, ct);

            return NoContent();
        }

        /// <summary>
        /// Batch delete attachments
        /// 
        /// </summary>        
        /// <returns>No content</returns>
        [HttpPost("batch-delete")]
        [Authorize]
        public async Task<IActionResult> PostBatchDelete([FromBody] List<long> idList)
        {
            if (serverState.IsClosed)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
            if (!ModelState.IsValid)
                return BadRequest(new ApiErrorResponse(ModelState));
            if (!Authorized.HasModifyRole(HttpContext.Items, SockType.FileAttachment))
                return StatusCode(403, new ApiNotAuthorizedResponse());

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

            foreach (long id in idList)
            {
                var dbObject = await ct.FileAttachment.FirstOrDefaultAsync(z => z.Id == id);
                if (dbObject == null)
                    continue;
                //do the delete
                //this handles removing the file if there are no refs left and also the db record for the attachment
                await FileUtil.DeleteFileAttachmentAsync(dbObject, ct);

                //Event log process delete
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.AttachToObjectId, dbObject.AttachToAType, SockEvent.AttachmentDelete, dbObject.DisplayFileName), ct);

                //Delete search index
                await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, SockType.FileAttachment, ct);
            }
            return NoContent();
        }
        /// <summary>
        /// Batch move attachments
        /// 
        /// </summary>        
        /// <returns>No content</returns>
        [HttpPost("batch-move")]
        [Authorize]
        public async Task<IActionResult> PostBatchMove([FromBody] dtoBatchMove dt)
        {
            if (serverState.IsClosed)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
            if (!ModelState.IsValid)
                return BadRequest(new ApiErrorResponse(ModelState));
            if (!Authorized.HasModifyRole(HttpContext.Items, SockType.FileAttachment))
                return StatusCode(403, new ApiNotAuthorizedResponse());
            if (!await BizObjectExistsInDatabase.ExistsAsync(dt.ToType, dt.ToId, ct))
                return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, null, "LT:ErrorAPI2010"));

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

            foreach (long id in dt.IdList)
            {
                var dbObject = await ct.FileAttachment.FirstOrDefaultAsync(z => z.Id == id);
                if (dbObject == null)
                    continue;

                //do the move  
                var msg = $"{dbObject.DisplayFileName} moved from {dbObject.AttachToAType}-{dbObject.AttachToObjectId} to {dt.ToType}-{dt.ToId} ";
                dbObject.AttachToObjectId = dt.ToId;
                dbObject.AttachToAType = dt.ToType;
                await ct.SaveChangesAsync();

                //Event log process move
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.AttachToObjectId, dbObject.AttachToAType, SockEvent.AttachmentModified, msg), ct);

            }
            return NoContent();
        }

        public class dtoBatchMove
        {
            public List<long> IdList { get; set; }
            public SockType ToType { get; set; }
            public long ToId { get; set; }
        }

        /// <summary>
        /// Download a file attachment
        /// </summary>
        /// <param name="id"></param>
        /// <param name="t">download token</param>
        /// <returns></returns>
        [HttpGet("download/{id}")]
        public async Task<IActionResult> DownloadAsync([FromRoute] long id, [FromQuery] string t)
        {
            //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(serverState.ApiErrorCode, null, serverState.Reason));


            var DownloadUser = await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct);
            if (DownloadUser == null)
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
                return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
            }


            //Ok, user has a valid download key and it's not expired yet so get the attachment record 
            var dbObject = await ct.FileAttachment.SingleOrDefaultAsync(z => z.Id == id);
            if (dbObject == null)
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection
                return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
            }

            //is this allowed?

            if (!Authorized.HasReadFullRole(DownloadUser.Roles, dbObject.AttachToAType))
            {
                await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
                return StatusCode(403, new ApiNotAuthorizedResponse());
            }


            //they are allowed, let's send the file
            string mimetype = dbObject.ContentType;
            var filePath = FileUtil.GetPermanentAttachmentFilePath(dbObject.StoredFileName);
            if (!System.IO.File.Exists(filePath))
            {

                //TODO: this should reset the validity

                var errText = $"Physical file {dbObject.StoredFileName} not found despite attachment record, this file is missing";
                log.LogError(errText);
                await NotifyEventHelper.AddOpsProblemEvent($"File attachment issue: {errText}");

                return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, null, errText));
            }

            //Log
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(DownloadUser.Id, dbObject.AttachToObjectId, dbObject.AttachToAType, SockEvent.AttachmentDownload, dbObject.DisplayFileName), ct);

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

        }

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


        async private Task<object> GetFileListForObjectAsync(SockType sockType, long sockId)
        {
            var retList = new List<FileAttachmentListItem>();
            using (var cmd = ct.Database.GetDbConnection().CreateCommand())
            {
                await ct.Database.OpenConnectionAsync();
                cmd.CommandText = $@"select afileattachment.id, afileattachment.xmin as concurrency, displayfilename,contenttype,lastmodified, afileattachment.notes, size, auser.name as attachedbyuser from afileattachment 
                                        left join auser on (afileattachment.attachedByUserId=auser.id)
                                        where attachtoatype={(int)sockType} and attachtoobjectid={sockId}
                                        order by displayfilename";

                using (var dr = await cmd.ExecuteReaderAsync())
                {
                    while (dr.Read())
                    {
                        retList.Add(new FileAttachmentListItem()
                        {
                            Id = dr.GetInt64(0),
                            Concurrency = (UInt32)dr.GetValue(1),
                            DisplayFileName = dr.GetString(2),
                            ContentType = dr.GetString(3),
                            LastModified = dr.GetDateTime(4),
                            Notes = dr.GetString(5),
                            Size = dr.GetInt64(6),
                            AttachedByUser = dr.GetString(7)
                        });
                    }
                }
            }

            return retList;
        }

        private class FileAttachmentListItem
        {
            public long Id { get; set; }
            public uint Concurrency { get; set; }
            public string DisplayFileName { get; set; }
            public string ContentType { get; set; }//mime type               
            public DateTime LastModified { get; set; }
            public string Notes { get; set; }
            public string AttachedByUser { get; set; }
            public long Size { get; set; }
        }

        /// <summary>
        /// Trigger immediate AttachmentMaintenanceJob
        /// 
        /// </summary>        
        /// <returns>Job Id</returns>
        [HttpPost("maintenance")]
        [Authorize]
        public async Task<IActionResult> PostTriggerAttachmentMaintenanceJob()
        {
            if (serverState.IsClosed)
                return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
            if (!Authorized.HasModifyRole(HttpContext.Items, SockType.FileAttachment))
                return StatusCode(403, new ApiNotAuthorizedResponse());
            var JobName = $"Attachment maintenance (demand) LT:User {UserNameFromContext.Name(HttpContext.Items)}";
            OpsJob j = new OpsJob();
            j.Name = JobName;
            j.SockType = SockType.FileAttachment;
            j.JobType = JobType.AttachmentMaintenance;
            j.SubType = JobSubType.NotSet;
            j.Exclusive = true;
            await JobsBiz.AddJobAsync(j);
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserIdFromContext.Id(HttpContext.Items), 0, SockType.ServerJob, SockEvent.Created, JobName), ct);
            return Accepted(new { JobId = j.GId });
        }


    }//eoc
}//eons


