using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Dtos.Mail;
using Marco.Pms.Model.Mail;
using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System.Data;
using System.Globalization;
using System.Net.Mail;

namespace Marco.Pms.Services.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [Authorize]
    public class ReportController : ControllerBase
    {
        private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
        private readonly IServiceScopeFactory _serviceScopeFactory;
        private readonly ApplicationDbContext _context;
        private readonly ILoggingService _logger;
        private readonly UserHelper _userHelper;
        private readonly IConfiguration _configuration;
        public ReportController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
            ApplicationDbContext context,
            ILoggingService logger,
            UserHelper userHelper,
            IConfiguration configuration,
            IServiceScopeFactory serviceScopeFactory)
        {
            _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
            _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
            _context = context ?? throw new ArgumentNullException(nameof(context));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
            _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
        }

        /// <summary>
        /// Adds new mail details for a project report.
        /// </summary>
        /// <param name="mailDetailsDto">The mail details data.</param>
        /// <returns>An API response indicating success or failure.</returns>
        [HttpPost("mail-details")] // More specific route for adding mail details
        public async Task<IActionResult> AddMailDetails([FromBody] MailDetailsDto mailDetailsDto)
        {
            // 1. Get Tenant ID and Basic Authorization Check
            Guid tenantId = _userHelper.GetTenantId();
            if (tenantId == Guid.Empty)
            {
                _logger.LogWarning("Authorization Error: Attempt to add mail details with an empty or invalid tenant ID.");
                return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401));
            }

            // 2. Input Validation (Leverage Model Validation attributes on DTO)
            if (mailDetailsDto == null)
            {
                _logger.LogWarning("Validation Error: MailDetails DTO is null. TenantId: {TenantId}", tenantId);
                return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Data", "Request body is empty.", 400));
            }

            // Ensure ProjectId and Recipient are not empty
            if (mailDetailsDto.ProjectId == Guid.Empty)
            {
                _logger.LogWarning("Validation Error: Project ID is empty. TenantId: {TenantId}", tenantId);
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Project ID cannot be empty.", 400));
            }

            if (string.IsNullOrWhiteSpace(mailDetailsDto.Recipient))
            {
                _logger.LogWarning("Validation Error: Recipient email is empty. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.ProjectId, tenantId);
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Recipient email cannot be empty.", 400));
            }

            // Optional: Validate email format using regex or System.Net.Mail.MailAddress
            try
            {
                var mailAddress = new MailAddress(mailDetailsDto.Recipient);
                _logger.LogInfo("nothing");
            }
            catch (FormatException)
            {
                _logger.LogWarning("Validation Error: Invalid recipient email format '{Recipient}'. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.Recipient, mailDetailsDto.ProjectId, tenantId);
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Invalid recipient email format.", 400));
            }

            // 3. Validate MailListId (Foreign Key Check)
            // Ensure the MailListId refers to an existing MailBody (template) within the same tenant.
            if (mailDetailsDto.MailListId != Guid.Empty) // Only validate if a MailListId is provided
            {
                bool mailTemplateExists;
                try
                {
                    mailTemplateExists = await _context.MailingList
                        .AsNoTracking()
                        .AnyAsync(m => m.Id == mailDetailsDto.MailListId && m.TenantId == tenantId);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}.", mailDetailsDto.MailListId, tenantId);
                    return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while validating mail template.", 500));
                }

                if (!mailTemplateExists)
                {
                    _logger.LogWarning("Validation Error: Provided MailListId '{MailListId}' does not exist or does not belong to TenantId: {TenantId}.", mailDetailsDto.MailListId, tenantId);
                    return NotFound(ApiResponse<object>.ErrorResponse("Invalid Mail Template", "The specified mail template (MailListId) was not found or accessible.", 404));
                }
            }
            // If MailListId can be null/empty and implies no specific template, adjust logic accordingly.
            // Currently assumes it must exist if provided.

            // 4. Create and Add New Mail Details
            var newMailDetails = new MailDetails
            {
                ProjectId = mailDetailsDto.ProjectId,
                Recipient = mailDetailsDto.Recipient,
                Schedule = mailDetailsDto.Schedule,
                MailListId = mailDetailsDto.MailListId,
                TenantId = tenantId,
            };

            try
            {
                _context.MailDetails.Add(newMailDetails);
                await _context.SaveChangesAsync();
                _logger.LogInfo("Successfully added new mail details with ID {MailDetailsId} for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.Id, newMailDetails.ProjectId, newMailDetails.Recipient, tenantId);

                // 5. Return Success Response (201 Created is ideal for resource creation)
                return StatusCode(201, ApiResponse<MailDetails>.SuccessResponse(
                    newMailDetails, // Return the newly created object (or a DTO of it)
                    "Mail details added successfully.",
                    201));
            }
            catch (DbUpdateException dbEx)
            {
                _logger.LogError(dbEx, "Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId);
                // Check for specific constraint violations if applicable (e.g., duplicate recipient for a project)
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while saving the mail details.", 500));
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId);
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500));
            }
        }

        /// <summary>
        /// Adds a new mail template.
        /// </summary>
        /// <param name="mailTemplateDto">The mail template data.</param>
        /// <returns>An API response indicating success or failure.</returns>
        [HttpPost("mail-template")] // More specific route for adding a template
        public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemplateDto) // Renamed parameter for consistency
        {
            // 1. Get Tenant ID and Basic Authorization Check
            Guid tenantId = _userHelper.GetTenantId();
            if (tenantId == Guid.Empty)
            {
                _logger.LogWarning("Authorization Error: Attempt to add mail template with an empty or invalid tenant ID.");
                return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401));
            }

            // 2. Input Validation (Moved to model validation if possible, or keep explicit)
            // Use proper model validation attributes ([Required], [StringLength]) on MailTemeplateDto
            // and rely on ASP.NET Core's automatic model validation if possible.
            // If not, these checks are good.
            if (mailTemplateDto == null)
            {
                _logger.LogWarning("Validation Error: Mail template DTO is null.");
                return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Data", "Request body is empty.", 400));
            }

            if (string.IsNullOrWhiteSpace(mailTemplateDto.Title))
            {
                _logger.LogWarning("Validation Error: Mail template title is empty or whitespace. TenantId: {TenantId}", tenantId);
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Mail template title cannot be empty.", 400));
            }

            // The original logic checked both body and title, but often a template needs at least a title.
            // Re-evalute if body can be empty. If so, remove the body check. Assuming title is always mandatory.
            // If both body and title are empty, it's definitely invalid.
            if (string.IsNullOrWhiteSpace(mailTemplateDto.Body) && string.IsNullOrWhiteSpace(mailTemplateDto.Subject))
            {
                _logger.LogWarning("Validation Error: Mail template body and subject are both empty or whitespace for title '{Title}'. TenantId: {TenantId}", mailTemplateDto.Title, tenantId);
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Mail template body or subject must be provided.", 400));
            }

            // 3. Check for Existing Template Title (Case-Insensitive)
            // Use AsNoTracking() for read-only query
            MailingList? existingTemplate;
            try
            {
                existingTemplate = await _context.MailingList
                    .AsNoTracking() // Important for read-only checks
                    .FirstOrDefaultAsync(t => t.Title.ToLower() == mailTemplateDto.Title.ToLower() && t.TenantId == tenantId); // IMPORTANT: Filter by TenantId!
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while checking for existing templates.", 500));
            }


            if (existingTemplate != null)
            {
                _logger.LogWarning("Conflict Error: User tries to add email template with title '{Title}' which already exists for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
                return Conflict(ApiResponse<object>.ErrorResponse("Conflict", $"Email template with title '{mailTemplateDto.Title}' already exists.", 409));
            }

            // 4. Create and Add New Template
            var newMailingList = new MailingList
            {
                Title = mailTemplateDto.Title,
                Body = mailTemplateDto.Body,
                Subject = mailTemplateDto.Subject,
                Keywords = mailTemplateDto.Keywords,
                TenantId = tenantId,
            };

            try
            {
                _context.MailingList.Add(newMailingList);
                await _context.SaveChangesAsync();
                _logger.LogInfo("Successfully added new mail template with ID {TemplateId} and title '{Title}' for TenantId: {TenantId}.", newMailingList.Id, newMailingList.Title, tenantId);

                // 5. Return Success Response (201 Created is ideal for resource creation)
                // It's good practice to return the created resource or its ID.
                return StatusCode(201, ApiResponse<MailingList>.SuccessResponse(
                    newMailingList, // Return the newly created object (or a DTO of it)
                    "Mail template added successfully.",
                    201));
            }
            catch (DbUpdateException dbEx)
            {
                _logger.LogError(dbEx, "Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId);
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while saving the mail template.", 500));
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500));
            }
        }

        [HttpGet("project-statistics")]
        public async Task<IActionResult> SendProjectReport()
        {
            Guid tenantId = _userHelper.GetTenantId();

            // 1. OPTIMIZATION: Perform grouping and projection on the database server.
            // This is far more efficient than loading all entities into memory.
            var projectMailGroups = await _context.MailDetails
                .AsNoTracking()
                .Where(m => m.TenantId == tenantId)
                .GroupBy(m => new { m.ProjectId, m.MailListId })
                .Select(g => new
                {
                    ProjectId = g.Key.ProjectId,
                    Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
                    // Project the mail body and subject from the first record in the group
                    MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault()
                })
                .ToListAsync();

            if (!projectMailGroups.Any())
            {
                return Ok(ApiResponse<object>.SuccessResponse(new { }, "No projects found to send reports for.", 200));
            }

            int successCount = 0;
            int notFoundCount = 0;
            int invalidIdCount = 0;
            int failureCount = 0;

            // 2. OPTIMIZATION: Use true concurrency by removing SemaphoreSlim(1)
            // and giving each task its own isolated set of services (including DbContext).
            var sendTasks = projectMailGroups.Select(async mailGroup =>
            {
                // SOLUTION: Create a new Dependency Injection scope for each parallel task.
                using (var scope = _serviceScopeFactory.CreateScope())
                {
                    // Resolve a new instance of the helper from this isolated scope.
                    // This ensures each task gets its own thread-safe DbContext.
                    var reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();

                    try
                    {
                        // Ensure MailInfo and ProjectId are valid before proceeding
                        if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty)
                        {
                            Interlocked.Increment(ref invalidIdCount);
                            return;
                        }

                        var response = await reportHelper.GetProjectStatistics(
                            mailGroup.ProjectId,
                            mailGroup.Recipients,
                            mailGroup.MailInfo.Body,
                            mailGroup.MailInfo.Subject,
                            tenantId);

                        // Use a switch expression for cleaner counting
                        switch (response.StatusCode)
                        {
                            case 200: Interlocked.Increment(ref successCount); break;
                            case 404: Interlocked.Increment(ref notFoundCount); break;
                            case 400: Interlocked.Increment(ref invalidIdCount); break;
                            default: Interlocked.Increment(ref failureCount); break;
                        }
                    }
                    catch (Exception ex)
                    {
                        // 3. OPTIMIZATION: Make the process resilient.
                        // If one task fails unexpectedly, log it and continue with others.
                        _logger.LogError(ex, "Failed to send report for project {ProjectId}", mailGroup.ProjectId);
                        Interlocked.Increment(ref failureCount);
                    }
                }
            }).ToList();

            await Task.WhenAll(sendTasks);

            var summaryMessage = $"Processing complete. Success: {successCount}, Not Found: {notFoundCount}, Invalid ID: {invalidIdCount}, Failures: {failureCount}.";

            _logger.LogInfo(
                "Project report sending complete for tenant {TenantId}. Success: {SuccessCount}, Not Found: {NotFoundCount}, Invalid ID: {InvalidIdCount}, Failures: {FailureCount}",
                tenantId, successCount, notFoundCount, invalidIdCount, failureCount);

            return Ok(ApiResponse<object>.SuccessResponse(
                new { successCount, notFoundCount, invalidIdCount, failureCount },
                summaryMessage,
                200));
        }

        [HttpPost("add-report-mail")]
        public async Task<IActionResult> StoreProjectStatistics()
        {
            Guid tenantId = _userHelper.GetTenantId();

            // 1. Database-Side Grouping (Still the most efficient way to get initial data)
            var projectMailGroups = await _context.MailDetails
                .AsNoTracking()
                .Where(m => m.TenantId == tenantId && m.ProjectId != Guid.Empty)
                .GroupBy(m => new { m.ProjectId, m.MailListId })
                .Select(g => new
                {
                    g.Key.ProjectId,
                    Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
                    MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault()
                })
                .ToListAsync();

            if (!projectMailGroups.Any())
            {
                _logger.LogInfo("No project mail details found for tenant {TenantId} to process.", tenantId);
                return Ok(ApiResponse<object>.SuccessResponse("No project reports to generate.", "No project reports to generate.", 200));
            }

            string env = _configuration["environment:Title"] ?? string.Empty;

            // 2. Process each group concurrently, but with isolated DBContexts.
            var processingTasks = projectMailGroups.Select(async group =>
            {
                // SOLUTION: Create a new DI scope for each parallel task.
                using (var scope = _serviceScopeFactory.CreateScope())
                {
                    // Resolve services from this new, isolated scope.
                    // These helpers will get their own fresh DbContext instance.
                    var reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
                    var emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
                    var cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>(); // e.g., IProjectReportCache

                    // The rest of the logic is the same, but now it's thread-safe.
                    try
                    {
                        var projectId = group.ProjectId;
                        var statisticReport = await reportHelper.GetDailyProjectReport(projectId, tenantId);

                        if (statisticReport == null)
                        {
                            _logger.LogWarning("Statistic report for project ID {ProjectId} not found. Skipping.", projectId);
                            return;
                        }

                        if (group.MailInfo == null)
                        {
                            _logger.LogWarning("MailBody info for project ID {ProjectId} not found. Skipping.", projectId);
                            return;
                        }

                        var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture);
                        // Assuming the first param to SendProjectStatisticsEmail was just a placeholder
                        var emailBody = await emailSender.SendProjectStatisticsEmail(new List<string>(), group.MailInfo.Body, string.Empty, statisticReport);

                        string subject = group.MailInfo.Subject
                            .Replace("{{DATE}}", date)
                            .Replace("{{PROJECT_NAME}}", statisticReport.ProjectName);

                        subject = string.IsNullOrWhiteSpace(env) ? subject : $"({env}) {subject}";

                        var mail = new ProjectReportEmailMongoDB
                        {
                            IsSent = false,
                            Body = emailBody,
                            Receivers = group.Recipients,
                            Subject = subject,
                        };

                        await cache.AddProjectReportMail(mail);
                    }
                    catch (Exception ex)
                    {
                        // It's good practice to log any unexpected errors within a concurrent task.
                        _logger.LogError(ex, "Failed to process project report for ProjectId {ProjectId}", group.ProjectId);
                    }
                }
            });

            // Await all the concurrent, now thread-safe, tasks.
            await Task.WhenAll(processingTasks);

            return Ok(ApiResponse<object>.SuccessResponse(
                $"{projectMailGroups.Count} Project Report Mail(s) are queued for storage.",
                "Project Report Mail processing initiated.",
                200));
        }

        [HttpGet("report-mail")]
        public async Task<IActionResult> GetProjectStatisticsFromCache()
        {
            using var scope = _serviceScopeFactory.CreateScope();
            var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();

            var mailList = await _cache.GetProjectReportMail(false);
            if (mailList == null)
            {
                return NotFound(ApiResponse<object>.ErrorResponse("Not mail found", "Not mail found", 404));
            }

            return Ok(ApiResponse<object>.SuccessResponse(mailList, "Fetched list of mail body successfully", 200));
        }

        [HttpGet("report-attendance")]
        public async Task<IActionResult> GetAttendanceReportAsync([FromQuery] bool isCurrentMonth = false)
        {
            Guid tenantId = _userHelper.GetTenantId();
            using var scope = _serviceScopeFactory.CreateScope();

            DateTime today = DateTime.Today;
            DateTime firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
            DateTime firstDayOfNextMonth = firstDayOfMonth.AddMonths(1);

            if (!isCurrentMonth)
            {
                firstDayOfNextMonth = firstDayOfMonth;
                firstDayOfMonth = firstDayOfMonth.AddMonths(-1);
            }

            // Generate list of all dates in the month
            var allDates = Enumerable.Range(0, (firstDayOfNextMonth - firstDayOfMonth).Days)
                                     .Select(offset => firstDayOfMonth.AddDays(offset))
                                     .ToList();

            var attendancesTask = Task.Run(async () =>
            {
                await using var context = await _dbContextFactory.CreateDbContextAsync();
                return await context.Attendes
                    .Where(a => a.AttendanceDate >= firstDayOfMonth && a.AttendanceDate < firstDayOfNextMonth && a.Employee != null && a.TenantId == tenantId)
                    .GroupBy(a => a.ProjectID)
                    .ToListAsync();
            });

            var projectAllocationTask = Task.Run(async () =>
            {
                await using var context = await _dbContextFactory.CreateDbContextAsync();
                return await context.ProjectAllocations
                    .Include(pa => pa.Employee)
                    .Where(pa => pa.TenantId == tenantId && pa.IsActive)
                    .ToListAsync();
            });

            await Task.WhenAll(attendancesTask, projectAllocationTask);

            var attendances = attendancesTask.Result;
            var projectAllocations = projectAllocationTask.Result;

            var result = attendances.Select(g =>
            {
                var projectAllocation = projectAllocations.Where(pa => pa.ProjectId == g.Key && pa.Employee != null).ToList();
                var projectAttendance = projectAllocation.Select(pa =>
                {
                    var attendances = g.Where(a => a.EmployeeId == pa.EmployeeId).ToList();
                    var attendanceDate = attendances.Select(a => a.AttendanceDate.Date).ToList();
                    return new
                    {
                        FirstName = pa.Employee!.FirstName,
                        LastName = pa.Employee.LastName,
                        Attendances = allDates.Select(d =>
                        {
                            var attendance = attendances.FirstOrDefault(a => a.AttendanceDate.Date == d);
                            return new
                            {
                                AttendanceDate = d,
                                CheckIn = attendance?.InTime,
                                CheckOut = attendance?.OutTime,
                                Activity = attendance?.Activity,
                                IsApproved = attendance?.ApprovedById.HasValue,
                            };
                        }).ToList(),
                        CheckInCheckOutDone = attendances.Where(a => a.InTime.HasValue && a.OutTime.HasValue && a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE).Count(),
                        CheckInDone = attendances.Where(a => a.InTime.HasValue).Count(),
                        CheckOutPending = attendances.Where(a => a.InTime.HasValue && !a.OutTime.HasValue).Count(),
                        RejectedRegularize = attendances.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT).Count(),
                        AbsentAttendance = allDates.Where(d => !attendanceDate.Contains(d) && d.DayOfWeek != DayOfWeek.Sunday).Count()
                    };
                }).OrderBy(ar => ar.FirstName).ThenBy(ar => ar.LastName).ToList();

                return new
                {
                    ProjectName = _context.Projects.Where(p => p.Id == g.Key && p.TenantId == tenantId).Select(p => p.Name).FirstOrDefault(),
                    ProjectAttendance = projectAttendance
                };
            }).ToList();

            var response = result.OrderBy(r => r.ProjectName).ToList();

            return Ok(ApiResponse<object>.SuccessResponse(response, "Attendance Report fetched successfully", 200));
        }
    }
}
