using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using PowderCoating.Application.DTOs.Scheduling;
using PowderCoating.Application.Interfaces;
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]
public class OvenSchedulerController : Controller
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IAiSchedulingService _aiSchedulingService;
    private readonly ILogger<OvenSchedulerController> _logger;
    private readonly IHubContext<ShopHub> _shopHub;
    private readonly ITenantContext _tenantContext;

    /// <summary>
    /// Status codes that identify jobs whose coats are eligible to be loaded into the oven scheduler queue.
    /// These are the shop-floor statuses where physical powder has been applied or surfaces are ready —
    /// coats at these stages are the ones that actually need oven time. Terminal statuses (Curing, Completed, etc.)
    /// are intentionally excluded so completed work never re-appears in the queue.
    /// </summary>
    private static readonly string[] QueueableStatuses =
    {
        AppConstants.StatusCodes.Job.InPreparation, AppConstants.StatusCodes.Job.Sandblasting, AppConstants.StatusCodes.Job.MaskingTaping, AppConstants.StatusCodes.Job.Cleaning, AppConstants.StatusCodes.Job.Coating
    };

    public OvenSchedulerController(
        IUnitOfWork unitOfWork,
        IAiSchedulingService aiSchedulingService,
        ILogger<OvenSchedulerController> logger,
        IHubContext<ShopHub> shopHub,
        ITenantContext tenantContext)
    {
        _unitOfWork = unitOfWork;
        _aiSchedulingService = aiSchedulingService;
        _logger = logger;
        _shopHub = shopHub;
        _tenantContext = tenantContext;
    }

    // ──────────────────────────────────────────────────────────────────────────
    // INDEX — Main scheduler board
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Renders the main oven scheduler board for a given date, showing all named ovens, their
    /// current batches, and the global job queue of unscheduled coats.
    /// Data is intentionally loaded in separate, targeted queries rather than one large join so that
    /// each query can be filtered at the database level — avoiding full-table scans on large datasets.
    /// The <see cref="QueueableStatuses"/> filter ensures only jobs with pending oven work appear in
    /// the queue; <c>scheduledCoatIds</c> is then used to suppress coats that are already in an
    /// active batch so the queue always shows true remaining work.
    /// </summary>
    public async Task<IActionResult> Index(DateTime? date, string goal = "maximize_throughput")
    {
        var scheduledDate = date?.Date ?? DateTime.Today;
        var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;

        // Load active Named Ovens — filter IsActive at database level
        var ovenCosts = (await _unitOfWork.OvenCosts.FindAsync(o => o.CompanyId == companyId && o.IsActive))
            .OrderBy(o => o.DisplayOrder).ThenBy(o => o.Label)
            .ToList();

        // Load batches for the selected date — filter at database level with includes
        var scheduledDateEnd = scheduledDate.AddDays(1);
        var batches = (await _unitOfWork.OvenBatches.FindAsync(
                b => b.CompanyId == companyId && b.ScheduledDate >= scheduledDate && b.ScheduledDate < scheduledDateEnd
                  && b.Status != OvenBatchStatus.Cancelled,
                false,
                b => b.OvenCost, b => b.Items))
            .ToList();

        // Load batch items — filter by batch IDs at database level
        var batchIds = batches.Select(b => b.Id).ToHashSet();
        var allBatchItems = batchIds.Any()
            ? (await _unitOfWork.OvenBatchItems.FindAsync(
                i => batchIds.Contains(i.OvenBatchId),
                false,
                i => i.Job, i => i.JobItem, i => i.JobItemCoat))
              .ToList()
            : new List<OvenBatchItem>();

        // Load customer/status/priority for batch item jobs — filter by job IDs at database level
        var batchJobIds = allBatchItems.Select(i => i.JobId).Distinct().ToHashSet();
        var batchJobs = batchJobIds.Any()
            ? (await _unitOfWork.Jobs.FindAsync(
                j => batchJobIds.Contains(j.Id),
                false,
                j => j.Customer, j => j.JobStatus, j => j.JobPriority))
              .ToDictionary(j => j.Id)
            : new Dictionary<int, Job>();

        // Load jobs in the queue — filter by status at database level
        var queueJobs = (await _unitOfWork.Jobs.FindAsync(
                j => j.CompanyId == companyId && j.JobStatus != null && QueueableStatuses.Contains(j.JobStatus.StatusCode),
                false,
                j => j.Customer, j => j.JobStatus, j => j.JobPriority, j => j.JobItems))
            .ToList();

        // Load coats for queue job items — filter by item IDs at database level
        var queueJobItemIds = queueJobs.SelectMany(j => j.JobItems.Select(i => i.Id)).ToHashSet();
        var queueCoats = queueJobItemIds.Any()
            ? (await _unitOfWork.JobItemCoats.FindAsync(
                c => queueJobItemIds.Contains(c.JobItemId),
                false,
                c => c.InventoryItem))
              .ToList()
            : new List<JobItemCoat>();

        // Map coats back to items
        var coatsByItem = queueCoats.GroupBy(c => c.JobItemId).ToDictionary(g => g.Key, g => g.ToList());
        foreach (var job in queueJobs)
        {
            foreach (var item in job.JobItems)
            {
                if (coatsByItem.TryGetValue(item.Id, out var coats))
                    item.Coats = coats;
            }
        }

        // Determine which coats are already scheduled — filter out removed/cancelled at database level
        var scheduledCoatIds = (await _unitOfWork.OvenBatchItems.FindAsync(
                i => i.Status != OvenBatchItemStatus.Removed && i.Batch.Status != OvenBatchStatus.Cancelled,
                false,
                i => i.Batch))
            .Select(i => i.JobItemCoatId)
            .ToHashSet();

        // Get company defaults
        var companyCosts = await _unitOfWork.CompanyOperatingCosts.FirstOrDefaultAsync(c => true);
        var defaultCycleMinutes = companyCosts?.DefaultOvenCycleMinutes ?? 45;

        // Build the view model
        var vm = new OvenSchedulerViewModel
        {
            ScheduledDate = scheduledDate,
            OptimizationGoal = goal,
            DefaultCycleMinutes = defaultCycleMinutes,

            Ovens = ovenCosts.Select(o => new OvenInfoDto
            {
                OvenCostId = o.Id,
                EquipmentId = 0,
                Name = o.Label,
                MaxLoadSqFt = o.MaxLoadSqFt,
                CycleMinutes = o.DefaultCycleMinutes ?? defaultCycleMinutes,
                Status = "Operational",
                IsOperational = true
            }).ToList(),

            Batches = BuildBatchDtos(batches, allBatchItems, batchJobs, ovenCosts),

            QueuedJobs = BuildQueuedJobDtos(queueJobs, scheduledCoatIds)
        };

        return View(vm);
    }

    // ──────────────────────────────────────────────────────────────────────────
    // AI SUGGEST — POST, returns JSON
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Calls the AI scheduling service to suggest optimal oven batches for today's queue and returns
    /// the recommendation as JSON for the client-side scheduler UI.
    /// All queueable jobs with their coats and surface-area data are packaged into a
    /// <see cref="BatchSchedulingRequest"/> and forwarded to <see cref="IAiSchedulingService.SuggestBatchesAsync"/>.
    /// Already-scheduled coat IDs are included so the AI does not re-suggest work that is already
    /// in a planned batch. The response is rendered client-side — no batches are persisted here;
    /// the user must call <see cref="AcceptSuggestion"/> to commit them.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> Suggest([FromBody] SuggestRequest req)
    {
        var goal = req?.OptimizationGoal ?? "maximize_throughput";
        var suggestCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;

        var equipmentList = (await _unitOfWork.OvenCosts.FindAsync(o => o.CompanyId == suggestCompanyId))
            .Where(o => o.IsActive)
            .OrderBy(o => o.DisplayOrder).ThenBy(o => o.Label)
            .ToList();

        if (!equipmentList.Any())
            return Json(new { success = false, error = "No active ovens found. Add Named Ovens in Settings → Operating Costs." });

        var companyCosts = await _unitOfWork.CompanyOperatingCosts.FindAsync(c => c.CompanyId == suggestCompanyId);
        var defaultCycleMinutes = companyCosts.FirstOrDefault()?.DefaultOvenCycleMinutes ?? 45;

        var queueJobs = (await _unitOfWork.Jobs.FindAsync(
                j => j.CompanyId == suggestCompanyId,
                false,
                j => j.Customer,
                j => j.JobStatus,
                j => j.JobPriority,
                j => j.JobItems))
            .Where(j => j.JobStatus != null && QueueableStatuses.Contains(j.JobStatus.StatusCode))
            .ToList();

        if (!queueJobs.Any())
            return Json(new { success = false, error = "No jobs in the queue to schedule." });

        // Load coats with inventory items
        var jobItemIds = queueJobs.SelectMany(j => j.JobItems.Select(i => i.Id)).ToHashSet();
        var allCoats = jobItemIds.Any()
            ? (await _unitOfWork.JobItemCoats.GetAllAsync(false, c => c.InventoryItem))
              .Where(c => jobItemIds.Contains(c.JobItemId))
              .ToList()
            : new List<JobItemCoat>();

        var coatsByItem = allCoats.GroupBy(c => c.JobItemId).ToDictionary(g => g.Key, g => g.ToList());
        foreach (var job in queueJobs)
            foreach (var item in job.JobItems)
                if (coatsByItem.TryGetValue(item.Id, out var coats))
                    item.Coats = coats;

        var allBatchedItems = await _unitOfWork.OvenBatchItems.GetAllAsync(false, i => i.Batch);
        var scheduledCoatIds = allBatchedItems
            .Where(i => i.Status != OvenBatchItemStatus.Removed && i.Batch.Status != OvenBatchStatus.Cancelled)
            .Select(i => i.JobItemCoatId)
            .ToHashSet();

        var aiRequest = new BatchSchedulingRequest
        {
            ScheduleFromDate = DateTime.Today,
            OptimizationGoal = goal,
            Ovens = equipmentList.Select(o => new OvenConfigDto
            {
                OvenCostId = o.Id,
                EquipmentId = 0,
                Name = o.Label,
                MaxLoadSqFt = o.MaxLoadSqFt,
                CycleMinutes = o.DefaultCycleMinutes ?? defaultCycleMinutes,
            }).ToList(),
            Jobs = BuildAiJobDtos(queueJobs, scheduledCoatIds)
        };

        var suggestion = await _aiSchedulingService.SuggestBatchesAsync(aiRequest);
        if (!suggestion.Success)
            return Json(new { success = false, error = suggestion.ErrorMessage });

        return Json(new { success = true, suggestion });
    }

    // ──────────────────────────────────────────────────────────────────────────
    // ACCEPT SUGGESTION — POST, creates OvenBatch records from AI suggestion
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Persists one or more AI-suggested batches as real <see cref="OvenBatch"/> and
    /// <see cref="OvenBatchItem"/> records, wrapped in a database transaction so that either
    /// all batches are created or none are (avoiding partial state on failure).
    /// Each batch receives a unique number via <see cref="GenerateBatchNumberAsync"/> and is
    /// flagged <c>AiSuggested = true</c> so the UI can distinguish AI-created batches from
    /// manually created ones. The AI reasoning text is stored as JSON in <c>AiReasoningJson</c>
    /// for auditability. Start/end times are only set when the AI provides a parseable
    /// <c>SuggestedStartTime</c> string.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> AcceptSuggestion([FromBody] AcceptSuggestionRequest req)
    {
        if (req?.Batches == null || !req.Batches.Any())
            return Json(new { success = false, error = "No batches provided." });

        var acceptCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
        var companyCosts = await _unitOfWork.CompanyOperatingCosts.FindAsync(c => c.CompanyId == acceptCompanyId);
        var defaultCycleMinutes = companyCosts.FirstOrDefault()?.DefaultOvenCycleMinutes ?? 45;

        var createdBatches = new List<object>();
        try
        {
        await _unitOfWork.ExecuteInTransactionAsync(async () =>
        {
            createdBatches.Clear();
            var scheduledDate = req.ScheduledDate?.Date ?? DateTime.Today;

            foreach (var suggested in req.Batches)
            {
                var batchNumber = await GenerateBatchNumberAsync();

                var batch = new OvenBatch
                {
                    BatchNumber = batchNumber,
                    OvenCostId = suggested.OvenCostId > 0 ? suggested.OvenCostId : (int?)null,
                    Status = OvenBatchStatus.Planned,
                    ScheduledDate = scheduledDate,
                    TotalSurfaceAreaSqFt = suggested.EstimatedSqFt,
                    CureTemperatureF = suggested.CureTemperatureF,
                    CycleMinutes = suggested.EstimatedCycleMinutes > 0 ? suggested.EstimatedCycleMinutes : defaultCycleMinutes,
                    PrimaryColorName = suggested.PrimaryColorName,
                    PrimaryColorCode = suggested.PrimaryColorCode,
                    AiSuggested = true,
                    AiReasoningJson = suggested.Rationale
                };

                if (!string.IsNullOrWhiteSpace(suggested.SuggestedStartTime)
                    && TimeSpan.TryParse(suggested.SuggestedStartTime, out var t))
                {
                    batch.ScheduledStartTime = scheduledDate.Date.Add(t);
                    batch.EstimatedEndTime = batch.ScheduledStartTime.Value.AddMinutes(batch.CycleMinutes);
                }

                await _unitOfWork.OvenBatches.AddAsync(batch);
                await _unitOfWork.SaveChangesAsync();

                var sortOrder = 0;
                foreach (var item in suggested.Items)
                {
                    await _unitOfWork.OvenBatchItems.AddAsync(new OvenBatchItem
                    {
                        OvenBatchId = batch.Id,
                        JobId = item.JobId,
                        JobItemId = item.JobItemId,
                        JobItemCoatId = item.JobItemCoatId,
                        SurfaceAreaContribution = item.SurfaceAreaSqFt,
                        CoatPassNumber = item.CoatPassNumber,
                        SortOrder = sortOrder++,
                        Status = OvenBatchItemStatus.Pending
                    });
                }

                createdBatches.Add(new { batchId = batch.Id, batchNumber });
            }

        }); // end ExecuteInTransactionAsync

            return Json(new { success = true, batches = createdBatches });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error accepting AI batch suggestion");
            return Json(new { success = false, error = "Failed to save batches." });
        }
    }

    // ──────────────────────────────────────────────────────────────────────────
    // CREATE BATCH — manually create an empty batch
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Creates a new empty <see cref="OvenBatch"/> for the specified named oven and returns the
    /// full batch DTO so the client can render it immediately without a page reload.
    /// The oven's <c>DefaultCycleMinutes</c> is preferred over the company-level default so that
    /// per-oven timing is honored; the company default acts as a safe fallback when an oven has
    /// no specific value. <c>AiSuggested</c> is set to <c>false</c> to distinguish manual batches
    /// from those created via <see cref="AcceptSuggestion"/>.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> CreateBatch([FromBody] CreateBatchRequest req)
    {
        if (req.OvenCostId <= 0)
            return Json(new { success = false, error = "Invalid oven selected." });

        var oven = await _unitOfWork.OvenCosts.GetByIdAsync(req.OvenCostId);
        if (oven == null)
            return Json(new { success = false, error = "Oven not found." });

        var createBatchCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
        var companyCosts = await _unitOfWork.CompanyOperatingCosts.FindAsync(c => c.CompanyId == createBatchCompanyId);
        var defaultCycleMinutes = companyCosts.FirstOrDefault()?.DefaultOvenCycleMinutes ?? 45;

        var batchNumber = await GenerateBatchNumberAsync();
        var scheduledDate = req.ScheduledDate?.Date ?? DateTime.Today;

        var batch = new OvenBatch
        {
            BatchNumber = batchNumber,
            OvenCostId = req.OvenCostId,
            Status = OvenBatchStatus.Planned,
            ScheduledDate = scheduledDate,
            CycleMinutes = oven.DefaultCycleMinutes ?? defaultCycleMinutes,
            TotalSurfaceAreaSqFt = 0,
            AiSuggested = false,
            Notes = req.Notes
        };

        if (req.ScheduledStartTime.HasValue)
        {
            batch.ScheduledStartTime = scheduledDate.Date.Add(req.ScheduledStartTime.Value);
            batch.EstimatedEndTime = batch.ScheduledStartTime.Value.AddMinutes(batch.CycleMinutes);
        }

        await _unitOfWork.OvenBatches.AddAsync(batch);
        await _unitOfWork.CompleteAsync();

        return Json(new
        {
            success = true,
            batch = new
            {
                id = batch.Id,
                batchNumber = batch.BatchNumber,
                ovenCostId = batch.OvenCostId,
                equipmentId = 0,
                ovenName = oven.Label,
                status = batch.Status.ToString(),
                statusId = (int)batch.Status,
                scheduledDate = batch.ScheduledDate,
                scheduledStartTime = batch.ScheduledStartTime,
                estimatedEndTime = batch.EstimatedEndTime,
                cycleMinutes = batch.CycleMinutes,
                totalSurfaceAreaSqFt = 0m,
                maxLoadSqFt = oven.MaxLoadSqFt,
                capacityPct = 0m,
                aiSuggested = false,
                items = new List<object>()
            }
        });
    }

    // ──────────────────────────────────────────────────────────────────────────
    // ADD TO BATCH — drag a coat from the queue into a batch
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Adds a single coat (identified by <c>JobItemCoatId</c>) from the scheduler queue into
    /// an existing batch and recalculates the batch's total surface area and capacity percentage.
    /// A coat-conflict check is performed before insertion: if the same coat is already active in
    /// a different non-cancelled batch, the request is rejected to prevent scheduling the same
    /// physical coat twice. Surface area is computed as <c>SurfaceAreaSqFt × Quantity</c> so that
    /// multi-piece items count their total oven footprint. The primary color is set from the first
    /// coat added if the batch has no color yet, giving operators a visual cue for batch grouping.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> AddToBatch([FromBody] AddToBatchRequest req)
    {
        var batch = await _unitOfWork.OvenBatches.GetByIdAsync(req.BatchId, false, b => b.Items, b => b.OvenCost);
        if (batch == null)
            return Json(new { success = false, error = "Batch not found." });

        if (batch.Status >= OvenBatchStatus.InProgress)
            return Json(new { success = false, error = "Cannot modify a batch that is already in progress." });

        // Check if coat is already in another active batch
        var existingItems = await _unitOfWork.OvenBatchItems.GetAllAsync(false, i => i.Batch);
        var conflicting = existingItems.FirstOrDefault(i =>
            i.JobItemCoatId == req.JobItemCoatId &&
            i.Status != OvenBatchItemStatus.Removed &&
            i.OvenBatchId != req.BatchId &&
            i.Batch.Status != OvenBatchStatus.Cancelled);

        if (conflicting != null)
            return Json(new { success = false, error = $"This coat is already scheduled in batch {conflicting.Batch.BatchNumber}." });

        var coat = await _unitOfWork.JobItemCoats.GetByIdAsync(req.JobItemCoatId, false, c => c.JobItem, c => c.InventoryItem);
        if (coat == null)
            return Json(new { success = false, error = "Coat not found." });

        var job = await _unitOfWork.Jobs.GetByIdAsync(coat.JobItem.JobId, false, j => j.Customer, j => j.JobPriority);

        var nextOrder = batch.Items.Any() ? batch.Items.Max(i => i.SortOrder) + 1 : 0;
        var sqft = coat.JobItem.SurfaceAreaSqFt * coat.JobItem.Quantity;

        var batchItem = new OvenBatchItem
        {
            OvenBatchId = req.BatchId,
            JobId = coat.JobItem.JobId,
            JobItemId = coat.JobItemId,
            JobItemCoatId = req.JobItemCoatId,
            SurfaceAreaContribution = sqft,
            CoatPassNumber = coat.Sequence,
            SortOrder = nextOrder,
            Status = OvenBatchItemStatus.Pending
        };

        await _unitOfWork.OvenBatchItems.AddAsync(batchItem);

        batch.TotalSurfaceAreaSqFt = batch.Items.Sum(i => i.SurfaceAreaContribution) + sqft;
        if (string.IsNullOrEmpty(batch.PrimaryColorName) && !string.IsNullOrEmpty(coat.ColorName))
            batch.PrimaryColorName = coat.ColorName;

        await _unitOfWork.CompleteAsync();

        var capacityPct = batch.OvenCost?.MaxLoadSqFt > 0
            ? Math.Round(batch.TotalSurfaceAreaSqFt / batch.OvenCost.MaxLoadSqFt.Value * 100, 1)
            : (decimal?)null;

        return Json(new
        {
            success = true,
            batchItemId = batchItem.Id,
            totalSurfaceAreaSqFt = batch.TotalSurfaceAreaSqFt,
            capacityPct,
            item = new
            {
                id = batchItem.Id,
                jobId = batchItem.JobId,
                jobItemId = batchItem.JobItemId,
                jobItemCoatId = batchItem.JobItemCoatId,
                jobNumber = job?.JobNumber ?? "",
                customerName = job?.Customer?.CompanyName ?? $"{job?.Customer?.ContactFirstName} {job?.Customer?.ContactLastName}".Trim(),
                itemDescription = coat.JobItem.Description,
                coatName = coat.CoatName,
                colorName = coat.ColorName,
                colorCode = coat.ColorCode,
                surfaceAreaContribution = sqft,
                coatPassNumber = coat.Sequence,
                priority = job?.JobPriority?.DisplayName ?? "Normal",
                dueDate = job?.DueDate
            }
        });
    }

    // ──────────────────────────────────────────────────────────────────────────
    // MOVE TO BATCH — drag a batch item from one batch to another
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Moves an existing <see cref="OvenBatchItem"/> from its current batch to a different target
    /// batch, adjusting the surface-area totals on both batches atomically.
    /// Both the source and target batch are guard-checked for <c>InProgress</c> or later status —
    /// in-progress batches are already physically loaded into the oven so their item list must not
    /// change. Surface area is subtracted from the source (floored at zero to avoid negative totals
    /// from floating-point drift) and added to the target.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> MoveToBatch([FromBody] MoveToBatchRequest req)
    {
        var item = await _unitOfWork.OvenBatchItems.GetByIdAsync(req.BatchItemId, false, i => i.Batch);
        if (item == null)
            return Json(new { success = false, error = "Batch item not found." });

        var targetBatch = await _unitOfWork.OvenBatches.GetByIdAsync(req.TargetBatchId, false, b => b.Items, b => b.Equipment);
        if (targetBatch == null)
            return Json(new { success = false, error = "Target batch not found." });

        if (targetBatch.Status >= OvenBatchStatus.InProgress)
            return Json(new { success = false, error = "Cannot add items to a batch in progress." });

        if (item.Batch.Status >= OvenBatchStatus.InProgress)
            return Json(new { success = false, error = "Cannot move items from a batch in progress." });

        var sourceBatch = await _unitOfWork.OvenBatches.GetByIdAsync(item.OvenBatchId, false, b => b.Items);
        var oldSqft = item.SurfaceAreaContribution;

        item.OvenBatchId = req.TargetBatchId;
        item.SortOrder = targetBatch.Items.Any() ? targetBatch.Items.Max(i => i.SortOrder) + 1 : 0;

        if (sourceBatch != null)
            sourceBatch.TotalSurfaceAreaSqFt = Math.Max(0, sourceBatch.TotalSurfaceAreaSqFt - oldSqft);

        targetBatch.TotalSurfaceAreaSqFt += oldSqft;

        await _unitOfWork.CompleteAsync();

        return Json(new
        {
            success = true,
            sourceBatchTotal = sourceBatch?.TotalSurfaceAreaSqFt ?? 0,
            targetBatchTotal = targetBatch.TotalSurfaceAreaSqFt
        });
    }

    // ──────────────────────────────────────────────────────────────────────────
    // REMOVE FROM BATCH
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Marks a batch item as <see cref="OvenBatchItemStatus.Removed"/> (soft-removal, not a delete)
    /// and returns enough job/coat data for the JavaScript client to reconstruct the queue card
    /// without a full page reload.
    /// Soft-removal is used rather than physical deletion so that historical audit data on the batch
    /// is preserved. The response includes the full queue item payload — job number, customer, color,
    /// surface area, etc. — because the client needs to re-insert the coat back into the queue column.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> RemoveFromBatch([FromBody] RemoveFromBatchRequest req)
    {
        var item = await _unitOfWork.OvenBatchItems.GetByIdAsync(req.BatchItemId, false,
            i => i.JobItem,
            i => i.JobItemCoat,
            i => i.JobItemCoat.InventoryItem);
        if (item == null)
            return Json(new { success = false, error = "Item not found." });

        var batch = await _unitOfWork.OvenBatches.GetByIdAsync(item.OvenBatchId, false, b => b.Items);
        if (batch?.Status >= OvenBatchStatus.InProgress)
            return Json(new { success = false, error = "Cannot remove items from a batch in progress." });

        var job = await _unitOfWork.Jobs.GetByIdAsync(item.JobId, false,
            j => j.Customer,
            j => j.JobPriority,
            j => j.JobStatus);

        item.Status = OvenBatchItemStatus.Removed;
        if (batch != null)
            batch.TotalSurfaceAreaSqFt = Math.Max(0, batch.TotalSurfaceAreaSqFt - item.SurfaceAreaContribution);

        await _unitOfWork.CompleteAsync();

        return Json(new
        {
            success = true,
            batchTotal = batch?.TotalSurfaceAreaSqFt ?? 0,
            // Full data needed to rebuild the queue item in JS without a page reload
            queueItem = new
            {
                jobId = item.JobId,
                jobNumber = job?.JobNumber ?? "",
                customerName = job?.Customer?.CompanyName
                    ?? $"{job?.Customer?.ContactFirstName} {job?.Customer?.ContactLastName}".Trim(),
                priority = job?.JobPriority?.DisplayName ?? "Normal",
                priorityId = job?.JobPriority?.DisplayOrder ?? 1,
                dueDate = job?.DueDate?.ToString("yyyy-MM-dd"),
                isOverdue = job?.DueDate.HasValue == true && job.DueDate.Value.Date < DateTime.Today,
                jobItemCoatId = item.JobItemCoatId,
                jobItemId = item.JobItemId,
                coatName = item.JobItemCoat?.CoatName ?? "",
                coatPassNumber = item.CoatPassNumber,
                colorName = item.JobItemCoat?.ColorName,
                colorCode = item.JobItemCoat?.ColorCode,
                surfaceAreaSqFt = item.SurfaceAreaContribution,
                itemDescription = item.JobItem?.Description ?? ""
            }
        });
    }

    // ──────────────────────────────────────────────────────────────────────────
    // START BATCH
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Transitions a batch from Planned/Loading to InProgress, advances all pending batch items
    /// to <see cref="OvenBatchItemStatus.InOven"/>, and pushes the affected jobs to the
    /// <c>IN_OVEN</c> status in the job lifecycle.
    /// The job status update is critical for shop-floor visibility: shop workers checking the
    /// <c>/hubs/shop</c> SignalR feed will see the status change immediately via the
    /// <c>JobStatusChanged</c> push event sent after the DB save. The estimated end time is
    /// recalculated from <c>DateTime.UtcNow</c> rather than the scheduled start to reflect the
    /// actual time the oven run began.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> StartBatch([FromBody] BatchActionRequest req)
    {
        var batch = await _unitOfWork.OvenBatches.GetByIdAsync(req.BatchId, false, b => b.Items);
        if (batch == null)
            return Json(new { success = false, error = "Batch not found." });

        if (batch.Status != OvenBatchStatus.Planned && batch.Status != OvenBatchStatus.Loading)
            return Json(new { success = false, error = "Batch must be Planned or Loading to start." });

        batch.Status = OvenBatchStatus.InProgress;
        batch.ActualStartTime = DateTime.UtcNow;
        batch.EstimatedEndTime = DateTime.UtcNow.AddMinutes(batch.CycleMinutes);

        foreach (var batchItem in batch.Items.Where(i => i.Status == OvenBatchItemStatus.Pending))
            batchItem.Status = OvenBatchItemStatus.InOven;

        var inOvenStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(s => s.StatusCode == AppConstants.StatusCodes.Job.InOven);
        if (inOvenStatus != null)
        {
            var jobIds = batch.Items.Select(i => i.JobId).Distinct().ToHashSet();
            var startBatchCid = _tenantContext.GetCurrentCompanyId() ?? 0;
            var jobs = await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == startBatchCid && jobIds.Contains(j.Id));
            foreach (var job in jobs)
                job.JobStatusId = inOvenStatus.Id;
        }

        await _unitOfWork.CompleteAsync();

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

        return Json(new { success = true, status = "InProgress", startTime = batch.ActualStartTime });
    }

    // ──────────────────────────────────────────────────────────────────────────
    // COMPLETE BATCH
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Marks a batch as Completed, closes all in-oven batch items, and advances each affected job
    /// to the correct next status based on whether all of that job's coats across all batches are now done.
    /// The per-job logic is intentionally nuanced: a job with multiple coat passes may span several
    /// batches. Only when every coat of a job (<c>allJobCoatIds</c>) is represented in a completed
    /// batch item (<c>completedCoatIds</c>) does the job advance to CURING. Otherwise it moves back
    /// to COATING, signaling to the shop that additional oven passes remain. A SignalR push is fired
    /// after the save so real-time shop-floor displays update immediately without polling.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> CompleteBatch([FromBody] BatchActionRequest req)
    {
        var batch = await _unitOfWork.OvenBatches.GetByIdAsync(req.BatchId, false, b => b.Items);
        if (batch == null)
            return Json(new { success = false, error = "Batch not found." });

        if (batch.Status != OvenBatchStatus.InProgress)
            return Json(new { success = false, error = "Batch must be In Progress to complete." });

        batch.Status = OvenBatchStatus.Completed;
        batch.ActualEndTime = DateTime.UtcNow;

        foreach (var batchItem in batch.Items.Where(i => i.Status == OvenBatchItemStatus.InOven))
            batchItem.Status = OvenBatchItemStatus.Completed;

        var curingStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(s => s.StatusCode == AppConstants.StatusCodes.Job.Curing);
        var coatingStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(s => s.StatusCode == AppConstants.StatusCodes.Job.Coating);

        var jobIds = batch.Items.Select(i => i.JobId).Distinct().ToList();
        var allBatchedItems = await _unitOfWork.OvenBatchItems.GetAllAsync(false, i => i.Batch);

        foreach (var jobId in jobIds)
        {
            var allJobCoatIds = (await _unitOfWork.JobItemCoats.GetAllAsync(false, c => c.JobItem))
                .Where(c => c.JobItem.JobId == jobId)
                .Select(c => c.Id)
                .ToHashSet();

            var completedCoatIds = allBatchedItems
                .Where(i => i.JobId == jobId &&
                            i.Status == OvenBatchItemStatus.Completed &&
                            i.Batch.Status == OvenBatchStatus.Completed)
                .Select(i => i.JobItemCoatId)
                .ToHashSet();

            var allDone = allJobCoatIds.All(id => completedCoatIds.Contains(id));
            var job = await _unitOfWork.Jobs.GetByIdAsync(jobId);
            if (job != null)
            {
                if (allDone && curingStatus != null)
                    job.JobStatusId = curingStatus.Id;
                else if (!allDone && coatingStatus != null)
                    job.JobStatusId = coatingStatus.Id;
            }
        }

        await _unitOfWork.CompleteAsync();

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

        return Json(new { success = true, status = "Completed", endTime = batch.ActualEndTime });
    }

    // ──────────────────────────────────────────────────────────────────────────
    // DELETE BATCH
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Soft-deletes a batch that has not yet started, preventing accidental removal of batches
    /// that are already physically running or have completed historical records.
    /// Batches at <see cref="OvenBatchStatus.InProgress"/> or beyond are rejected because their
    /// items have already influenced job statuses; deleting them would leave jobs stuck in
    /// <c>IN_OVEN</c> with no corresponding batch record.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> DeleteBatch([FromBody] BatchActionRequest req)
    {
        var batch = await _unitOfWork.OvenBatches.GetByIdAsync(req.BatchId);
        if (batch == null)
            return Json(new { success = false, error = "Batch not found." });

        if (batch.Status >= OvenBatchStatus.InProgress)
            return Json(new { success = false, error = "Cannot delete a batch that is in progress or completed." });

        await _unitOfWork.OvenBatches.SoftDeleteAsync(req.BatchId);
        await _unitOfWork.CompleteAsync();
        return Json(new { success = true });
    }

    // ──────────────────────────────────────────────────────────────────────────
    // PRIVATE HELPERS
    // ──────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Generates the next sequential batch number in the format <c>OVN-YYMM-####</c>.
    /// Uses <c>ignoreQueryFilters: true</c> to include soft-deleted batches in the sequence scan —
    /// this prevents number reuse if a batch was deleted after being created in the same month,
    /// which is important for financial traceability and shop-floor audit trails.
    /// </summary>
    private async Task<string> GenerateBatchNumberAsync()
    {
        var prefix = $"OVN-{DateTime.Now:yyMM}-";
        var last = (await _unitOfWork.OvenBatches.FindAsync(
                b => b.BatchNumber.StartsWith(prefix), ignoreQueryFilters: true))
            .OrderByDescending(b => b.BatchNumber)
            .Select(b => b.BatchNumber)
            .FirstOrDefault();

        int next = 1;
        if (last != null && int.TryParse(last[prefix.Length..], out int num))
            next = num + 1;

        return $"{prefix}{next:D4}";
    }

    /// <summary>
    /// Projects a collection of <see cref="OvenBatch"/> records into <see cref="OvenBatchDto"/> view objects,
    /// attaching item details and capacity percentage for each batch.
    /// Capacity percentage is derived from the oven's <c>MaxLoadSqFt</c> defined on the
    /// <see cref="OvenCost"/> (Named Oven); when not set, capacity is shown as zero rather than throwing.
    /// Items with status <see cref="OvenBatchItemStatus.Removed"/> are excluded from the output so
    /// the UI only shows active items in each batch lane.
    /// The <paramref name="ovenCosts"/> dictionary is consulted as a fallback for batches whose
    /// <c>OvenCost</c> navigation property wasn't eagerly loaded (e.g., legacy batches created before
    /// the Named Ovens migration).
    /// </summary>
    private static List<OvenBatchDto> BuildBatchDtos(
        IEnumerable<OvenBatch> batches,
        IEnumerable<OvenBatchItem> allBatchItems,
        Dictionary<int, Job> jobDict,
        IEnumerable<OvenCost> ovenCosts)
    {
        var itemsByBatch = allBatchItems.GroupBy(i => i.OvenBatchId).ToDictionary(g => g.Key, g => g.ToList());
        var ovenDict = ovenCosts.ToDictionary(o => o.Id);

        return batches.Select(b =>
        {
            var items = itemsByBatch.GetValueOrDefault(b.Id) ?? new List<OvenBatchItem>();
            ovenDict.TryGetValue(b.OvenCostId ?? 0, out var oven);
            var maxLoad = b.OvenCost?.MaxLoadSqFt ?? oven?.MaxLoadSqFt;
            var capPct = maxLoad > 0 ? Math.Round(b.TotalSurfaceAreaSqFt / maxLoad.Value * 100, 1) : 0m;

            return new OvenBatchDto
            {
                Id = b.Id,
                BatchNumber = b.BatchNumber,
                OvenCostId = b.OvenCostId ?? 0,
                EquipmentId = b.EquipmentId ?? 0,
                OvenName = b.OvenCost?.Label ?? oven?.Label ?? "Unknown Oven",
                Status = b.Status.ToString(),
                StatusId = (int)b.Status,
                ScheduledDate = b.ScheduledDate,
                ScheduledStartTime = b.ScheduledStartTime,
                EstimatedEndTime = b.EstimatedEndTime,
                ActualStartTime = b.ActualStartTime,
                ActualEndTime = b.ActualEndTime,
                TotalSurfaceAreaSqFt = b.TotalSurfaceAreaSqFt,
                MaxLoadSqFt = maxLoad,
                CapacityPct = capPct,
                CureTemperatureF = b.CureTemperatureF,
                CycleMinutes = b.CycleMinutes,
                PrimaryColorName = b.PrimaryColorName,
                PrimaryColorCode = b.PrimaryColorCode,
                AiSuggested = b.AiSuggested,
                AiReasoning = b.AiReasoningJson,
                Notes = b.Notes,
                Items = items
                    .Where(i => i.Status != OvenBatchItemStatus.Removed)
                    .Select(i =>
                    {
                        jobDict.TryGetValue(i.JobId, out var job);
                        return new BatchItemDto
                        {
                            Id = i.Id,
                            JobId = i.JobId,
                            JobItemId = i.JobItemId,
                            JobItemCoatId = i.JobItemCoatId,
                            JobNumber = job?.JobNumber ?? i.Job?.JobNumber ?? "",
                            CustomerName = job?.Customer?.CompanyName ?? $"{job?.Customer?.ContactFirstName} {job?.Customer?.ContactLastName}".Trim(),
                            ItemDescription = i.JobItem?.Description ?? "",
                            CoatName = i.JobItemCoat?.CoatName ?? "",
                            ColorName = i.JobItemCoat?.ColorName,
                            ColorCode = i.JobItemCoat?.ColorCode,
                            SurfaceAreaContribution = i.SurfaceAreaContribution,
                            CoatPassNumber = i.CoatPassNumber,
                            Priority = job?.JobPriority?.DisplayName ?? "Normal",
                            DueDate = job?.DueDate,
                            ItemStatus = i.Status.ToString()
                        };
                    }).ToList()
            };
        }).ToList();
    }

    /// <summary>
    /// Builds the sorted, unscheduled coat queue shown in the left-hand column of the scheduler board.
    /// Only coats whose IDs are absent from <paramref name="scheduledCoatIds"/> are included,
    /// so each coat appears in the queue exactly once — it disappears when dropped into a batch and
    /// reappears if removed from the batch via <see cref="RemoveFromBatch"/>.
    /// Jobs are ordered by descending priority ID then ascending due date so the most urgent,
    /// most overdue work surfaces at the top. Jobs with no remaining pending coats are filtered out
    /// entirely so an otherwise-complete job does not litter the queue.
    /// </summary>
    private static List<QueuedJobDto> BuildQueuedJobDtos(
        IEnumerable<Job> jobs,
        HashSet<int> scheduledCoatIds)
    {
        var today = DateTime.Today;

        return jobs
            .Select(j =>
            {
                var pendingCoats = j.JobItems
                    .SelectMany(item => item.Coats
                        .Where(c => !scheduledCoatIds.Contains(c.Id))
                        .OrderBy(c => c.Sequence)
                        .Select(c => new QueuedCoatDto
                        {
                            JobItemId = item.Id,
                            JobItemCoatId = c.Id,
                            ItemDescription = item.Description,
                            CoatName = c.CoatName,
                            CoatPassNumber = c.Sequence,
                            ColorName = c.ColorName,
                            ColorCode = c.ColorCode,
                            SurfaceAreaSqFt = item.SurfaceAreaSqFt * item.Quantity,
                            CureTemperatureF = c.InventoryItem?.CureTemperatureF
                        }))
                    .ToList();

                var priorityId = j.JobPriority?.DisplayOrder ?? 1;

                return new QueuedJobDto
                {
                    JobId = j.Id,
                    JobNumber = j.JobNumber,
                    CustomerName = j.Customer?.CompanyName ?? $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim(),
                    Priority = j.JobPriority?.DisplayName ?? "Normal",
                    PriorityId = priorityId,
                    DueDate = j.DueDate,
                    IsOverdue = j.DueDate.HasValue && j.DueDate.Value.Date < today,
                    JobStatus = j.JobStatus?.DisplayName ?? "",
                    TotalSqFt = j.JobItems.Sum(i => i.SurfaceAreaSqFt * i.Quantity),
                    PendingCoats = pendingCoats
                };
            })
            .Where(q => q.PendingCoats.Any())
            .OrderByDescending(q => q.PriorityId)
            .ThenBy(q => q.DueDate ?? DateTime.MaxValue)
            .ToList();
    }

    /// <summary>
    /// Converts queue jobs into the richer <see cref="OvenReadyJobDto"/> shape expected by the AI
    /// scheduling service, including per-coat cure temperatures and already-baked flags.
    /// The <c>AlreadyBaked</c> flag on each coat (derived from <paramref name="scheduledCoatIds"/>)
    /// allows the AI to understand which passes are still outstanding without requiring it to query
    /// the database itself — all relevant context is packaged up front. Jobs with no unscheduled
    /// coats are excluded so the AI payload is minimal and focused.
    /// </summary>
    private static List<OvenReadyJobDto> BuildAiJobDtos(
        IEnumerable<Job> jobs,
        HashSet<int> scheduledCoatIds)
    {
        return jobs.Select(j => new OvenReadyJobDto
        {
            JobId = j.Id,
            JobNumber = j.JobNumber,
            CustomerName = j.Customer?.CompanyName ?? $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim(),
            Priority = j.JobPriority?.DisplayName ?? "Normal",
            DueDate = j.DueDate,
            Status = j.JobStatus?.StatusCode ?? "",
            Items = j.JobItems
                .Where(i => i.Coats.Any(c => !scheduledCoatIds.Contains(c.Id)))
                .Select(item => new OvenReadyItemDto
                {
                    JobItemId = item.Id,
                    Description = item.Description,
                    Quantity = item.Quantity,
                    SurfaceAreaSqFt = item.SurfaceAreaSqFt,
                    TotalSqFt = item.SurfaceAreaSqFt * item.Quantity,
                    Coats = item.Coats.OrderBy(c => c.Sequence).Select(c => new OvenReadyCoatDto
                    {
                        JobItemCoatId = c.Id,
                        CoatName = c.CoatName,
                        Sequence = c.Sequence,
                        ColorName = c.ColorName,
                        ColorCode = c.ColorCode,
                        CureTemperatureF = c.InventoryItem?.CureTemperatureF,
                        CureTimeMinutes = c.InventoryItem?.CureTimeMinutes,
                        AlreadyBaked = scheduledCoatIds.Contains(c.Id)
                    }).ToList()
                }).ToList()
        }).Where(j => j.Items.Any()).ToList();
    }
}

// ──────────────────────────────────────────────────────────────────────────────
// Request DTOs
// ──────────────────────────────────────────────────────────────────────────────

public class SuggestRequest
{
    public string OptimizationGoal { get; set; } = "maximize_throughput";
}

public class AcceptSuggestionRequest
{
    public DateTime? ScheduledDate { get; set; }
    public List<SuggestedBatch> Batches { get; set; } = new();
}

public class CreateBatchRequest
{
    public int OvenCostId { get; set; }
    public int EquipmentId { get; set; }  // legacy, kept for backward compatibility
    public DateTime? ScheduledDate { get; set; }
    public TimeSpan? ScheduledStartTime { get; set; }
    public string? Notes { get; set; }
}

public class AddToBatchRequest
{
    public int BatchId { get; set; }
    public int JobItemCoatId { get; set; }
}

public class MoveToBatchRequest
{
    public int BatchItemId { get; set; }
    public int TargetBatchId { get; set; }
}

public class RemoveFromBatchRequest
{
    public int BatchItemId { get; set; }
}

public class BatchActionRequest
{
    public int BatchId { get; set; }
}
