using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
namespace PowderCoating.Infrastructure.Services;
public partial class SeedDataService
{
///
/// Seeds a plausible status-transition history for every job belonging to the company,
/// reconstructing the sequence of transitions a job must have passed through to reach
/// its current status.
///
///
///
/// Idempotency: returns 0 immediately if any non-deleted history rows already exist for
/// this company.
///
///
/// The method does not record arbitrary transitions — it follows the canonical 14-step
/// pipeline array (PENDING → QUOTED → APPROVED → … → DELIVERED) and generates
/// one row per transition step, from PENDING up to
/// and including the job's current status.
///
///
/// Terminal side-branch statuses are handled explicitly:
///
/// - ON_HOLD — assumed to have reached QUALITY_CHECK before pausing.
/// - CANCELLED — assumed to have been cancelled from IN_PREPARATION.
///
///
///
/// Transition timestamps are spread ~6 hours apart starting from job.CreatedAt.
/// This is an approximation chosen for demo realism; actual production transitions record
/// the wall-clock time at which a user changes the status. A safety clamp prevents any
/// generated timestamp from exceeding DateTime.UtcNow.
///
///
/// All history rows are batched into a single AddRangeAsync / SaveChangesAsync
/// call for performance, since the total count can be several hundred rows (50 jobs × up
/// to 14 transitions each).
///
///
/// The tenant company to seed job status history for.
/// Total number of history rows inserted, or 0 if already seeded or no jobs exist.
private async Task SeedJobStatusHistoryAsync(Company company)
{
var existingCount = await _context.Set()
.IgnoreQueryFilters()
.CountAsync(h => h.CompanyId == company.Id && !h.IsDeleted);
if (existingCount > 0)
return 0;
// Load all job status lookups into a code → id map
var statusMap = await _context.Set()
.IgnoreQueryFilters()
.Where(s => s.CompanyId == company.Id)
.ToDictionaryAsync(s => s.StatusCode, s => s.Id);
// Load jobs with their current status
var jobs = await _context.Set()
.IgnoreQueryFilters()
.Include(j => j.JobStatus)
.Where(j => j.CompanyId == company.Id && !j.IsDeleted)
.OrderBy(j => j.Id)
.ToListAsync();
if (jobs.Count == 0 || statusMap.Count == 0)
return 0;
// Ordered pipeline — each status code in the order a job advances through it.
// ON_HOLD and CANCELLED are terminal side-branches handled separately.
var pipeline = new[]
{
"PENDING", "QUOTED", "APPROVED", "IN_PREPARATION",
"SANDBLASTING", "MASKING_TAPING", "CLEANING", "IN_OVEN",
"COATING", "CURING", "QUALITY_CHECK",
"COMPLETED", "READY_FOR_PICKUP", "DELIVERED"
};
var pipelineIndex = pipeline
.Select((code, idx) => (code, idx))
.ToDictionary(t => t.code, t => t.idx);
var history = new List();
var now = DateTime.UtcNow;
foreach (var job in jobs)
{
var currentCode = job.JobStatus.StatusCode;
// Determine the sequence of transitions that happened to reach current state.
// For ON_HOLD: assume it came from QUALITY_CHECK before going on hold.
// For CANCELLED: assume cancelled from APPROVED or IN_PREPARATION.
string[] codesTraversed;
if (currentCode == "ON_HOLD")
{
// Traversed up to QUALITY_CHECK then went ON_HOLD
codesTraversed = [.. pipeline.Take(pipelineIndex["QUALITY_CHECK"] + 1), "ON_HOLD"];
}
else if (currentCode == "CANCELLED")
{
// Cancelled from IN_PREPARATION
codesTraversed = ["PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "CANCELLED"];
}
else if (pipelineIndex.TryGetValue(currentCode, out int curIdx))
{
// Normal pipeline job — traversed from PENDING up to current status
codesTraversed = pipeline.Take(curIdx + 1).ToArray();
}
else
{
// Unknown status — just record a single PENDING → currentCode entry
codesTraversed = ["PENDING", currentCode];
}
// Spread transition dates backwards from job.CreatedAt.
// Each step took roughly 4–8 hours, so transitions are spaced a few hours apart.
// Jobs further along in the pipeline have older start dates.
var stepCount = codesTraversed.Length - 1; // number of transitions
if (stepCount <= 0) continue;
// Job was created at job.CreatedAt; each transition is spaced ~6h apart
// so the first transition (PENDING→QUOTED) happened ~6h after creation, etc.
for (int t = 0; t < stepCount; t++)
{
var fromCode = codesTraversed[t];
var toCode = codesTraversed[t + 1];
if (!statusMap.TryGetValue(fromCode, out int fromId)) continue;
if (!statusMap.TryGetValue(toCode, out int toId)) continue;
// Spread: first transitions happened closer to job creation,
// later ones closer to now. Add a few hours per step.
var hoursOffset = (t + 1) * 6;
var changedDate = job.CreatedAt.AddHours(hoursOffset);
// Don't let transitions exceed "now"
if (changedDate > now) changedDate = now.AddMinutes(-(stepCount - t) * 10);
history.Add(new JobStatusHistory
{
JobId = job.Id,
FromStatusId = fromId,
ToStatusId = toId,
ChangedDate = changedDate,
Notes = null,
CompanyId = company.Id,
CreatedAt = changedDate
});
}
}
if (history.Count == 0) return 0;
await _context.Set().AddRangeAsync(history);
await _context.SaveChangesAsync();
return history.Count;
}
}