using System.Data;
using System.Net;
using System.Net.Mail;
using System.Text;

using MesaFabApproval.API.Utilities;
using MesaFabApproval.Models;
using MesaFabApproval.Shared.Models;
using MesaFabApproval.Shared.Utilities;

using Microsoft.Extensions.Caching.Memory;

namespace MesaFabApproval.API.Services;

public interface IMRBService {
    Task<IEnumerable<MRB>> GetAllMRBs(bool bypassCache);
    Task<bool> MRBNumberIsValid(int number);
    Task<MRB> GetMRBById(int id, bool bypassCache = false);
    Task<MRB> GetMRBByTitle(string title, bool bypassCache);
    Task CreateNewMRB(MRB mrb);
    Task UpdateMRB(MRB mrb);
    Task CreateMRBAction(MRBAction mrbAction);
    Task<IEnumerable<MRBAction>> GetMRBActionsForMRB(int mrbNumber, bool bypassCache);
    Task UpdateMRBAction(MRBAction mrbAction);
    Task DeleteMRBAction(int mrbActionID, int mrbNumber);
    Task<IEnumerable<UploadResult>> UploadAttachments(IEnumerable<IFormFile> files, int mrbNumber);
    Task<IEnumerable<UploadResult>> UploadActionAttachments(IEnumerable<IFormFile> files, int actionId);
    Task<IEnumerable<MRBAttachment>> GetAllAttachmentsForMRB(int mrbNumber, bool bypassCache);
    Task<IEnumerable<MRBActionAttachment>> GetAllActionAttachmentsForMRB(int mrbNumber, bool bypassCache);
    Task DeleteAttachment(MRBAttachment attachment);
    Task NotifyNewApprovals(MRB mrb);
    Task NotifyApprovers(MRBNotification notification);
    Task NotifyOriginator(MRBNotification notification);
    Task NotifyQAPreApprover(MRBNotification notification);
    Task DeleteMRB(int mrbNumber);
    Task ConvertActionsToCsvFile(int mrbNumber, string path);
}

public class MRBService : IMRBService {
    private readonly ILogger<MRBService> _logger;
    private readonly IDalService _dalService;
    private readonly IMemoryCache _cache;
    private readonly IUserService _userService;
    private readonly IApprovalService _approvalService;
    private readonly ISmtpService _smtpService;

    private readonly string _siteBaseUrl;
    private readonly string _mrbAttachmentPath;

    public MRBService(ILogger<MRBService> logger,
                      IDalService dalService,
                      IMemoryCache cache,
                      IUserService userService,
                      IApprovalService approvalService,
                      ISmtpService smtpService,
                      AppSettings appSettings) {
        _logger = logger ?? throw new ArgumentNullException("ILogger not injected");
        _dalService = dalService ?? throw new ArgumentNullException("IDalService not injected");
        _cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
        _userService = userService ?? throw new ArgumentNullException("IUserService not injected");
        _approvalService = approvalService ?? throw new ArgumentNullException("IApprovalService not injected");
        _smtpService = smtpService ?? throw new ArgumentNullException("ISmtpService not injected");
        _siteBaseUrl = appSettings.SiteBaseUrl;
        _mrbAttachmentPath = appSettings.MrbAttachmentPath;
    }
    public async Task CreateNewMRB(MRB mrb) {
        try {
            _logger.LogInformation("Attempting to generate new MRB");

            if (mrb is null) throw new ArgumentNullException("MRB cannot be null");

            StringBuilder queryBuilder = new();
            queryBuilder.Append("insert into MRB (OriginatorID, Title, SubmittedDate, ");
            queryBuilder.Append("CloseDate, CancelDate, NumberOfLotsAffected, ApprovalDate, ");
            queryBuilder.Append("IssueDescription, CustomerImpacted, Department, Process, Val, RMANo, ");
            queryBuilder.Append("PCRBNo, SpecsImpacted, TrainingRequired, Status, StageNo, ");
            queryBuilder.Append("CustomerImpactedName, ProcessECNNumber, Tool, Category) values (");
            queryBuilder.Append($"{mrb.OriginatorID}, '{mrb.Title}', ");
            if (mrb.SubmittedDate < DateTimeUtilities.MIN_DT)
                mrb.SubmittedDate = DateTimeUtilities.MIN_DT;
            queryBuilder.Append($"'{mrb.SubmittedDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            if (mrb.CloseDate > DateTimeUtilities.MAX_DT)
                mrb.CloseDate = DateTimeUtilities.MAX_DT;
            queryBuilder.Append($"'{mrb.CloseDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            if (mrb.CancelDate > DateTimeUtilities.MAX_DT)
                mrb.CancelDate = DateTimeUtilities.MAX_DT;
            queryBuilder.Append($"'{mrb.CancelDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            queryBuilder.Append($"{mrb.NumberOfLotsAffected}, ");
            if (mrb.ApprovalDate > DateTimeUtilities.MAX_DT)
                mrb.ApprovalDate = DateTimeUtilities.MAX_DT;
            queryBuilder.Append($"'{mrb.ApprovalDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            queryBuilder.Append($"'{mrb.IssueDescription}', {Convert.ToUInt32(mrb.CustomerImpacted)}, ");
            queryBuilder.Append($"'{mrb.Department}', '{mrb.Process}', {mrb.Val}, {mrb.RMANo}, '{mrb.PCRBNo}', ");
            queryBuilder.Append($"{Convert.ToInt32(mrb.SpecsImpacted)}, {Convert.ToInt32(mrb.TrainingRequired)}, ");
            queryBuilder.Append($"'{mrb.Status}', {mrb.StageNo}, '{mrb.CustomerImpactedName}', ");
            queryBuilder.Append($"{mrb.ProcessECNNumber}, '{mrb.Tool}', '{mrb.Category}');");

            int rowsCreated = await _dalService.ExecuteAsync(queryBuilder.ToString());

            if (rowsCreated <= 0) throw new Exception("Unable to create new MRB");

            mrb = await GetMRBByTitle(mrb.Title, true);

            _cache.Set($"mrb{mrb.MRBNumber}", mrb, DateTimeOffset.Now.AddHours(1));
            _cache.Set($"mrb{mrb.Title}", mrb, DateTimeOffset.Now.AddHours(1));

            IEnumerable<MRB>? allMrbs = _cache.Get<IEnumerable<MRB>>("allMrbs");
            if (allMrbs is not null) {
                List<MRB> mrbList = allMrbs.ToList();
                mrbList.Add(mrb);
                _cache.Set("allMrbs", mrbList, DateTimeOffset.Now.AddHours(1));
            }
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to create new MRB. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task<IEnumerable<MRB>> GetAllMRBs(bool bypassCache) {
        try {
            _logger.LogInformation("Attempting to get all MRBs");

            IEnumerable<MRB>? allMrbs = null;
            if (!bypassCache) allMrbs = _cache.Get<IEnumerable<MRB>>("allMrbs");

            if (allMrbs is null) {
                StringBuilder queryBuilder = new();
                queryBuilder.Append("select (u.FirstName + ' ' + u.LastName) as OriginatorName, m.* ");
                queryBuilder.Append("from MRB m join Users u on m.OriginatorID = u.UserID;");

                allMrbs = (await _dalService.QueryAsync<MRB>(queryBuilder.ToString())).ToList();

                _cache.Set("allMrbs", allMrbs, DateTimeOffset.Now.AddHours(1));
            }

            return allMrbs;
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to get all MRBs. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task<bool> MRBNumberIsValid(int number) {
        try {
            _logger.LogInformation($"Attempting to determine if {number} is a valid MRB#");

            if (number <= 0) return false;

            IEnumerable<int> mrbNumbers = _cache.Get<IEnumerable<int>>("mrbNumbers") ??
                                            new HashSet<int>();

            if (mrbNumbers.Contains(number)) return true;

            string sql = $"select count(MRBNumber) as count from MRB where MRBNumber={number}";

            int rowsReturned = (await _dalService.QueryAsync<int>(sql)).FirstOrDefault();

            if (rowsReturned > 0) {
                mrbNumbers.Append(number);
                _cache.Set("mrbNumbers", mrbNumbers);
                return true;
            }

            return false;
        } catch (Exception ex) {
            _logger.LogError($"Unable to determine if {number} is a valid MRB#, because {ex.Message}");
            throw;
        }
    }

    public async Task<MRB> GetMRBById(int id, bool bypassCache = false) {
        try {
            _logger.LogInformation("Attempting to get an MRB by ID");

            if (id < 0) throw new ArgumentException("Invalid MRB number");

            MRB? mrb = null;
            if (!bypassCache)
                mrb = _cache.Get<MRB>($"mrb{id}");

            if (mrb is null) {
                StringBuilder queryBuilder = new();
                queryBuilder.Append("select (u.FirstName + ' ' + u.LastName) as OriginatorName, m.* ");
                queryBuilder.Append("from MRB m join Users u on m.OriginatorID = u.UserID ");
                queryBuilder.Append($"where m.MRBNumber = {id}");

                mrb = (await _dalService.QueryAsync<MRB>(queryBuilder.ToString())).FirstOrDefault();

                _cache.Set($"mrb{id}", mrb, DateTimeOffset.Now.AddHours(1));
            }

            if (mrb is null) throw new Exception($"Unable to get MRB {id}");

            return mrb;
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to get an MRB. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task<MRB> GetMRBByTitle(string title, bool bypassCache) {
        try {
            _logger.LogInformation("Attempting to get an MRB by title");

            if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Title cannot be null or empty");

            MRB? mrb = null;

            if (!bypassCache) mrb = _cache.Get<MRB>($"mrb{title}");

            if (mrb is null) {
                StringBuilder queryBuilder = new();
                queryBuilder.Append("select (u.FirstName + ' ' + u.LastName) as OriginatorName, m.* ");
                queryBuilder.Append("from MRB m join Users u on m.OriginatorID = u.UserID ");
                queryBuilder.Append($"where m.Title = '{title}'");

                mrb = (await _dalService.QueryAsync<MRB>(queryBuilder.ToString())).FirstOrDefault();

                _cache.Set($"mrb{title}", mrb, DateTimeOffset.Now.AddHours(1));
            }

            if (mrb is null) throw new Exception($"Unable to get MRB {title}");

            return mrb;
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to get an MRB. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task UpdateMRB(MRB mrb) {
        try {
            _logger.LogInformation("Attempting to update an MRB");

            if (mrb is null) throw new ArgumentNullException("MRB cannot be null");

            StringBuilder queryBuilder = new();
            queryBuilder.Append($"update MRB set OriginatorID = {mrb.OriginatorID}, ");
            queryBuilder.Append($"Title = '{mrb.Title.Replace("'", "''")}', ");
            if (mrb.SubmittedDate < DateTimeUtilities.MIN_DT)
                mrb.SubmittedDate = DateTimeUtilities.MIN_DT;
            queryBuilder.Append($"SubmittedDate = '{mrb.SubmittedDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            if (mrb.CloseDate > DateTimeUtilities.MAX_DT)
                mrb.CloseDate = DateTimeUtilities.MAX_DT;
            queryBuilder.Append($"CloseDate = '{mrb.CloseDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            if (mrb.CancelDate > DateTimeUtilities.MAX_DT)
                mrb.CancelDate = DateTimeUtilities.MAX_DT;
            queryBuilder.Append($"CancelDate = '{mrb.CancelDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            queryBuilder.Append($"NumberOfLotsAffected = {mrb.NumberOfLotsAffected}, ");
            if (mrb.ApprovalDate > DateTimeUtilities.MAX_DT)
                mrb.ApprovalDate = DateTimeUtilities.MAX_DT;
            queryBuilder.Append($"ApprovalDate = '{mrb.ApprovalDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            queryBuilder.Append($"IssueDescription = '{mrb.IssueDescription.Replace("'", "''")}', ");
            queryBuilder.Append($"CustomerImpacted = {Convert.ToInt32(mrb.CustomerImpacted)}, ");
            queryBuilder.Append($"Department = '{mrb.Department.Replace("'", "''")}', ");
            queryBuilder.Append($"Process = '{mrb.Process.Replace("'", "''")}', ");
            queryBuilder.Append($"Val = {mrb.Val}, ");
            queryBuilder.Append($"RMANo = {mrb.RMANo}, ");
            queryBuilder.Append($"PCRBNo = '{mrb.PCRBNo}', ");
            queryBuilder.Append($"SpecsImpacted = {Convert.ToInt32(mrb.SpecsImpacted)}, ");
            queryBuilder.Append($"TrainingRequired = {Convert.ToInt32(mrb.TrainingRequired)}, ");
            queryBuilder.Append($"Status = '{mrb.Status}', StageNo = {mrb.StageNo}, ");
            queryBuilder.Append($"CustomerImpactedName = '{mrb.CustomerImpactedName.Replace("'", "''")}', ");
            queryBuilder.Append($"ProcessECNNumber = '{mrb.ProcessECNNumber}', ");
            queryBuilder.Append($"Tool = '{mrb.Tool.Replace("'", "''")}', Category = '{mrb.Category.Replace("'", "''")}' ");
            queryBuilder.Append($"where MRBNumber = {mrb.MRBNumber};");

            int rowsAffected = await _dalService.ExecuteAsync(queryBuilder.ToString());

            if (rowsAffected <= 0) throw new Exception($"Unable to update MRB {mrb.MRBNumber}");

            _cache.Set($"mrb{mrb.MRBNumber}", mrb, DateTimeOffset.Now.AddHours(1));
            _cache.Set($"mrb{mrb.Title}", mrb, DateTimeOffset.Now.AddHours(1));

            IEnumerable<MRB>? allMrbs = _cache.Get<IEnumerable<MRB>>("allMrbs");
            if (allMrbs is not null) {
                List<MRB> mrbList = allMrbs.ToList();
                mrbList.RemoveAll(m => m.MRBNumber == mrb.MRBNumber);
                mrbList.Add(mrb);
                _cache.Set("allMrbs", mrbList, DateTimeOffset.Now.AddHours(1));
            }
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to update an MRB. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task CreateMRBAction(MRBAction mrbAction) {
        try {
            _logger.LogInformation("Attempting to generate new MRB action");

            if (mrbAction is null) throw new ArgumentNullException("MRB action cannot be null");

            StringBuilder queryBuilder = new();
            queryBuilder.Append("insert into MRBAction (Action, Customer, Quantity, PartNumber, LotNumber, MRBNumber, ");
            queryBuilder.Append("ConvertFrom, ConvertTo, Justification) ");
            queryBuilder.Append($"values ('{mrbAction.Action}', '{mrbAction.Customer}', {mrbAction.Quantity}, ");
            queryBuilder.Append($"'{mrbAction.PartNumber}', '{mrbAction.LotNumber}', {mrbAction.MRBNumber}, ");
            queryBuilder.Append($"'{mrbAction.ConvertFrom}', '{mrbAction.ConvertTo}', '{mrbAction.Justification}');");

            int rowsAffected = await _dalService.ExecuteAsync(queryBuilder.ToString());

            if (rowsAffected <= 0) throw new Exception("Unable to create MRB action in database");

        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to create new MRB action. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task<IEnumerable<MRBAction>> GetMRBActionsForMRB(int mrbNumber, bool bypassCache) {
        try {
            _logger.LogInformation($"Attempting to get MRB actions for MRB {mrbNumber}");

            if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRB number");

            IEnumerable<MRBAction>? mrbActions = null;

            if (!bypassCache)
                _cache.Get<IEnumerable<MRBAction>>($"mrbActions{mrbNumber}");

            if (mrbActions is null) {
                string sql = $"select * from MRBAction where MRBNumber = {mrbNumber}";

                mrbActions = (await _dalService.QueryAsync<MRBAction>(sql)).ToList();

                if (mrbActions is not null) {
                    foreach (MRBAction action in mrbActions) {
                        if (action.CompletedDate < DateTime.MaxValue && action.CompletedByUserID > 0) {
                            action.CompletedByUser = await _userService.GetUserByUserId(action.CompletedByUserID);
                        }
                    }

                    _cache.Set($"mrbActions{mrbNumber}", mrbActions, DateTimeOffset.Now.AddMinutes(30));
                }
            }

            if (mrbActions is null) throw new Exception($"Unable to find MRB actions for MRB {mrbNumber}");

            return mrbActions;
        } catch (Exception ex) {
            _logger.LogError($"Unable to get MRB actions for MRB {mrbNumber}, because {ex.Message}");
            throw;
        }
    }

    public async Task UpdateMRBAction(MRBAction mrbAction) {
        try {
            _logger.LogInformation("Attempting to update MRB action");

            if (mrbAction is null) throw new ArgumentNullException("MRB action cannot be null");

            StringBuilder queryBuilder = new();
            queryBuilder.Append($"update MRBAction set Action = '{mrbAction.Action}', ");
            queryBuilder.Append($"Customer = '{mrbAction.Customer.Replace("'", "''")}', ");
            queryBuilder.Append($"Quantity = {mrbAction.Quantity}, ");
            queryBuilder.Append($"PartNumber = '{mrbAction.PartNumber.Replace("'", "''")}', ");
            queryBuilder.Append($"LotNumber = '{mrbAction.LotNumber.Replace("'", "''")}', ");
            if (mrbAction.AssignedDate < DateTimeUtilities.MIN_DT)
                mrbAction.AssignedDate = DateTimeUtilities.MIN_DT;
            queryBuilder.Append($"AssignedDate= '{mrbAction.AssignedDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            if (mrbAction.CompletedDate > DateTimeUtilities.MAX_DT)
                mrbAction.CompletedDate = DateTimeUtilities.MAX_DT;
            queryBuilder.Append($"CompletedDate= '{mrbAction.CompletedDate.ToString("yyyy-MM-dd HH:mm:ss")}', ");
            queryBuilder.Append($"CompletedByUserID={mrbAction.CompletedByUserID}, ");
            queryBuilder.Append($"ConvertFrom='{mrbAction.ConvertFrom.Replace("'", "''")}', ");
            queryBuilder.Append($"ConvertTo='{mrbAction.ConvertTo.Replace("'", "''")}', ");
            queryBuilder.Append($"Justification='{mrbAction.Justification.Replace("'", "''")}' ");
            queryBuilder.Append($"where ActionID={mrbAction.ActionID};");

            int rowsAffected = await _dalService.ExecuteAsync(queryBuilder.ToString());

            if (rowsAffected <= 0) throw new Exception($"There were no MRB actions found with MRB# {mrbAction.MRBNumber}");

            List<MRBAction>? mrbActions = _cache.Get<IEnumerable<MRBAction>>($"mrbActions{mrbAction.MRBNumber}")?.ToList();
            if (mrbActions is not null) {
                mrbActions.RemoveAll(m => m.ActionID == mrbAction.ActionID);
                mrbActions.Add(mrbAction);
                _cache.Set($"mrbActions{mrbAction.MRBNumber}", mrbActions, DateTimeOffset.Now.AddMinutes(30));
            }
        } catch (Exception ex) {
            _logger.LogError($"Unable to update MRB action, because {ex.Message}");
            throw;
        }
    }

    public async Task DeleteMRBAction(int mrbActionID, int mrbNumber) {
        try {
            _logger.LogInformation($"Attempting to delete MRB action {mrbActionID}");

            if (mrbActionID <= 0) throw new ArgumentException($"{mrbActionID} is not a valid MRBActionID");
            if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRBNumber");

            string sql = $"delete from MRBAction where ActionID = {mrbActionID};";

            int rowsAffected = await _dalService.ExecuteAsync(sql);

            if (rowsAffected <= 0) throw new Exception($"No MRB action was found to delete for ActionID {mrbActionID}");

            List<MRBAction>? mrbActions = _cache.Get<IEnumerable<MRBAction>>($"mrbActions{mrbNumber}")?.ToList();
            if (mrbActions is not null) {
                mrbActions.RemoveAll(m => m.ActionID == mrbActionID);
                _cache.Set($"mrbActions{mrbNumber}", mrbActions, DateTimeOffset.Now.AddMinutes(30));
            }
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to delete MRB action {mrbActionID}. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task<IEnumerable<UploadResult>> UploadAttachments(IEnumerable<IFormFile> files, int mrbNumber) {
        try {
            _logger.LogInformation("Attempting to upload attachments");

            List<UploadResult> uploadResults = new();

            if (files is null) throw new ArgumentNullException("Files cannot be null");
            if (files.Count() <= 0) throw new ArgumentException("Files cannot be empty");
            if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRB number");

            foreach (IFormFile file in files) {
                try {
                    if (file is null) throw new ArgumentException("File cannot be null");
                    if (file.Length <= 0) throw new ArgumentException("File size cannot be zero");

                    string encodedName = WebUtility.HtmlEncode(file.FileName);
                    string path = $"{_mrbAttachmentPath}\\{mrbNumber}\\{encodedName}";

                    await FileUtilities.SaveFileToFileSystem(file, path);
                    await SaveAttachmentInDb(file, path, mrbNumber);

                    UploadResult uploadResult = new() {
                        UploadSuccessful = true,
                        FileName = file.FileName
                    };
                    uploadResults.Add(uploadResult);
                } catch (Exception ex) {
                    UploadResult uploadResult = new() {
                        UploadSuccessful = false,
                        FileName = file.FileName,
                        Error = ex.Message
                    };
                    uploadResults.Add(uploadResult);
                }
            }

            return uploadResults;
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to upload attachment. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task<IEnumerable<UploadResult>> UploadActionAttachments(IEnumerable<IFormFile> files, int actionId) {
        try {
            _logger.LogInformation("Attempting to upload action attachments");

            List<UploadResult> uploadResults = new();

            if (files is null) throw new ArgumentNullException("Files cannot be null");
            if (files.Count() <= 0) throw new ArgumentException("Files cannot be empty");
            if (actionId <= 0) throw new ArgumentException($"{actionId} is not a valid MRB action ID");

            List<Task> taskList = new();
            foreach (IFormFile file in files) {
                try {
                    if (file is null) throw new ArgumentException("File cannot be null");
                    if (file.Length <= 0) throw new ArgumentException("File size cannot be zero");

                    string encodedName = WebUtility.HtmlEncode(file.FileName);
                    string path = $"{_mrbAttachmentPath}\\{actionId}\\{encodedName}";

                    taskList.Add(FileUtilities.SaveFileToFileSystem(file, path));
                    taskList.Add(SaveActionAttachmentInDb(file, path, actionId));

                    UploadResult uploadResult = new() {
                        UploadSuccessful = true,
                        FileName = file.Name
                    };
                    uploadResults.Add(uploadResult);
                } catch (Exception ex) {
                    UploadResult uploadResult = new() {
                        UploadSuccessful = false,
                        FileName = file.Name,
                        Error = ex.Message
                    };
                    uploadResults.Add(uploadResult);
                }
            }

            Task.WaitAll(taskList.ToArray());

            return uploadResults;
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to upload action attachment. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task<IEnumerable<MRBAttachment>> GetAllAttachmentsForMRB(int mrbNumber, bool bypassCache) {
        try {
            _logger.LogInformation($"Attempting to get all attachments for MRB {mrbNumber}");

            if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRB number");

            IEnumerable<MRBAttachment>? attachments = null;
            if (!bypassCache)
                _cache.Get<IEnumerable<MRBAttachment>>($"mrbAttachments{mrbNumber}");

            if (attachments is null) {
                string sql = $"select * from MRBAttachment where MRBNumber = {mrbNumber};";

                attachments = (await _dalService.QueryAsync<MRBAttachment>(sql)).ToList();

                _cache.Set($"mrbAttachments{mrbNumber}", attachments, DateTimeOffset.Now.AddMinutes(15));
            }

            return attachments;
        } catch (Exception ex) {
            StringBuilder errMsgBuilder = new();
            errMsgBuilder.Append($"An error occurred when attempting to get all attachments for MRB {mrbNumber}. ");
            errMsgBuilder.Append($"Exception: {ex.Message}");
            _logger.LogError(errMsgBuilder.ToString());
            throw;
        }
    }

    public async Task<IEnumerable<MRBActionAttachment>> GetAllActionAttachmentsForMRB(int mrbNumber, bool bypassCache) {
        try {
            _logger.LogInformation($"Attempting to get all action attachments for MRB {mrbNumber}");

            if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRB#");

            List<MRBActionAttachment>? attachments = null;
            if (!bypassCache)
                attachments = _cache.Get<List<MRBActionAttachment>>($"mrbActionAttachments{mrbNumber}");

            if (attachments is null) {
                attachments = new List<MRBActionAttachment>();

                foreach (MRBAction action in await GetMRBActionsForMRB(mrbNumber, false)) {
                    string sql = $"select * from MRBActionAttachment where ActionID = {action.ActionID};";

                    IEnumerable<MRBActionAttachment> newAttachments =
                        (await _dalService.QueryAsync<MRBActionAttachment>(sql)).ToList();

                    attachments.AddRange(newAttachments);
                }

                _cache.Set($"mrbActionAttachments{mrbNumber}", attachments, DateTimeOffset.Now.AddMinutes(15));
            }

            return attachments;
        } catch (Exception ex) {
            StringBuilder errMsgBuilder = new();
            errMsgBuilder.Append($"An error occurred when attempting to get all attachments for MRB action {mrbNumber}. ");
            errMsgBuilder.Append($"Exception: {ex.Message}");
            _logger.LogError(errMsgBuilder.ToString());
            throw;
        }
    }

    public async Task DeleteAttachment(MRBAttachment attachment) {
        try {
            _logger.LogInformation("Attempting to delete an attachment");

            if (attachment is null) throw new ArgumentNullException("Attachment cannot be null");
            if (!File.Exists(attachment.Path)) throw new FileNotFoundException("No file found at provided path");

            File.Delete(attachment.Path);

            string sql = $"delete from MRBAttachment where AttachmentID = {attachment.AttachmentID};";

            int rowsDeleted = await _dalService.ExecuteAsync(sql);

            if (rowsDeleted <= 0)
                throw new Exception($"No attachments found in the database with attachment ID {attachment.AttachmentID}");
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to delete an attachment. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task NotifyNewApprovals(MRB mrb) {
        try {
            _logger.LogInformation("Attempting to notify approvers");

            if (mrb is null) throw new ArgumentNullException("MRB cannot be null");

            IEnumerable<Approval> approvals = await _approvalService.GetApprovalsForIssueId(mrb.MRBNumber, true);

            List<Approval> approvalsNeedingNotification = approvals.Where(a => a.NotifyDate <= DateTimeUtilities.MIN_DT).ToList();

            HashSet<string> emailsAlreadySent = new();
            foreach (Approval approval in approvalsNeedingNotification) {
                User user = await _userService.GetUserByUserId(approval.UserID);

                if (!emailsAlreadySent.Contains(user.Email)) {
                    emailsAlreadySent.Add(user.Email);

                    List<MailAddress> toAddresses = new();
                    toAddresses.Add(new MailAddress(user.Email));

                    List<MailAddress> ccAddresses = new();

                    string subject = $"[New Task] Mesa Fab Approval - MRB# {mrb.MRBNumber} - {mrb.Title}";

                    StringBuilder bodyBuilder = new();
                    bodyBuilder.Append($"MRB# {mrb.MRBNumber} [{mrb.Title}] is ready for your approval. ");
                    bodyBuilder.Append($"The assigned role is {approval.SubRoleCategoryItem}. <br /> <br />");
                    bodyBuilder.Append($"Click {_siteBaseUrl}/redirect?redirectPath=mrb/{approval.IssueID} to view the task.");

                    await _smtpService.SendEmail(toAddresses, ccAddresses, subject, bodyBuilder.ToString());

                    approval.NotifyDate = DateTime.Now;
                    await _approvalService.UpdateApproval(approval);
                }
            }
        } catch (Exception ex) {
            _logger.LogError($"Unable to notify approvers, because {ex.Message}");
            throw;
        }
    }

    public async Task NotifyApprovers(MRBNotification notification) {
        try {
            _logger.LogInformation("Attempting to send notification to approvers");

            if (notification is null) throw new ArgumentNullException("notification cannot be null");
            if (notification.MRB is null) throw new ArgumentNullException("MRB cannot be null");
            if (string.IsNullOrWhiteSpace(notification.Message)) throw new ArgumentException("message cannot be null or empty");

            IEnumerable<Approval> approvals = await _approvalService.GetApprovalsForIssueId(notification.MRB.MRBNumber, true);

            HashSet<string> emailsAlreadySent = new();
            foreach (Approval approval in approvals) {
                User user = await _userService.GetUserByUserId(approval.UserID);

                if (!emailsAlreadySent.Contains(user.Email)) {
                    emailsAlreadySent.Add(user.Email);

                    List<MailAddress> toAddresses = new();
                    toAddresses.Add(new MailAddress(user.Email));

                    List<MailAddress> ccAddresses = new();

                    string subject = $"[Update] Mesa Fab Approval - MRB# {notification.MRB.MRBNumber} - {notification.MRB.Title}";

                    StringBuilder bodyBuilder = new();
                    bodyBuilder.Append($"{notification.Message} <br /> <br />");
                    bodyBuilder.Append($"Click {_siteBaseUrl}/redirect?redirectPath=mrb/{approval.IssueID} to view the MRB.");

                    await _smtpService.SendEmail(toAddresses, ccAddresses, subject, bodyBuilder.ToString());

                    approval.NotifyDate = DateTime.Now;
                    await _approvalService.UpdateApproval(approval);
                }
            }
        } catch (Exception ex) {
            _logger.LogError($"Unable to send notification to originator, because {ex.Message}");
            throw;
        }
    }

    public async Task NotifyOriginator(MRBNotification notification) {
        try {
            _logger.LogInformation("Attempting to send notification to originator");

            if (notification is null) throw new ArgumentNullException("notification cannot be null");
            if (notification.MRB is null) throw new ArgumentNullException("MRB cannot be null");
            if (string.IsNullOrWhiteSpace(notification.Message)) throw new ArgumentException("message cannot be null or empty");

            User user = await _userService.GetUserByUserId(notification.MRB.OriginatorID);

            List<MailAddress> toAddresses = new();
            toAddresses.Add(new MailAddress(user.Email));

            List<MailAddress> ccAddresses = new();

            string subject = $"[Update] Mesa Fab Approval - MRB# {notification.MRB.MRBNumber} - {notification.MRB.Title}";

            StringBuilder bodyBuilder = new();
            bodyBuilder.Append($"{notification.Message} <br /> <br />");
            bodyBuilder.Append($"Click {_siteBaseUrl}/redirect?redirectPath=mrb/{notification.MRB.MRBNumber} to view the MRB.");

            await _smtpService.SendEmail(toAddresses, ccAddresses, subject, bodyBuilder.ToString());
        } catch (Exception ex) {
            _logger.LogError($"Unable to send notification to originator, because {ex.Message}");
            throw;
        }
    }

    public async Task NotifyQAPreApprover(MRBNotification notification) {
        try {
            _logger.LogInformation("Attempting to send notification to QA pre approver");

            if (notification is null) throw new ArgumentNullException("notification cannot be null");
            if (notification.MRB is null) throw new ArgumentNullException("MRB cannot be null");
            if (string.IsNullOrWhiteSpace(notification.Message)) throw new ArgumentException("message cannot be null or empty");

            string roleName = "QA_PRE_APPROVAL";

            int roleId = await _approvalService.GetRoleIdForRoleName(roleName);

            if (roleId <= 0) throw new Exception($"could not find {roleName} role ID");

            SubRole? subRole = (await _approvalService.GetSubRolesForSubRoleName(roleName, roleId)).FirstOrDefault();

            if (subRole is null)
                throw new Exception("Unable to find QA pre approver role");

            IEnumerable<User> members = await _approvalService.GetApprovalGroupMembers(subRole.SubRoleID);

            List<MailAddress> toAddresses = new();
            foreach (User member in members)
                toAddresses.Add(new MailAddress(member.Email));

            List<MailAddress> ccAddresses = new();

            string subject = $"[Update] Mesa Fab Approval - MRB# {notification.MRB.MRBNumber} - {notification.MRB.Title}";

            StringBuilder bodyBuilder = new();
            bodyBuilder.Append($"{notification.Message} <br /> <br />");
            bodyBuilder.Append($"Click {_siteBaseUrl}/redirect?redirectPath=mrb/{notification.MRB.MRBNumber} to view the MRB.");

            await _smtpService.SendEmail(toAddresses, ccAddresses, subject, bodyBuilder.ToString());
        } catch (Exception ex) {
            _logger.LogError($"Unable to send notification to QA pre approver, because {ex.Message}");
            throw;
        }
    }

    public async Task DeleteMRB(int mrbNumber) {
        try {
            _logger.LogInformation($"Attempting to delete MRB# {mrbNumber}");

            if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRB number");

            string sql = $"delete from MRB where MRBNumber={mrbNumber}";

            int rowsAffected = await _dalService.ExecuteAsync(sql);

            if (rowsAffected <= 0) throw new Exception($"MRB {mrbNumber} not deleted from database");

            IEnumerable<MRB>? allMrbs = _cache.Get<IEnumerable<MRB>>("allMrbs");
            if (allMrbs is not null) {
                List<MRB> mrbList = allMrbs.ToList();
                mrbList.RemoveAll(m => m.MRBNumber == mrbNumber);
                _cache.Set("allMrbs", mrbList, DateTimeOffset.Now.AddHours(1));
            }
        } catch (Exception ex) {
            _logger.LogError($"Unable to delete MRB {mrbNumber}, because {ex.Message}");
            throw;
        }
    }

    public async Task ConvertActionsToCsvFile(int mrbNumber, string path) {
        try {
            _logger.LogInformation($"Attempting to convert MRB {mrbNumber} actions to a CSV file");

            if (!(await MRBNumberIsValid(mrbNumber))) throw new ArgumentException($"{mrbNumber} is not a valid ");
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path cannot be null or empty");

            if (File.Exists(path)) File.Delete(path);

            string? directoryPath = Path.GetDirectoryName(path);
            if (!string.IsNullOrWhiteSpace(directoryPath))
                Directory.CreateDirectory(directoryPath);

            IEnumerable<MRBAction> actions = await GetMRBActionsForMRB(mrbNumber, false);

            DataTable dt = await ConvertActionsToDataTable(actions);

            using StreamWriter sw = new StreamWriter(path, false);

            for (int i = 0; i < dt.Columns.Count; i++) {
                sw.Write(dt.Columns[i]);
                if (i < dt.Columns.Count - 1) sw.Write(",");
            }

            sw.Write(sw.NewLine);

            foreach (DataRow dr in dt.Rows) {
                for (int i = 0; i < dt.Columns.Count; i++) {
                    if (!Convert.IsDBNull(dr[i])) {
                        string? value = dr[i].ToString();
                        if (value is null) {
                            sw.Write("");
                        } else if (value.Contains(',')) {
                            value = String.Format("\"{0}\"", value);
                            sw.Write(value);
                        } else {
                            sw.Write(dr[i].ToString());
                        }
                    }

                    if (i < dt.Columns.Count - 1) {
                        sw.Write(",");
                    }
                }

                sw.Write(sw.NewLine);
            }

            sw.Close();
        } catch (Exception ex) {
            _logger.LogError($"Unable to convert MRB {mrbNumber} actions to a CSV file, because {ex.Message}");
            throw;
        }
    }

    private async Task<DataTable> ConvertActionsToDataTable(IEnumerable<MRBAction> actions) {
        try {
            _logger.LogInformation("Attempting to convert MRB actions to a DataTable");

            if (actions is null) throw new ArgumentNullException("MRB actions cannot be null");

            DataTable dt = new();

            if (actions.Count() > 0 && actions.First().Action.Equals("Convert", StringComparison.InvariantCultureIgnoreCase)) {
                dt.Columns.Add("Action", typeof(string));
                dt.Columns.Add("From Customer", typeof(string));
                dt.Columns.Add("From Part Number", typeof(string));
                dt.Columns.Add("Batch Number / Lot Number", typeof(string));
                dt.Columns.Add("Qty", typeof(string));
                dt.Columns.Add("To Customer", typeof(string));
                dt.Columns.Add("To Part Number", typeof(string));
                dt.Columns.Add("Assigned Date", typeof(string));
                dt.Columns.Add("Completed Date", typeof(string));
                dt.Columns.Add("Completed By", typeof(string));

                foreach (MRBAction action in actions) {
                    if (action.CompletedByUser is null && action.CompletedByUserID > 0)
                        action.CompletedByUser = await _userService.GetUserByUserId(action.CompletedByUserID);

                    string convertFromCustomer = string.Empty;
                    string convertFromPart = string.Empty;
                    string convertToCustomer = string.Empty;
                    string convertToPart = string.Empty;

                    string[] convertFrom = action.ConvertFrom.Split(" ");
                    if (convertFrom.Length > 1) {
                        convertFromCustomer = convertFrom[0];
                        foreach (string partStr in convertFrom.Skip(1))
                            convertFromPart += partStr;
                    }

                    string[] convertTo = action.ConvertTo.Split(" ");
                    if (convertTo.Length > 1) {
                        convertToCustomer = convertTo[0];
                        foreach (string partStr in convertTo.Skip(1))
                            convertToPart += partStr;
                    }

                    dt.Rows.Add(action.Action, convertFromCustomer, convertFromPart, action.Quantity.ToString(),
                                convertToCustomer, convertToPart,
                                DateTimeUtilities.GetDateAsStringMinDefault(action.AssignedDate),
                                DateTimeUtilities.GetDateAsStringMaxDefault(action.CompletedDate),
                                action.CompletedByUser is null ? "" : action.CompletedByUser.GetFullName());
                }
            } else {
                dt.Columns.Add("Action", typeof(string));
                dt.Columns.Add("Customer", typeof(string));
                dt.Columns.Add("Qty", typeof(string));
                dt.Columns.Add("Convert From", typeof(string));
                dt.Columns.Add("Convert To", typeof(string));
                dt.Columns.Add("Part Number", typeof(string));
                dt.Columns.Add("Batch Number / Lot Number", typeof(string));
                dt.Columns.Add("Justification", typeof(string));
                dt.Columns.Add("Assigned Date", typeof(string));
                dt.Columns.Add("Completed Date", typeof(string));
                dt.Columns.Add("Completed By", typeof(string));

                foreach (MRBAction action in actions) {
                    if (action.CompletedByUser is null && action.CompletedByUserID > 0)
                        action.CompletedByUser = await _userService.GetUserByUserId(action.CompletedByUserID);

                    dt.Rows.Add(action.Action, action.Customer, action.Quantity.ToString(), action.ConvertFrom, action.ConvertTo,
                                action.PartNumber, action.LotNumber, action.Justification,
                                DateTimeUtilities.GetDateAsStringMinDefault(action.AssignedDate),
                                DateTimeUtilities.GetDateAsStringMaxDefault(action.CompletedDate),
                                action.CompletedByUser is null ? "" : action.CompletedByUser.GetFullName());
                }
            }

            return dt;
        } catch (Exception ex) {
            _logger.LogError($"Unable to convert MRB actions to a DataTable, because {ex.Message}");
            throw;
        }
    }

    private async Task SaveAttachmentInDb(IFormFile file, string path, int mrbNumber) {
        try {
            _logger.LogInformation($"Attempting to save attachment to database");

            if (file is null) throw new ArgumentNullException("File cannot be null");
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path cannot be null or empty");
            if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRB number");

            StringBuilder queryBuilder = new();
            queryBuilder.Append("insert into MRBAttachment (MRBNumber, FileName, UploadDate, Path) ");
            queryBuilder.Append($"values ({mrbNumber}, '{file.FileName}', ");
            queryBuilder.Append($"'{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}', '{path}');");

            int rowsAffected = await _dalService.ExecuteAsync(queryBuilder.ToString());

            if (rowsAffected <= 0)
                throw new Exception("Unable to insert attachment in database");
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to save file to DB. Exception: {ex.Message}");
            throw;
        }
    }

    private async Task SaveActionAttachmentInDb(IFormFile file, string path, int actionId) {
        try {
            _logger.LogInformation($"Attempting to save action attachment to database");

            if (file is null) throw new ArgumentNullException("File cannot be null");
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path cannot be null or empty");
            if (actionId <= 0) throw new ArgumentException($"{actionId} is not a valid MRB action ID");

            StringBuilder queryBuilder = new();
            queryBuilder.Append("insert into MRBActionAttachment (ActionID, FileName, UploadDate, Path) ");
            queryBuilder.Append($"values ({actionId}, '{file.FileName}', ");
            queryBuilder.Append($"'{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}', '{path}');");

            int rowsAffected = await _dalService.ExecuteAsync(queryBuilder.ToString());

            if (rowsAffected <= 0)
                throw new Exception("Unable to insert action attachment in database");
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to save file to DB. Exception: {ex.Message}");
            throw;
        }
    }
}