using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.Dtos.Expenses;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Expenses;
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.MongoDBModels.Employees;
using Marco.Pms.Model.MongoDBModels.Expenses;
using Marco.Pms.Model.MongoDBModels.Masters;
using Marco.Pms.Model.MongoDBModels.Project;
using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Expanses;
using Marco.Pms.Model.ViewModels.Expenses;
using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Service;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using Document = Marco.Pms.Model.DocumentManager.Document;

namespace Marco.Pms.Services.Service
{
    public class ExpensesService : IExpensesService
    {
        private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
        private readonly ApplicationDbContext _context;
        private readonly ILoggingService _logger;
        private readonly S3UploadService _s3Service;
        private readonly IServiceScopeFactory _serviceScopeFactory;
        private readonly UtilityMongoDBHelper _updateLogHelper;
        private readonly CacheUpdateHelper _cache;
        private readonly IMapper _mapper;
        private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
        private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7");
        private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b");
        private static readonly Guid Approve = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8");
        private static readonly Guid RejectedByApprover = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
        private static readonly Guid ProcessPending = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27");
        private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95");
        private static readonly string Collection = "ExpensesModificationLog";
        public ExpensesService(
            IDbContextFactory<ApplicationDbContext> dbContextFactory,
            ApplicationDbContext context,
            IServiceScopeFactory serviceScopeFactory,
            UtilityMongoDBHelper updateLogHelper,
            CacheUpdateHelper cache,
            ILoggingService logger,
            S3UploadService s3Service,
            IMapper mapper)
        {
            _dbContextFactory = dbContextFactory;
            _context = context;
            _logger = logger;
            _cache = cache;
            _serviceScopeFactory = serviceScopeFactory;
            _updateLogHelper = updateLogHelper;
            _s3Service = s3Service;
            _mapper = mapper;
        }

        #region =================================================================== Get Functions ===================================================================

        /// <summary>
        /// Retrieves a paginated list of expenses based on user permissions and optional filters.
        /// </summary>
        /// <param name="filter">A URL-encoded JSON string containing filter criteria. See <see cref="ExpensesFilter"/>.</param>
        /// <param name="pageSize">The number of records to return per page.</param>
        /// <param name="pageNumber">The page number to retrieve.</param>
        /// <returns>A paginated list of expenses.</returns>
        public async Task<ApiResponse<object>> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber)
        {
            try
            {
                _logger.LogInfo(
                    "Attempting to fetch expenses list for PageNumber: {PageNumber}, PageSize: {PageSize} with Filter: {Filter}",
                    pageNumber, pageSize, filter ?? "");

                // 1. --- Get User Permissions ---
                if (loggedInEmployee == null)
                {
                    // This is an authentication/authorization issue. The user should be logged in.
                    _logger.LogWarning("Could not find an employee for the current logged-in user.");
                    return ApiResponse<object>.ErrorResponse("User not found or not authenticated.", 403);
                }
                Guid loggedInEmployeeId = loggedInEmployee.Id;
                List<ExpenseList> expenseVM = new List<ExpenseList>();
                var totalEntites = 0;
                var hasViewSelfPermissionTask = Task.Run(async () =>
                {
                    using var scope = _serviceScopeFactory.CreateScope();
                    var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
                    return await permissionService.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
                });

                var hasViewAllPermissionTask = Task.Run(async () =>
                {
                    using var scope = _serviceScopeFactory.CreateScope();
                    var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
                    return await permissionService.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
                });

                await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);

                if (!hasViewAllPermissionTask.Result && !hasViewSelfPermissionTask.Result)
                {
                    // User has neither required permission. Deny access.
                    _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
                    return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "You do not have permission to view any expenses.", 200);
                }


                // 2. --- Deserialize Filter and Apply ---
                ExpensesFilter? expenseFilter = TryDeserializeFilter(filter);

                var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result,
                    pageNumber, pageSize, expenseFilter, searchString);

                // 3. --- Build Base Query and Apply Permissions ---
                // Start with a base IQueryable. Filters will be chained onto this.
                var expensesQuery = _context.Expenses
                .Where(e => e.TenantId == tenantId); // Always filter by TenantId first.

                if (cacheList == null)
                {
                    await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId);

                    // Apply permission-based filtering BEFORE any other filters or pagination.
                    if (hasViewAllPermissionTask.Result)
                    {
                        expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId || e.StatusId != Draft);
                    }
                    else if (hasViewSelfPermissionTask.Result)
                    {
                        // User only has 'View Self' permission, so restrict the query to their own expenses.
                        _logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
                        expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId);
                    }

                    if (expenseFilter != null)
                    {
                        if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue)
                        {
                            if (expenseFilter.IsTransactionDate)
                            {
                                expensesQuery = expensesQuery.Where(e => e.TransactionDate.Date >= expenseFilter.StartDate.Value.Date && e.TransactionDate.Date <= expenseFilter.EndDate.Value.Date);
                            }
                            else
                            {
                                expensesQuery = expensesQuery.Where(e => e.CreatedAt.Date >= expenseFilter.StartDate.Value.Date && e.CreatedAt.Date <= expenseFilter.EndDate.Value.Date);
                            }

                        }

                        if (expenseFilter.ProjectIds?.Any() == true)
                        {
                            expensesQuery = expensesQuery.Where(e => expenseFilter.ProjectIds.Contains(e.ProjectId));
                        }

                        if (expenseFilter.StatusIds?.Any() == true)
                        {
                            expensesQuery = expensesQuery.Where(e => expenseFilter.StatusIds.Contains(e.StatusId));
                        }

                        if (expenseFilter.PaidById?.Any() == true)
                        {
                            expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
                        }

                        // Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
                        if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
                        {
                            expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById));
                        }
                    }

                    if (!string.IsNullOrWhiteSpace(searchString))
                    {
                        var searchStringLower = searchString.ToLower();
                        expensesQuery = expensesQuery.Include(e => e.PaidBy).Include(e => e.CreatedBy)
                            .Where(e => e.Description.ToLower().Contains(searchStringLower) ||
                            (e.TransactionId != null && e.TransactionId.ToLower().Contains(searchStringLower)) ||
                            (e.PaidBy != null && (e.PaidBy.FirstName + " " + e.PaidBy.LastName).ToLower().Contains(searchStringLower)) ||
                            (e.CreatedBy != null && (e.CreatedBy.FirstName + " " + e.CreatedBy.LastName).ToLower().Contains(searchStringLower)));
                    }

                    // 4. --- Apply Ordering and Pagination ---
                    // This should be the last step before executing the query.

                    totalEntites = await expensesQuery.CountAsync();

                    var paginatedQuery = expensesQuery
                        .OrderByDescending(e => e.CreatedAt)
                        .Skip((pageNumber - 1) * pageSize)
                        .Take(pageSize);

                    // 5. --- Execute Query and Map Results ---
                    var expensesList = await paginatedQuery.ToListAsync();

                    if (!expensesList.Any())
                    {
                        _logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployeeId);
                        return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
                    }

                    expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
                    totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);

                }
                else
                {
                    var permissionStatusMapping = await _context.StatusPermissionMapping
                            .GroupBy(ps => ps.StatusId)
                            .Select(g => new
                            {
                                StatusId = g.Key,
                                PermissionIds = g.Select(ps => ps.PermissionId).ToList()
                            }).ToListAsync();

                    expenseVM = cacheList.Select(m =>
                        {
                            var response = _mapper.Map<ExpenseList>(m);
                            if (response.Status != null && (response.NextStatus?.Any() ?? false))
                            {
                                response.Status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == Guid.Parse(m.Status.Id)).Select(ps => ps.PermissionIds).FirstOrDefault();
                                foreach (var status in response.NextStatus)
                                {
                                    status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
                                }
                            }
                            return response;
                        }).ToList();
                    totalEntites = (int)totalCount;
                }
                // 7. --- Return Final Success Response ---
                var message = $"{expenseVM.Count} expense records fetched successfully.";
                _logger.LogInfo(message);


                var response = new
                {
                    CurrentFilter = expenseFilter,
                    CurrentPage = pageNumber,
                    TotalPages = totalPages,
                    TotalEntites = totalEntites,
                    Data = expenseVM,
                };
                return ApiResponse<object>.SuccessResponse(response, message, 200);
            }
            catch (DbUpdateException dbEx)
            {
                _logger.LogError(dbEx, "Databsae Exception occured while fetching list expenses");
                return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error occured while fetching list expenses");
                return ApiResponse<object>.ErrorResponse("Error Occured", ExceptionMapper(ex), 500);
            }
        }
        public async Task<ApiResponse<object>> GetExpenseDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId)
        {
            try
            {
                var expenseDetails = await _cache.GetExpenseDetailsById(id, tenantId);
                if (expenseDetails == null)
                {
                    var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);

                    if (expense == null)
                    {
                        _logger.LogWarning("User attempted to fetch expense details with ID {ExpenseId}, but not found in both database and cache", id);
                        return ApiResponse<object>.ErrorResponse("Expense Not Found", "Expense Not Found", 404);
                    }
                    expenseDetails = await GetAllExpnesRelatedTablesForSingle(expense, expense.TenantId);
                }
                var vm = _mapper.Map<ExpenseDetailsVM>(expenseDetails);

                var permissionStatusMappingTask = Task.Run(async () =>
                   {
                       await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                       return await dbContext.StatusPermissionMapping
                           .GroupBy(ps => ps.StatusId)
                           .Select(g => new
                           {
                               StatusId = g.Key,
                               PermissionIds = g.Select(ps => ps.PermissionId).ToList()
                           }).ToListAsync();
                   });
                var expenseReimburseTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.ExpensesReimburseMapping
                        .Include(er => er.ExpensesReimburse)
                            .ThenInclude(er => er!.ReimburseBy)
                                .ThenInclude(e => e!.JobRole)
                        .Where(er => er.TenantId == tenantId && er.ExpensesId == vm.Id)
                        .Select(er => er.ExpensesReimburse).FirstOrDefaultAsync();
                });
                var permissionStatusMappings = permissionStatusMappingTask.Result;
                var expensesReimburse = expenseReimburseTask.Result;

                if (vm.Status != null && (vm.NextStatus?.Any() ?? false))
                {
                    vm.Status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == vm.Status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
                    foreach (var status in vm.NextStatus)
                    {
                        status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
                    }
                }
                vm.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);

                foreach (var document in expenseDetails.Documents)
                {
                    var response = vm.Documents.FirstOrDefault(d => d.DocumentId == Guid.Parse(document.DocumentId));

                    response!.PreSignedUrl = _s3Service.GeneratePreSignedUrl(document.S3Key);
                    response!.ThumbPreSignedUrl = _s3Service.GeneratePreSignedUrl(document.ThumbS3Key);
                }

                var expenselogs = await _context.ExpenseLogs.Include(el => el.UpdatedBy).Where(el => el.ExpenseId == vm.Id).Select(el => _mapper.Map<ExpenseLogVM>(el)).ToListAsync();

                vm.ExpenseLogs = expenselogs;

                _logger.LogInfo("Employee {EmployeeId} successfully fetched expense details with ID {ExpenseId}", loggedInEmployee.Id, vm.Id);
                return ApiResponse<object>.SuccessResponse(vm, "Successfully fetched the details of expense", 200);

            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An unhandled exception occurred while fetching an expense details {ExpenseId}.", id);
                return ApiResponse<object>.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500);
            }
        }
        public async Task<ApiResponse<object>> GetSupplerNameListAsync(Employee loggedInEmployee, Guid tenantId)
        {
            try
            {
                var supplerNameList = await _context.Expenses.Where(e => e.TenantId == tenantId).Select(e => e.SupplerName).Distinct().ToListAsync();
                _logger.LogInfo("Employee {EmployeeId} fetched list of suppler names from expenses in a tenant {TenantId}", loggedInEmployee.Id, tenantId);
                return ApiResponse<object>.SuccessResponse(supplerNameList, $"{supplerNameList.Count} records of suppler names fetched from expense", 200);
            }
            catch (DbUpdateException dbEx)
            {
                _logger.LogError(dbEx, "Databsae Exception occured while fetching suppler name list from expense");
                return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);
            }
        }
        public async Task<ApiResponse<object>> GetFilterObjectAsync(Employee loggedInEmployee, Guid tenantId)
        {
            try
            {
                // Task 1: Get all distinct projects associated with the tenant's expenses.
                var projectsTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.Expenses
                        .Where(e => e.TenantId == tenantId && e.Project != null)
                        .Select(e => e.Project!)
                        .Distinct()
                        .Select(p => new { p.Id, Name = $"{p.Name}" })
                        .ToListAsync();
                });

                // Task 2: Get all distinct users who paid for the tenant's expenses.
                var paidByTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.Expenses
                        .Where(e => e.TenantId == tenantId && e.PaidBy != null)
                        .Select(e => e.PaidBy!)
                        .Distinct()
                        .Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
                        .ToListAsync();
                });

                // Task 3: Get all distinct users who created the tenant's expenses.
                var createdByTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.Expenses
                        .Where(e => e.TenantId == tenantId && e.CreatedBy != null)
                        .Select(e => e.CreatedBy!)
                        .Distinct()
                        .Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
                        .ToListAsync();
                });

                // Task 4: Get all distinct statuses associated with the tenant's expenses.
                var statusTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.Expenses
                        .Where(e => e.TenantId == tenantId && e.Status != null)
                        .Select(e => e.Status!)
                        .Distinct()
                        .Select(s => new { s.Id, s.Name })
                        .ToListAsync();
                });

                // Execute all four queries concurrently. The total wait time will be determined
                // by the longest-running query, not the sum of all four.
                await Task.WhenAll(projectsTask, paidByTask, createdByTask, statusTask);

                // Construct the final object from the results of the completed tasks.
                return ApiResponse<object>.SuccessResponse(new
                {
                    Projects = await projectsTask,
                    PaidBy = await paidByTask,
                    CreatedBy = await createdByTask,
                    Status = await statusTask
                }, "Successfully fetched the filter list", 200);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Exception occured while fetching the list filters for expenses");
                return ApiResponse<object>.ErrorResponse("Internal Exception Occured", ExceptionMapper(ex), 500);
            }
        }

        #endregion

        #region =================================================================== Post Functions ===================================================================

        /// <summary>
        /// Creates a new expense entry along with its bill attachments.
        /// This operation is transactional and performs validations and file uploads concurrently for optimal performance
        /// by leveraging async/await without unnecessary thread-pool switching via Task.Run.
        /// </summary>
        /// <param name="dto">The data transfer object containing expense details and attachments.</param>
        /// <returns>An IActionResult indicating the result of the creation operation.</returns>
        public async Task<ApiResponse<object>> CreateExpenseAsync(CreateExpensesDto dto, Employee loggedInEmployee, Guid tenantId)
        {
            _logger.LogDebug("Starting CreateExpense for Project {ProjectId}", dto.ProjectId);
            // The entire operation is wrapped in a transaction to ensure data consistency.
            await using var transaction = await _context.Database.BeginTransactionAsync();

            try
            {
                // 1. Authorization & Validation: Run all I/O-bound checks concurrently using factories for safety.

                // PERMISSION CHECKS: Use IServiceScopeFactory for thread-safe access to scoped services.
                var hasUploadPermissionTask = Task.Run(async () => // Task.Run is acceptable here to create a new scope, but let's do it cleaner.
                {
                    using var scope = _serviceScopeFactory.CreateScope();
                    var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
                    return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
                });

                var hasProjectPermissionTask = Task.Run(async () =>
                {
                    using var scope = _serviceScopeFactory.CreateScope();
                    var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
                    return await permissionService.HasProjectPermission(loggedInEmployee, dto.ProjectId);
                });

                // VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries.
                // Each task gets its own DbContext instance.
                var projectTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == dto.ProjectId);
                });
                var paidByTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.Employees.AsNoTracking().FirstOrDefaultAsync(e => e.Id == dto.PaidById);
                });
                var expenseTypeTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == dto.ExpensesTypeId);
                });
                var paymentModeTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId);
                });
                var statusMappingTask = Task.Run(async () =>
                {
                    await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                    return await dbContext.ExpensesStatusMapping
                    .Include(s => s.Status)
                    .Include(s => s.NextStatus)
                    .AsNoTracking()
                    .Where(es => es.StatusId == Draft && es.Status != null)
                    .GroupBy(s => s.StatusId)
                    .Select(g => new
                    {
                        Status = g.Select(s => s.Status).FirstOrDefault(),
                        NextStatus = g.Select(s => s.NextStatus).ToList()
                    })
                    .FirstOrDefaultAsync();
                });


                // Await all prerequisite checks at once.
                await Task.WhenAll(
                    hasUploadPermissionTask, hasProjectPermissionTask,
                    projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask
                );

                // 2. Aggregate and Check Results
                if (!await hasUploadPermissionTask || !await hasProjectPermissionTask)
                {
                    _logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
                    return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403);
                }

                var validationErrors = new List<string>();
                var project = await projectTask;
                var expenseType = await expenseTypeTask;
                var paymentMode = await paymentModeTask;
                var statusMapping = await statusMappingTask;
                var paidBy = await paidByTask;

                if (project == null) validationErrors.Add("Project not found.");
                if (paidBy == null) validationErrors.Add("Paid by employee not found");
                if (expenseType == null) validationErrors.Add("Expense Type not found.");
                if (paymentMode == null) validationErrors.Add("Payment Mode not found.");
                if (statusMapping == null) validationErrors.Add("Default status 'Draft' not found.");
                if ((expenseType?.IsAttachmentRequried ?? true) && !(dto.BillAttachments?.Any() ?? false)) validationErrors.Add("Bill Attachment is requried, but not found");

                if (validationErrors.Any())
                {
                    await transaction.RollbackAsync();
                    var errorMessage = string.Join(" ", validationErrors);
                    _logger.LogWarning("Expense creation failed due to validation errors: {ValidationErrors}", errorMessage);
                    return ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400);
                }

                // 3. Entity Creation
                var expense = _mapper.Map<Expenses>(dto);
                expense.CreatedById = loggedInEmployee.Id;
                expense.CreatedAt = DateTime.UtcNow;
                expense.TenantId = tenantId;
                expense.IsActive = true;
                expense.StatusId = Draft;

                _context.Expenses.Add(expense);

                // 4. Process Attachments
                if (dto.BillAttachments?.Any() ?? false)
                {
                    await ProcessAndUploadAttachmentsAsync(dto.BillAttachments, expense, loggedInEmployee.Id, tenantId);
                }

                // 5. Database Commit
                await _context.SaveChangesAsync();

                // 6. Transaction Commit
                await transaction.CommitAsync();

                await _cache.AddExpenseByObjectAsync(expense);

                var response = _mapper.Map<ExpenseList>(expense);
                response.PaidBy = _mapper.Map<BasicEmployeeVM>(paidBy);
                response.Project = _mapper.Map<ProjectInfoVM>(project);
                response.Status = _mapper.Map<ExpensesStatusMasterVM>(statusMapping!.Status);
                response.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(statusMapping.NextStatus);
                response.PaymentMode = _mapper.Map<PaymentModeMatserVM>(paymentMode);
                response.ExpensesType = _mapper.Map<ExpensesTypeMasterVM>(expenseType);

                _logger.LogInfo("Successfully created Expense {ExpenseId} for Project {ProjectId}.", expense.Id, expense.ProjectId);
                return ApiResponse<object>.SuccessResponse(response, "Expense created successfully.", 201);
            }
            catch (DbUpdateException dbEx)
            {
                await transaction.RollbackAsync();
                _logger.LogError(dbEx, "Databsae Exception occured while adding expense");
                return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);
            }
            catch (ArgumentException ex) // Catches bad Base64 from attachment pre-validation
            {
                _logger.LogError(ex, "Invalid argument during expense creation for project {ProjectId}.", dto.ProjectId);
                return ApiResponse<object>.ErrorResponse("Invalid Request Data.", ExceptionMapper(ex), 400);
            }
            catch (Exception ex) // General-purpose catch for unexpected errors (e.g., S3 or DB connection failure)
            {
                _logger.LogError(ex, "An unhandled exception occurred while creating an expense for project {ProjectId}.", dto.ProjectId);
                return ApiResponse<object>.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500);
            }
        }

        /// <summary>
        /// Changes the status of an expense record, performing validation, permission checks, and logging.
        /// </summary>
        /// <param name="model">The DTO containing the expense ID and the target status ID.</param>
        /// <param name="loggedInEmployee">The employee performing the action.</param>
        /// <param name="tenantId">The ID of the tenant owning the expense.</param>
        /// <returns>An ApiResponse containing the updated expense details or an error.</returns>
        public async Task<ApiResponse<object>> ChangeStatusAsync(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId)
        {
            using var scope = _serviceScopeFactory.CreateScope();
            var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();

            // 1. Fetch Existing Expense with Related Entities (Single Query)
            var expense = await _context.Expenses
                .Include(e => e.ExpensesType)
                .Include(e => e.Project)
                .Include(e => e.PaidBy).ThenInclude(e => e!.JobRole)
                .Include(e => e.PaymentMode)
                .Include(e => e.Status)
                .Include(e => e.CreatedBy)
                .Include(e => e.ReviewedBy)
                .Include(e => e.ApprovedBy)
                .Include(e => e.ProcessedBy)
                .FirstOrDefaultAsync(e =>
                    e.Id == model.ExpenseId &&
                    e.StatusId != model.StatusId &&
                    e.TenantId == tenantId
                );

            if (expense == null)
            {
                _logger.LogWarning("ChangeStatus: Expense not found or already at target status. ExpenseId={ExpenseId}, TenantId={TenantId}", model.ExpenseId, tenantId);
                return ApiResponse<object>.ErrorResponse("Expense not found or status is already set.", "Expense not found", 404);
            }

            _logger.LogInfo("ChangeStatus: Requested status change. ExpenseId={ExpenseId} FromStatus={FromStatusId} ToStatus={ToStatusId}",
                expense.Id, expense.StatusId, model.StatusId);

            // 2. Run Prerequisite Checks in Parallel (Status transition + Permissions)
            var statusTransitionTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.ExpensesStatusMapping
                    .Include(m => m.NextStatus)
                    .FirstOrDefaultAsync(m => m.StatusId == expense.StatusId && m.NextStatusId == model.StatusId);
            });

            var targetStatusPermissionsTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.StatusPermissionMapping
                    .Where(spm => spm.StatusId == model.StatusId)
                    .ToListAsync();
            });

            await Task.WhenAll(statusTransitionTask, targetStatusPermissionsTask);
            var statusTransition = await statusTransitionTask;
            var requiredPermissions = await targetStatusPermissionsTask;

            // 3. Validate Transition and Required Fields
            if (statusTransition == null)
            {
                _logger.LogWarning("ChangeStatus: Invalid status transition. ExpenseId={ExpenseId}, FromStatus={FromStatus}, ToStatus={ToStatus}",
                    expense.Id, expense.StatusId, model.StatusId);
                return ApiResponse<object>.ErrorResponse("Status change is not permitted.", "Invalid Transition", 400);
            }

            // Validate special logic for "Processed"
            if (statusTransition.NextStatusId == Processed &&
                (string.IsNullOrWhiteSpace(model.ReimburseTransactionId) ||
                 !model.ReimburseDate.HasValue ||
                 model.ReimburseById == null ||
                 model.ReimburseById == Guid.Empty))
            {
                _logger.LogWarning("ChangeStatus: Missing reimbursement fields for 'Processed'. ExpenseId={ExpenseId}", expense.Id);
                return ApiResponse<object>.ErrorResponse("Reimbursement details are missing or invalid.", "Invalid Reimbursement", 400);
            }

            // 4. Permission Check (CreatedBy -> Reviewer bypass, else required permissions)
            bool hasPermission = false;
            if (model.StatusId == Review && expense.CreatedById == loggedInEmployee.Id)
            {
                hasPermission = true;
            }
            else if (requiredPermissions.Any())
            {
                var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
                foreach (var permission in requiredPermissions)
                {
                    if (await permissionService.HasPermission(permission.PermissionId, loggedInEmployee.Id) && model.StatusId != Review)
                    {
                        hasPermission = true;
                        break;
                    }
                }
            }

            if (!hasPermission)
            {
                _logger.LogWarning("ChangeStatus: Permission denied. EmployeeId={EmployeeId} ExpenseId={ExpenseId} ToStatus={ToStatusId}",
                    loggedInEmployee.Id, expense.Id, model.StatusId);
                return ApiResponse<object>.ErrorResponse("You do not have permission for this action.", "Access Denied", 403);
            }

            // 5. Prepare for update (Audit snapshot)
            var expenseStateBeforeChange = _updateLogHelper.EntityToBsonDocument(expense);

            // 6. Apply Status Transition
            expense.StatusId = statusTransition.NextStatusId;
            expense.Status = statusTransition.NextStatus;

            // Handle reviewer/approver/processor fields based on target StatusId (Guid)
            if (model.StatusId == Approve || model.StatusId == RejectedByReviewer)
            {
                expense.ReviewedById = loggedInEmployee.Id;
            }
            else if (model.StatusId == ProcessPending || model.StatusId == RejectedByApprover)
            {
                expense.ApprovedById = loggedInEmployee.Id;
            }
            else if (model.StatusId == Processed)
            {
                expense.ProcessedById = loggedInEmployee.Id;
            }

            // 7. Add Reimbursement if applicable
            if (model.StatusId == Processed)
            {
                var reimbursement = new ExpensesReimburse
                {
                    ReimburseTransactionId = model.ReimburseTransactionId!,
                    ReimburseDate = model.ReimburseDate!.Value,
                    ReimburseById = model.ReimburseById!.Value,
                    ReimburseNote = model.Comment ?? string.Empty,
                    TenantId = tenantId
                };
                _context.ExpensesReimburse.Add(reimbursement);
                _context.ExpensesReimburseMapping.Add(new ExpensesReimburseMapping
                {
                    ExpensesId = expense.Id,
                    ExpensesReimburseId = reimbursement.Id,
                    TenantId = tenantId
                });
            }

            // 8. Add Expense Log Entry
            _context.ExpenseLogs.Add(new ExpenseLog
            {
                ExpenseId = expense.Id,
                Action = $"Status changed to '{statusTransition.NextStatus?.Name}'",
                UpdatedById = loggedInEmployee.Id,
                UpdateAt = DateTime.UtcNow,
                Comment = model.Comment,
                TenantId = tenantId
            });

            // 9. Commit database transaction
            try
            {
                await _context.SaveChangesAsync();
                _logger.LogInfo("ChangeStatus: Status updated successfully. ExpenseId={ExpenseId} NewStatus={NewStatusId}", expense.Id, expense.StatusId);
            }
            catch (DbUpdateConcurrencyException ex)
            {
                _logger.LogError(ex, "ChangeStatus: Concurrency error. ExpenseId={ExpenseId}", expense.Id);
                return ApiResponse<object>.ErrorResponse("Expense was modified by another user. Please refresh and try again.", "Concurrency Error", 409);
            }

            _ = Task.Run(async () =>
            {
                // --- Push Notification Section ---
                // This section attempts to send a test push notification to the user's device.
                // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.

                var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}";

                await _firebase.SendExpenseMessageAsync(expense, name, tenantId);

            });

            // 10. Post-processing (audit log, cache, fetch next states)
            try
            {
                var auditLogTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
                {
                    EntityId = expense.Id.ToString(),
                    UpdatedById = loggedInEmployee.Id.ToString(),
                    OldObject = expenseStateBeforeChange,
                    UpdatedAt = DateTime.UtcNow
                }, Collection);

                var cacheUpdateTask = _cache.ReplaceExpenseAsync(expense);

                var getNextStatusesTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(async t =>
                {
                    var dbContext = t.Result;
                    var nextStatuses = await dbContext.ExpensesStatusMapping
                        .Include(m => m.NextStatus)
                        .Where(m => m.StatusId == expense.StatusId && m.NextStatus != null)
                        .Select(m => m.NextStatus)
                        .ToListAsync();
                    await dbContext.DisposeAsync();
                    return nextStatuses;
                }).Unwrap();

                await Task.WhenAll(auditLogTask, getNextStatusesTask, cacheUpdateTask);

                // Prepare response with possible next states
                var nextPossibleStatuses = await getNextStatusesTask;
                var responseDto = _mapper.Map<ExpenseList>(expense);
                if (nextPossibleStatuses is { Count: > 0 })
                    responseDto.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(nextPossibleStatuses);

                return ApiResponse<object>.SuccessResponse(responseDto);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "ChangeStatus: Post-operation error (e.g. audit logging). ExpenseId={ExpenseId}", expense.Id);
                var responseDto = _mapper.Map<ExpenseList>(expense);
                return ApiResponse<object>.SuccessResponse(responseDto, "Status updated, but audit logging or cache update failed.");
            }
        }

        #endregion

        #region =================================================================== Put Functions ===================================================================

        public async Task<ApiResponse<object>> UpdateExpanseAsync(Guid id, UpdateExpensesDto model, Employee loggedInEmployee, Guid tenantId)
        {
            if (id != model.Id)
            {
                _logger.LogWarning("Id provided by path parameter and Id from body not matches for employee {EmployeeId}", loggedInEmployee.Id);
                return ApiResponse<object>.ErrorResponse("Invalid Parameters", "Invalid Parameters", 400);
            }
            var existingExpense = await _context.Expenses
                .Include(e => e.ExpensesType)
                .Include(e => e.Project)
                .Include(e => e.PaidBy)
                    .ThenInclude(e => e!.JobRole)
                .Include(e => e.PaymentMode)
                .Include(e => e.Status)
                .Include(e => e.CreatedBy)
                .Include(e => e.ReviewedBy)
                .Include(e => e.ApprovedBy)
                .Include(e => e.ProcessedBy)
                .FirstOrDefaultAsync(e =>
                    e.Id == model.Id &&
                    e.TenantId == tenantId);


            if (existingExpense == null)
            {
                _logger.LogWarning("User attempted to update expense with ID {ExpenseId}, but not found in database", id);
                return ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404);
            }

            if (existingExpense.StatusId != Draft && existingExpense.StatusId != RejectedByReviewer && existingExpense.StatusId != RejectedByApprover)
            {
                _logger.LogWarning("User attempted to update expense with ID {ExpenseId}, but donot have status of DRAFT or REJECTED, but is {StatusId}", existingExpense.Id, existingExpense.StatusId);
                return ApiResponse<object>.ErrorResponse("Expense connot be updated", "Expense connot be updated", 400);
            }
            if (existingExpense.CreatedById != loggedInEmployee.Id)
            {
                _logger.LogWarning("User attempted to update expense with ID {ExpenseId} which not created by them", existingExpense.Id);
                return ApiResponse<object>.ErrorResponse("You donot have access to update this expense", "You donot have access to update this expense", 400);
            }

            var existingEntityBson = _updateLogHelper.EntityToBsonDocument(existingExpense); // Capture state for audit log BEFORE changes
            _mapper.Map(model, existingExpense);
            _context.Entry(existingExpense).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
                _logger.LogInfo("Successfully updated Expense {ExpenseId} by user {UserId}.", id, loggedInEmployee.Id);
            }
            catch (DbUpdateConcurrencyException ex)
            {
                // --- Step 3: Handle Concurrency Conflicts ---
                // This happens if another user modified the project after we fetched it.
                _logger.LogError(ex, "Concurrency conflict while updating project {ProjectId} ", id);
                return ApiResponse<object>.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409);
            }

            if (model.BillAttachments?.Any() ?? false)
            {
                var newBillAttachments = model.BillAttachments.Where(ba => ba.DocumentId == null && ba.IsActive).ToList();
                if (newBillAttachments.Any())
                {
                    await ProcessAndUploadAttachmentsAsync(newBillAttachments, existingExpense, loggedInEmployee.Id, tenantId);
                    try
                    {
                        await _context.SaveChangesAsync();
                        _logger.LogInfo("{Count} New attachments added while updating expense {ExpenseId} by employee {EmployeeId}",
                            newBillAttachments.Count, existingExpense.Id, loggedInEmployee.Id);
                    }
                    catch (DbUpdateException dbEx)
                    {
                        _logger.LogError(dbEx, "Databsae Exception occured while adding new attachments during updating expense");
                        return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);

                    }
                }

                var deleteBillAttachments = model.BillAttachments.Where(ba => ba.DocumentId != null && !ba.IsActive).ToList();
                if (deleteBillAttachments.Any())
                {
                    var documentIds = deleteBillAttachments.Select(d => d.DocumentId!.Value).ToList();
                    try
                    {
                        await DeleteAttachemnts(documentIds);
                        _logger.LogInfo("{Count} Attachments deleted while updating expense {ExpenseId} by employee {EmployeeId}",
                            deleteBillAttachments.Count, existingExpense.Id, loggedInEmployee.Id);
                    }
                    catch (DbUpdateException dbEx)
                    {
                        _logger.LogError(dbEx, "Databsae Exception occured while deleting attachments during updating expense");
                        return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Exception occured while deleting attachments during updating expense");
                        return ApiResponse<object>.ErrorResponse("Exception occured while deleting attachments during updating expense ", ExceptionMapper(ex), 500);
                    }
                }
            }


            try
            {
                // Task to save the detailed audit log to a separate system (e.g., MongoDB).
                var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
                {
                    EntityId = existingExpense.Id.ToString(),
                    UpdatedById = loggedInEmployee.Id.ToString(),
                    OldObject = existingEntityBson,
                    UpdatedAt = DateTime.UtcNow
                }, Collection);

                // Task to get all possible next statuses from the *new* current state to help the UI.
                // NOTE: This now fetches a list of all possible next states, which is more useful for a UI.
                var getNextStatusesTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(t =>
                {
                    var dbContext = t.Result;
                    return dbContext.ExpensesStatusMapping
                        .Include(s => s.NextStatus)
                        .Where(s => s.StatusId == existingExpense.StatusId && s.NextStatus != null)
                        .Select(s => s.NextStatus) // Select only the status object
                        .ToListAsync()
                        .ContinueWith(res =>
                        {
                            dbContext.Dispose(); // Ensure the context is disposed
                            return res.Result;
                        });
                }).Unwrap();

                var cacheUpdateTask = _cache.ReplaceExpenseAsync(existingExpense);

                await Task.WhenAll(mongoDBTask, getNextStatusesTask, cacheUpdateTask);

                var nextPossibleStatuses = getNextStatusesTask.Result;

                var response = _mapper.Map<ExpenseList>(existingExpense);
                if (nextPossibleStatuses != null)
                {
                    // The response DTO should have a property like: public List<ExpensesStatusMasterVM> NextAvailableStatuses { get; set; }
                    response.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(nextPossibleStatuses);
                }

                return ApiResponse<object>.SuccessResponse(response, "Expense Updated Successfully", 200);
            }
            catch (Exception ex)
            {
                // This catch block handles errors from post-save operations like MongoDB logging.
                // The primary update was successful, but we must log this failure.
                _logger.LogError(ex, "Error occurred during post-save operations for ExpenseId: {ExpenseId} (e.g., audit logging). The primary status change was successful.", existingExpense.Id);

                // We can still return a success response because the main operation succeeded,
                // but we should not block the user for a failed audit log.
                // Alternatively, if audit logging is critical, you could return an error.
                // Here, we choose to return success but log the ancillary failure.
                var response = _mapper.Map<ExpenseList>(existingExpense);
                return ApiResponse<object>.SuccessResponse(response, "Status updated, but a post-processing error occurred.");
            }
        }

        #endregion

        #region =================================================================== Delete Functions ===================================================================

        public async Task<ApiResponse<object>> DeleteExpanseAsync(Guid id, Employee loggedInEmployee, Guid tenantId)
        {
            var expenseTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Expenses.Where(e => e.Id == id && e.StatusId == Draft && e.TenantId == tenantId).FirstOrDefaultAsync();
            });

            var hasAprrovePermissionTask = Task.Run(async () =>
            {
                using var scope = _serviceScopeFactory.CreateScope();
                var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
                return await permissionService.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
            });

            await Task.WhenAll(expenseTask, hasAprrovePermissionTask);

            var hasAprrovePermission = hasAprrovePermissionTask.Result;
            var existingExpense = expenseTask.Result;

            if (existingExpense == null)
            {
                var message = hasAprrovePermission ? "Expenses not found" : "Expense cannot be deleted";
                if (hasAprrovePermission)
                {
                    _logger.LogWarning("Employee {EmployeeId} attempted to delete expense {ExpenseId}, but not found in database", loggedInEmployee.Id, id);
                }
                else
                {
                    _logger.LogWarning("Employee {EmployeeId} attempted to delete expense {ExpenseId}, Which is created by another employee", loggedInEmployee.Id, id);
                }
                return ApiResponse<object>.ErrorResponse(message, message, 400);
            }
            if (existingExpense.StatusId != Draft)
            {
                _logger.LogWarning("User attempted to delete expense with ID {ExpenseId}, but donot have status of DRAFT or REJECTED", loggedInEmployee.Id);
                return ApiResponse<object>.ErrorResponse("Expense connot be deleted", "Expense connot be deleted", 400);
            }

            if (!hasAprrovePermission && existingExpense.CreatedById != loggedInEmployee.Id)
            {
                _logger.LogWarning("User attempted to delete expense with ID {ExpenseId} which not created by them", loggedInEmployee.Id);
                return ApiResponse<object>.ErrorResponse("You donot have access to delete this expense", "You donot have access to delete this expense", 400);

            }

            var documentIds = await _context.BillAttachments
                .Where(ba => ba.ExpensesId == existingExpense.Id)
                .Select(ba => ba.DocumentId)
                .ToListAsync();

            var existingEntityBson = _updateLogHelper.EntityToBsonDocument(existingExpense);

            _context.Expenses.Remove(existingExpense);
            try
            {
                await _context.SaveChangesAsync();
                _logger.LogInfo("Employeee {EmployeeId} successfully deleted the expense {EmpenseId}", loggedInEmployee.Id, id);
            }
            catch (DbUpdateException dbEx)
            {
                _logger.LogError(dbEx, "Databsae Exception occured while deleting expense");
                return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);
            }
            try
            {
                var attachmentDeletionTask = Task.Run(async () =>
                {
                    await DeleteAttachemnts(documentIds);
                });

                var cacheTask = Task.Run(async () =>
                {
                    await _cache.DeleteExpenseAsync(id, tenantId);
                });
                var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
                {
                    EntityId = existingExpense.Id.ToString(),
                    UpdatedById = loggedInEmployee.Id.ToString(),
                    OldObject = existingEntityBson,
                    UpdatedAt = DateTime.UtcNow
                }, Collection);

                await Task.WhenAll(attachmentDeletionTask, cacheTask, mongoDBTask);
            }
            catch (DbUpdateException dbEx)
            {
                _logger.LogError(dbEx, "Databsae Exception occured while deleting attachments during updating expense");
                return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Exception occured while deleting attachments during updating expense");
                return ApiResponse<object>.ErrorResponse("Exception occured while deleting attachments during updating expense ", ExceptionMapper(ex), 500);
            }
            return ApiResponse<object>.SuccessResponse("Success", "Expense Deleted Successfully", 200);
        }

        #endregion

        #region =================================================================== Helper Functions ===================================================================
        private static object ExceptionMapper(Exception ex)
        {
            return new
            {
                Message = ex.Message,
                StackTrace = ex.StackTrace,
                Source = ex.Source,
                InnerException = new
                {
                    Message = ex.InnerException?.Message,
                    StackTrace = ex.InnerException?.StackTrace,
                    Source = ex.InnerException?.Source,
                }
            };
        }
        private async Task<List<ExpenseList>> GetAllExpnesRelatedTables(List<Expenses> model, Guid tenantId)
        {
            List<ExpenseList> expenseList = new List<ExpenseList>();
            var projectIds = model.Select(m => m.ProjectId).ToList();
            var statusIds = model.Select(m => m.StatusId).ToList();
            var expensesTypeIds = model.Select(m => m.ExpensesTypeId).ToList();
            var paymentModeIds = model.Select(m => m.PaymentModeId).ToList();
            var createdByIds = model.Select(m => m.CreatedById).ToList();
            var paidByIds = model.Select(m => m.PaidById).ToList();

            var projectTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Projects.AsNoTracking().Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
            });
            var paidByTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Employees.AsNoTracking().Where(e => paidByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
            });
            var createdByTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Employees.AsNoTracking().Where(e => createdByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
            });
            var expenseTypeTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.ExpensesTypeMaster.AsNoTracking().Where(et => expensesTypeIds.Contains(et.Id) && et.TenantId == tenantId).ToListAsync();
            });
            var paymentModeTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.PaymentModeMatser.AsNoTracking().Where(pm => paymentModeIds.Contains(pm.Id) && pm.TenantId == tenantId).ToListAsync();
            });
            var statusMappingTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.ExpensesStatusMapping
                .Include(s => s.Status)
                .Include(s => s.NextStatus)
                .AsNoTracking()
                .Where(es => statusIds.Contains(es.StatusId) && es.Status != null)
                .GroupBy(s => s.StatusId)
                .Select(g => new
                {
                    StatusId = g.Key,
                    Status = g.Select(s => s.Status).FirstOrDefault(),
                    NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList()
                }).ToListAsync();
            });
            var statusTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.ExpensesStatusMaster
                .AsNoTracking()
                .Where(es => statusIds.Contains(es.Id))
                .ToListAsync();
            });
            var permissionStatusMappingTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.StatusPermissionMapping
                    .GroupBy(ps => ps.StatusId)
                    .Select(g => new
                    {
                        StatusId = g.Key,
                        PermissionIds = g.Select(ps => ps.PermissionId).ToList()
                    }).ToListAsync();
            });

            // Await all prerequisite checks at once.
            await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, statusTask, permissionStatusMappingTask);

            var projects = projectTask.Result;
            var expenseTypes = expenseTypeTask.Result;
            var paymentModes = paymentModeTask.Result;
            var statusMappings = statusMappingTask.Result;
            var permissionStatusMappings = permissionStatusMappingTask.Result;
            var paidBys = paidByTask.Result;
            var createdBys = createdByTask.Result;

            expenseList = model.Select(m =>
            {
                var response = _mapper.Map<ExpenseList>(m);

                response.Project = projects.Where(p => p.Id == m.ProjectId).Select(p => _mapper.Map<ProjectInfoVM>(p)).FirstOrDefault();
                response.PaidBy = paidBys.Where(p => p.Id == m.PaidById).Select(p => _mapper.Map<BasicEmployeeVM>(p)).FirstOrDefault();
                response.CreatedBy = createdBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
                response.Status = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map<ExpensesStatusMasterVM>(s.Status)).FirstOrDefault();
                if (response.Status == null)
                {
                    var status = statusTask.Result;
                    response.Status = status.Where(s => s.Id == m.StatusId).Select(s => _mapper.Map<ExpensesStatusMasterVM>(s)).FirstOrDefault();
                }
                if (response.Status != null)
                {
                    response.Status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == m.StatusId).Select(ps => ps.PermissionIds).FirstOrDefault();
                }
                response.NextStatus = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map<List<ExpensesStatusMasterVM>>(s.NextStatus)).FirstOrDefault();
                if (response.NextStatus != null)
                {
                    foreach (var status in response.NextStatus)
                    {
                        status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
                    }
                }
                response.PaymentMode = paymentModes.Where(pm => pm.Id == m.PaymentModeId).Select(pm => _mapper.Map<PaymentModeMatserVM>(pm)).FirstOrDefault();
                response.ExpensesType = expenseTypes.Where(et => et.Id == m.ExpensesTypeId).Select(et => _mapper.Map<ExpensesTypeMasterVM>(et)).FirstOrDefault();

                return response;
            }).ToList();

            return expenseList;
        }

        private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId)
        {
            var projectTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
            });
            var paidByTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
            });
            var createdByTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
            });
            var reviewedByTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
            });
            var approvedByTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
            });
            var processedByTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
            });
            var expenseTypeTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.ExpensesTypeId && et.TenantId == tenantId);
            });
            var paymentModeTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId && pm.TenantId == tenantId);
            });
            var statusMappingTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.ExpensesStatusMapping
                .Include(s => s.Status)
                .Include(s => s.NextStatus)
                .AsNoTracking()
                .Where(es => es.StatusId == model.StatusId && es.Status != null)
                .GroupBy(s => s.StatusId)
                .Select(g => new
                {
                    StatusId = g.Key,
                    Status = g.Select(s => s.Status).FirstOrDefault(),
                    NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList()
                }).FirstOrDefaultAsync();
            });
            var statusTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.ExpensesStatusMaster
                .AsNoTracking()
                .FirstOrDefaultAsync(es => es.Id == model.StatusId);
            });
            var billAttachmentsTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                return await dbContext.BillAttachments
                .Include(ba => ba.Document)
                .AsNoTracking()
                .Where(ba => ba.ExpensesId == model.Id && ba.Document != null)
                .GroupBy(ba => ba.ExpensesId)
                .Select(g => new
                {
                    ExpensesId = g.Key,
                    Documents = g.Select(ba => new DocumentMongoDB
                    {
                        DocumentId = ba.Document!.Id.ToString(),
                        FileName = ba.Document.FileName,
                        ContentType = ba.Document.ContentType,
                        S3Key = ba.Document.S3Key,
                        ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key
                    }).ToList()
                })
                .FirstOrDefaultAsync();
            });

            // Await all prerequisite checks at once.
            await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
                processedByTask, statusTask, billAttachmentsTask);

            var project = projectTask.Result;
            var expenseType = expenseTypeTask.Result;
            var paymentMode = paymentModeTask.Result;
            var statusMapping = statusMappingTask.Result;
            var paidBy = paidByTask.Result;
            var createdBy = createdByTask.Result;
            var reviewedBy = reviewedByTask.Result;
            var approvedBy = approvedByTask.Result;
            var processedBy = processedByTask.Result;
            var billAttachment = billAttachmentsTask.Result;


            var response = _mapper.Map<ExpenseDetailsMongoDB>(model);

            response.Project = _mapper.Map<ProjectBasicMongoDB>(project);
            response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(paidBy);
            response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(createdBy);
            response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(reviewedBy);
            response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(approvedBy);
            response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(processedBy);
            if (statusMapping != null)
            {
                response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(statusMapping.Status);
                response.NextStatus = _mapper.Map<List<ExpensesStatusMasterMongoDB>>(statusMapping.NextStatus);
            }
            if (response.Status == null)
            {
                var status = statusTask.Result;
                response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(status);
            }
            response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(paymentMode);
            response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(expenseType);
            if (billAttachment != null) response.Documents = billAttachment.Documents;

            return response;

        }


        /// <summary>
        /// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
        /// </summary>
        /// <param name="filter">The JSON filter string from the request.</param>
        /// <returns>An <see cref="ExpensesFilter"/> object or null if deserialization fails.</returns>
        private ExpensesFilter? TryDeserializeFilter(string? filter)
        {
            if (string.IsNullOrWhiteSpace(filter))
            {
                return null;
            }

            var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
            ExpensesFilter? expenseFilter = null;

            try
            {
                // First, try to deserialize directly. This is the expected case (e.g., from a web client).
                expenseFilter = JsonSerializer.Deserialize<ExpensesFilter>(filter, options);
            }
            catch (JsonException ex)
            {
                _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter);

                // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
                try
                {
                    // Unescape the string first, then deserialize the result.
                    string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
                    if (!string.IsNullOrWhiteSpace(unescapedJsonString))
                    {
                        expenseFilter = JsonSerializer.Deserialize<ExpensesFilter>(unescapedJsonString, options);
                    }
                }
                catch (JsonException ex1)
                {
                    // If both attempts fail, log the final error and return null.
                    _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
                    return null;
                }
            }
            return expenseFilter;
        }

        /// <summary>
        /// Processes and uploads attachments concurrently, then adds the resulting entities to the main DbContext.
        /// </summary>
        private async Task ProcessAndUploadAttachmentsAsync(IEnumerable<FileUploadModel> attachments, Expenses expense, Guid employeeId, Guid tenantId)
        {
            // Pre-validate all attachments to fail fast before any uploads.
            foreach (var attachment in attachments)
            {
                if (string.IsNullOrWhiteSpace(attachment.Base64Data) || !_s3Service.IsBase64String(attachment.Base64Data))
                {
                    throw new ArgumentException($"Invalid or missing Base64 data for attachment: {attachment.FileName ?? "N/A"}");
                }
            }

            var batchId = Guid.NewGuid();

            // Create a list of tasks to be executed concurrently.
            var processingTasks = attachments.Select(attachment =>
                ProcessSingleAttachmentAsync(attachment, expense, employeeId, tenantId, batchId)
            ).ToList();

            var results = await Task.WhenAll(processingTasks);

            // This part is thread-safe as it runs after all concurrent tasks are complete.
            foreach (var (document, billAttachment) in results)
            {
                _context.Documents.Add(document);
                _context.BillAttachments.Add(billAttachment);
            }
            _logger.LogInfo("{AttachmentCount} attachments processed and staged for saving.", results.Length);
        }

        /// <summary>
        /// Handles the logic for a single attachment: upload to S3 and create corresponding entities.
        /// </summary>
        private async Task<(Document document, BillAttachments billAttachment)> ProcessSingleAttachmentAsync(
            FileUploadModel attachment, Expenses expense, Guid employeeId, Guid tenantId, Guid batchId)
        {
            var base64Data = attachment.Base64Data!.Contains(',') ? attachment.Base64Data[(attachment.Base64Data.IndexOf(",") + 1)..] : attachment.Base64Data;
            var fileType = _s3Service.GetContentTypeFromBase64(base64Data);
            var fileName = _s3Service.GenerateFileName(fileType, expense.Id, "Expense");
            var objectKey = $"tenant-{tenantId}/project-{expense.ProjectId}/Expenses/{fileName}";

            // Await the I/O-bound upload operation directly.
            await _s3Service.UploadFileAsync(base64Data, fileType, objectKey);
            _logger.LogInfo("Uploaded file to S3 with key: {ObjectKey}", objectKey);

            return CreateAttachmentEntities(batchId, expense.Id, employeeId, tenantId, objectKey, attachment);
        }


        /// <summary>
        /// A private static helper method to create Document and BillAttachment entities.
        /// This remains unchanged as it's a pure data-shaping method.
        /// </summary>
        private static (Document document, BillAttachments billAttachment) CreateAttachmentEntities(
            Guid batchId, Guid expenseId, Guid uploadedById, Guid tenantId, string s3Key, FileUploadModel attachmentDto)
        {
            var document = new Document
            {
                BatchId = batchId,
                UploadedById = uploadedById,
                FileName = attachmentDto.FileName ?? "",
                ContentType = attachmentDto.ContentType ?? "",
                S3Key = s3Key,
                FileSize = attachmentDto.FileSize,
                UploadedAt = DateTime.UtcNow,
                TenantId = tenantId
            };
            var billAttachment = new BillAttachments { Document = document, ExpensesId = expenseId, TenantId = tenantId };
            return (document, billAttachment);
        }

        private async Task DeleteAttachemnts(List<Guid> documentIds)
        {
            var attachmentTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                var attachments = await dbContext.BillAttachments.AsNoTracking().Where(ba => documentIds.Contains(ba.DocumentId)).ToListAsync();

                dbContext.BillAttachments.RemoveRange(attachments);
                await dbContext.SaveChangesAsync();
            });
            var documentsTask = Task.Run(async () =>
            {
                await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
                var documents = await dbContext.Documents.AsNoTracking().Where(ba => documentIds.Contains(ba.Id)).ToListAsync();

                if (documents.Any())
                {
                    dbContext.Documents.RemoveRange(documents);
                    await dbContext.SaveChangesAsync();

                    List<S3DeletionObject> deletionObject = new List<S3DeletionObject>();
                    foreach (var document in documents)
                    {
                        deletionObject.Add(new S3DeletionObject
                        {
                            Key = document.S3Key
                        });
                        if (!string.IsNullOrWhiteSpace(document.ThumbS3Key) && document.ThumbS3Key != document.S3Key)
                        {
                            deletionObject.Add(new S3DeletionObject
                            {
                                Key = document.ThumbS3Key
                            });
                        }
                    }
                    await _updateLogHelper.PushToS3DeletionAsync(deletionObject);
                }
            });

            await Task.WhenAll(attachmentTask, documentsTask);
        }

        #endregion
    }
}
