using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Application.DTOs.Job;
using PowderCoating.Application.Services;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
using PowderCoating.Web.Hubs;

namespace PowderCoating.Web.Controllers;

[Authorize(Policy = AppConstants.Policies.CanManageJobs)]
public class JobsPriorityController : Controller
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger<JobsPriorityController> _logger;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ITenantContext _tenantContext;
    private readonly IHubContext<ShopHub> _shopHub;

    public JobsPriorityController(
        IUnitOfWork unitOfWork,
        ILogger<JobsPriorityController> logger,
        UserManager<ApplicationUser> userManager,
        ITenantContext tenantContext,
        IHubContext<ShopHub> shopHub)
    {
        _unitOfWork = unitOfWork;
        _logger = logger;
        _userManager = userManager;
        _tenantContext = tenantContext;
        _shopHub = shopHub;
    }

    /// <summary>
    /// Renders the daily job-scheduling board for the given date (defaults to today).
    /// <para>
    /// Loads all jobs whose <c>ScheduledDate</c> matches the requested day, then
    /// merges them with any existing <c>JobDailyPriority</c> drag-drop order records.
    /// Jobs that have not yet been given an explicit display order receive
    /// <c>int.MaxValue</c> so they sort to the bottom, after already-ordered jobs.
    /// </para>
    /// <para>
    /// Maintenance records for the same day (status Scheduled or InProgress) are
    /// also loaded and passed via <c>ViewBag.MaintenanceItems</c> so the view can
    /// render a separate maintenance panel on the same board without a second round-trip.
    /// Priority and worker option lists are serialised to JSON ViewBag properties for
    /// use by the inline-edit JavaScript modals.
    /// </para>
    /// </summary>
    // GET: JobsPriority (Job Schedule)
    public async Task<IActionResult> Index(DateTime? date)
    {
        var today = date?.Date ?? DateTime.Today;

        // Get all jobs scheduled for today with related data
        var jobs = await _unitOfWork.Jobs.GetScheduledJobsForDateAsync(today);

        // Get existing priority records for today
        var existingPriorities = await _unitOfWork.JobDailyPriorities
            .FindAsync(p => p.ScheduledDate.Date == today);

        var priorityDict = existingPriorities.ToDictionary(p => p.JobId);

        // Map to DTOs with display order
        var jobDtos = jobs.Select(j => new JobDailyPriorityDto
        {
            Id = priorityDict.ContainsKey(j.Id) ? priorityDict[j.Id].Id : 0,
            JobId = j.Id,
            JobNumber = j.JobNumber,
            CustomerName = j.Customer.CompanyName ?? $"{j.Customer.ContactFirstName} {j.Customer.ContactLastName}".Trim(),
            StatusDisplayName = j.JobStatus.DisplayName,
            StatusColorClass = j.JobStatus.ColorClass,
            JobPriorityId = j.JobPriorityId,
            PriorityDisplayName = j.JobPriority.DisplayName,
            PriorityColorClass = j.JobPriority.ColorClass,
            AssignedUserId = j.AssignedUserId,
            AssignedWorkerName = j.AssignedUser?.FullName,
            ScheduledDate = j.ScheduledDate,
            DueDate = j.DueDate,
            DisplayOrder = priorityDict.ContainsKey(j.Id) ? priorityDict[j.Id].DisplayOrder : int.MaxValue
        })
        .OrderBy(j => j.DisplayOrder)
        .ThenBy(j => j.JobNumber)
        .ToList();

        // Get priorities and workers for modal options
        var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
        var priorities = await _unitOfWork.JobPriorityLookups.FindAsync(p => p.CompanyId == companyId);
        var workers = await _userManager.Users
            .Where(u => u.CompanyId == companyId && u.IsActive && u.CompanyRole != null)
            .OrderBy(u => u.FirstName).ThenBy(u => u.LastName)
            .ToListAsync();

        // Get maintenance records scheduled for today (Scheduled or InProgress)
        var maintenanceItems = (await _unitOfWork.MaintenanceRecords.FindAsync(
                m => m.ScheduledDate.Date == today &&
                     (m.Status == MaintenanceStatus.Scheduled || m.Status == MaintenanceStatus.InProgress),
                false,
                m => m.Equipment, m => m.AssignedUser))
            .OrderByDescending(m => (int)m.Priority)
            .ThenBy(m => m.ScheduledDate)
            .ToList();

        // Load overdue jobs only when viewing today — past-date navigation shows that day as-is
        if (today == DateTime.Today)
        {
            var overdueJobs = await _unitOfWork.Jobs.GetOverdueScheduledJobsAsync();
            ViewBag.OverdueJobs = overdueJobs.Select(j => new JobDailyPriorityDto
            {
                Id = 0,
                JobId = j.Id,
                JobNumber = j.JobNumber,
                CustomerName = j.Customer.CompanyName ?? $"{j.Customer.ContactFirstName} {j.Customer.ContactLastName}".Trim(),
                StatusDisplayName = j.JobStatus.DisplayName,
                StatusColorClass = j.JobStatus.ColorClass,
                JobPriorityId = j.JobPriorityId,
                PriorityDisplayName = j.JobPriority.DisplayName,
                PriorityColorClass = j.JobPriority.ColorClass,
                AssignedUserId = j.AssignedUserId,
                AssignedWorkerName = j.AssignedUser?.FullName,
                ScheduledDate = j.ScheduledDate,
                DueDate = j.DueDate,
                DisplayOrder = int.MaxValue
            }).ToList();
        }

        ViewBag.ScheduledDate = today;
        ViewBag.MaintenanceItems = maintenanceItems;
        ViewBag.PrioritiesJson = priorities.OrderBy(p => p.DisplayOrder)
            .Select(p => new { id = p.Id, name = p.DisplayName, colorClass = p.ColorClass })
            .ToList();
        ViewBag.WorkersJson = workers.OrderBy(w => w.FirstName).ThenBy(w => w.LastName)
            .Select(w => new { id = w.Id, name = w.FullName })
            .ToList();

        return View(jobDtos);
    }

    /// <summary>
    /// Persists the new drag-drop display order for jobs on the daily scheduling board.
    /// Called via AJAX after the user reorders cards; returns JSON so the page does not
    /// reload.
    /// <para>
    /// For each job in the payload the action either updates the existing
    /// <c>JobDailyPriority</c> row for today or creates a new one if the job has not
    /// been ordered before. Using an upsert-style approach (check dictionary → update or
    /// insert) avoids a separate EXISTS query per row while keeping the logic clear.
    /// After saving, a <c>DailyBoardUpdated</c> SignalR event is pushed to the
    /// company's shop-hub group so any connected shop-floor screen refreshes
    /// automatically without polling.
    /// </para>
    /// </summary>
    // POST: JobsPriority/UpdateOrder
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UpdateOrder([FromBody] List<UpdateJobOrderDto> updates)
    {
        try
        {
            if (updates == null || !updates.Any())
            {
                return Json(new { success = false, message = "No updates provided" });
            }

            var today = DateTime.Today;

            // Get all existing priority records for today
            var existingPriorities = await _unitOfWork.JobDailyPriorities
                .FindAsync(p => p.ScheduledDate.Date == today);

            var priorityDict = existingPriorities.ToDictionary(p => p.JobId);

            // Update or create priority records
            foreach (var update in updates)
            {
                if (priorityDict.ContainsKey(update.JobId))
                {
                    // Update existing record
                    var priority = priorityDict[update.JobId];
                    priority.DisplayOrder = update.DisplayOrder;
                }
                else
                {
                    // Create new record
                    var newPriority = new JobDailyPriority
                    {
                        JobId = update.JobId,
                        ScheduledDate = today,
                        DisplayOrder = update.DisplayOrder
                    };
                    await _unitOfWork.JobDailyPriorities.AddAsync(newPriority);
                }
            }

            await _unitOfWork.CompleteAsync();

            var companyId = _tenantContext.GetCurrentCompanyId()?.ToString();
            if (!string.IsNullOrEmpty(companyId))
                await _shopHub.Clients.Group($"shop-{companyId}").SendAsync("DailyBoardUpdated");

            return Json(new { success = true, message = "Job order updated successfully" });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error updating job order");
            return Json(new { success = false, message = "Failed to update job order" });
        }
    }

    /// <summary>
    /// Changes the urgency priority of a job directly from the scheduling board via
    /// an inline-edit modal, without navigating to the full Job Edit page.
    /// <para>
    /// <c>JobPriority</c> is a lookup-table entity (<c>JobPriorityLookup</c>), not an
    /// enum, so the action validates that <paramref name="priorityId"/> maps to a known
    /// lookup row before writing it to the job. The resolved
    /// <c>DisplayName</c> and <c>ColorClass</c> are returned in the JSON response so
    /// the JavaScript can update the board badge in-place without reloading.
    /// A SignalR <c>DailyBoardUpdated</c> push notifies other connected clients.
    /// </para>
    /// </summary>
    // POST: JobsPriority/UpdatePriority
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UpdatePriority(int jobId, int priorityId)
    {
        try
        {
            var job = await _unitOfWork.Jobs.GetByIdAsync(jobId);
            if (job == null)
            {
                return Json(new { success = false, message = "Job not found" });
            }

            var priority = await _unitOfWork.JobPriorityLookups.GetByIdAsync(priorityId);
            if (priority == null)
            {
                return Json(new { success = false, message = "Priority not found" });
            }

            job.JobPriorityId = priorityId;
            await _unitOfWork.Jobs.UpdateAsync(job);
            await _unitOfWork.CompleteAsync();

            var companyId = _tenantContext.GetCurrentCompanyId()?.ToString();
            if (!string.IsNullOrEmpty(companyId))
                await _shopHub.Clients.Group($"shop-{companyId}").SendAsync("DailyBoardUpdated");

            return Json(new {
                success = true,
                message = "Priority updated successfully",
                displayName = priority.DisplayName,
                colorClass = priority.ColorClass
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error updating job priority");
            return Json(new { success = false, message = "Failed to update priority" });
        }
    }

    /// <summary>
    /// Assigns or unassigns a worker (ASP.NET Identity user) to a job from the scheduling board.
    /// Passing <c>null</c> or an empty <paramref name="workerId"/> clears the assignment.
    /// <para>
    /// Worker lookup is done via <c>UserManager</c> (not the repository layer) because
    /// workers are stored as Identity users, not as <c>ShopWorker</c> entities.
    /// The resolved <c>FullName</c> ("Unassigned" when cleared) is echoed back in JSON
    /// so the board card updates immediately without a page reload.
    /// A SignalR push keeps other open boards in sync.
    /// </para>
    /// </summary>
    // POST: JobsPriority/UpdateWorker
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UpdateWorker(int jobId, string? workerId)
    {
        try
        {
            var job = await _unitOfWork.Jobs.GetByIdAsync(jobId);
            if (job == null)
            {
                return Json(new { success = false, message = "Job not found" });
            }

            string workerName = "Unassigned";
            if (!string.IsNullOrEmpty(workerId))
            {
                var user = await _userManager.FindByIdAsync(workerId);
                if (user == null)
                {
                    return Json(new { success = false, message = "User not found" });
                }
                workerName = user.FullName;
                job.AssignedUserId = workerId;
            }
            else
            {
                job.AssignedUserId = null;
            }

            await _unitOfWork.Jobs.UpdateAsync(job);
            await _unitOfWork.CompleteAsync();

            var companyId = _tenantContext.GetCurrentCompanyId()?.ToString();
            if (!string.IsNullOrEmpty(companyId))
                await _shopHub.Clients.Group($"shop-{companyId}").SendAsync("DailyBoardUpdated");

            return Json(new {
                success = true,
                message = "Worker assigned successfully",
                workerName = workerName
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error updating job worker assignment");
            return Json(new { success = false, message = "Failed to update worker assignment" });
        }
    }

    /// <summary>
    /// Updates the <c>ScheduledDate</c> on a job inline from the scheduling board.
    /// Moving a job to a different date will cause it to disappear from the current
    /// day's board view on the next render — this is intentional behaviour.
    /// <para>
    /// Returns both a human-readable formatted date ("Jan 15, 2026") and an ISO raw
    /// value ("2026-01-15") in the JSON response so the JavaScript can update the
    /// display label and any hidden form inputs simultaneously.
    /// This action does NOT push a SignalR notification because rescheduling a job
    /// affects a different day's board, not the currently viewed board.
    /// </para>
    /// </summary>
    // POST: JobsPriority/UpdateScheduledDate
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UpdateScheduledDate(int jobId, DateTime? scheduledDate)
    {
        try
        {
            var job = await _unitOfWork.Jobs.GetByIdAsync(jobId);
            if (job == null)
            {
                return Json(new { success = false, message = "Job not found" });
            }

            job.ScheduledDate = scheduledDate;
            await _unitOfWork.Jobs.UpdateAsync(job);
            await _unitOfWork.CompleteAsync();

            return Json(new {
                success = true,
                message = "Scheduled date updated successfully",
                scheduledDate = scheduledDate?.ToString("MMM dd, yyyy"),
                scheduledDateRaw = scheduledDate?.ToString("yyyy-MM-dd")
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error updating job scheduled date");
            return Json(new { success = false, message = "Failed to update scheduled date" });
        }
    }

    /// <summary>
    /// Assigns or unassigns a worker on a maintenance record directly from the
    /// scheduling board's maintenance panel.
    /// <para>
    /// Because this action uses <c>DbContext.FindAsync</c> (which bypasses EF global
    /// query filters), a manual company-ownership check is performed: if the current
    /// user is not a SuperAdmin, the record's <c>CompanyId</c> must match the tenant
    /// context to prevent cross-tenant data mutation. This guard is intentional and
    /// must not be removed.
    /// The raw <c>ApplicationDbContext</c> is used instead of <c>IUnitOfWork</c> here
    /// because the maintenance repository does not expose <c>FindAsync(int)</c> with
    /// the bypass semantics needed for this cross-filter lookup.
    /// </para>
    /// </summary>
    // POST: JobsPriority/UpdateMaintenanceWorker
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UpdateMaintenanceWorker(int maintenanceId, string? workerId)
    {
        try
        {
            var record = await _unitOfWork.MaintenanceRecords.GetByIdAsync(maintenanceId);
            if (record == null || record.IsDeleted)
                return Json(new { success = false, message = "Maintenance record not found" });

            string workerName = "Unassigned";
            if (!string.IsNullOrEmpty(workerId))
            {
                var user = await _userManager.FindByIdAsync(workerId);
                if (user == null)
                    return Json(new { success = false, message = "User not found" });

                workerName = user.FullName;
                record.AssignedUserId = workerId;
            }
            else
            {
                record.AssignedUserId = null;
            }

            record.UpdatedAt = DateTime.UtcNow;
            await _unitOfWork.CompleteAsync();

            return Json(new { success = true, message = "Worker assigned successfully", workerName });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error updating maintenance worker assignment");
            return Json(new { success = false, message = "Failed to update worker assignment" });
        }
    }

    /// <summary>
    /// Updates the customer-facing <c>DueDate</c> on a job from the scheduling board.
    /// Unlike <c>ScheduledDate</c> (the internal work date), <c>DueDate</c> is the
    /// date promised to the customer and is used to calculate overdue status.
    /// <para>
    /// Returns an <c>isOverdue</c> flag in JSON (true when the new date is in the past)
    /// so the JavaScript can immediately toggle the overdue badge colour on the board
    /// card without waiting for a reload.
    /// </para>
    /// </summary>
    // POST: JobsPriority/UpdateDueDate
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UpdateDueDate(int jobId, DateTime? dueDate)
    {
        try
        {
            var job = await _unitOfWork.Jobs.GetByIdAsync(jobId);
            if (job == null)
            {
                return Json(new { success = false, message = "Job not found" });
            }

            job.DueDate = dueDate;
            await _unitOfWork.Jobs.UpdateAsync(job);
            await _unitOfWork.CompleteAsync();

            var isOverdue = dueDate.HasValue && dueDate.Value.Date < DateTime.Today;

            return Json(new {
                success = true,
                message = "Due date updated successfully",
                dueDate = dueDate?.ToString("MMM dd, yyyy"),
                dueDateRaw = dueDate?.ToString("yyyy-MM-dd"),
                isOverdue = isOverdue
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error updating job due date");
            return Json(new { success = false, message = "Failed to update due date" });
        }
    }
}
