using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
namespace PowderCoating.Infrastructure.Services;
public partial class SeedDataService
{
///
/// Seeds transition records for every completed, delivered,
/// or ready-for-pickup job so the Job Cycle Time report can calculate time-per-stage data.
///
///
///
/// For each qualifying job the seeder builds a realistic stage sequence:
/// PENDING → IN_PREPARATION → (SANDBLASTING if any item requires it)
/// → (MASKING_TAPING if any item requires it) → CLEANING → IN_OVEN
/// → COATING → CURING → QUALITY_CHECK → [terminal status].
///
///
/// 85 % of the job's total cycle time (CreatedAt → CompletedDate) is
/// distributed across work stages using fixed per-stage weights that reflect realistic
/// relative durations (e.g. SANDBLASTING > CLEANING). The remaining 15 %
/// is left as residual "terminal status" time, which surfaces correctly in the report's
/// last-entry formula (job.CompletedDate − last.ChangedDate).
///
///
/// Idempotency: returns 0 immediately if any history records already exist for
/// this company, matching the pattern used by all other partial seeders.
///
///
/// The tenant company to seed history for.
/// Number of history records inserted, or 0 if already seeded.
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;
// Only completed-terminal jobs have a meaningful CompletedDate to calculate cycle time.
// CANCELLED is excluded — the report cares only about successfully finished work.
var terminalCodes = new[] { "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" };
var jobs = await _context.Set()
.IgnoreQueryFilters()
.Include(j => j.JobItems)
.Include(j => j.JobStatus)
.Where(j => j.CompanyId == company.Id && !j.IsDeleted && j.CompletedDate.HasValue)
.ToListAsync();
jobs = jobs.Where(j => terminalCodes.Contains(j.JobStatus.StatusCode)).ToList();
if (jobs.Count == 0) return 0;
var statuses = await _context.Set()
.IgnoreQueryFilters()
.Where(s => s.CompanyId == company.Id)
.ToDictionaryAsync(s => s.StatusCode, s => s);
var records = new List();
foreach (var job in jobs)
{
var totalSeconds = (job.CompletedDate!.Value - job.CreatedAt).TotalSeconds;
if (totalSeconds < 60) continue; // skip malformed dates
var needsSand = job.JobItems.Any(i => i.RequiresSandblasting);
var needsMask = job.JobItems.Any(i => i.RequiresMasking);
// Build ordered stage list; the last element is the terminal "to" status only —
// it never appears as a "from" and is not assigned a work weight.
var stages = new List { "PENDING", "IN_PREPARATION" };
if (needsSand) stages.Add("SANDBLASTING");
if (needsMask) stages.Add("MASKING_TAPING");
stages.AddRange(new[] { "CLEANING", "IN_OVEN", "COATING", "CURING", "QUALITY_CHECK" });
stages.Add(job.JobStatus.StatusCode);
// Time weight for each "from" status — reflects typical relative hours in that stage.
static double Weight(string code) => code switch
{
"PENDING" => 2.0, // intake / scheduling buffer
"IN_PREPARATION" => 1.5, // disassembly, hang, pre-inspect
"SANDBLASTING" => 2.0, // media blast + blow-off
"MASKING_TAPING" => 1.0, // tape & plug work
"CLEANING" => 0.5, // chemical wash + dry
"IN_OVEN" => 1.5, // pre-heat before coating
"COATING" => 1.5, // powder application
"CURING" => 1.0, // oven cure cycle
"QUALITY_CHECK" => 0.5, // inspection & touch-up
_ => 0.5,
};
// Work stages are all entries except the terminal "to" at the end.
int n = stages.Count;
var workWeights = stages.Take(n - 1).Select(Weight).ToList();
double totalWeight = workWeights.Sum();
// 85% of cycle time covers the work stages; 15% becomes terminal-status residual
// so (job.CompletedDate − last.ChangedDate) produces a non-zero, plausible value.
double workSeconds = totalSeconds * 0.85;
var currentDate = job.CreatedAt;
for (int i = 0; i < n - 1; i++)
{
if (!statuses.TryGetValue(stages[i], out var fromLookup)) continue;
if (!statuses.TryGetValue(stages[i + 1], out var toLookup)) continue;
currentDate = currentDate.AddSeconds(workSeconds * workWeights[i] / totalWeight);
records.Add(new JobStatusHistory
{
JobId = job.Id,
FromStatusId = fromLookup.Id,
ToStatusId = toLookup.Id,
ChangedDate = currentDate,
CompanyId = company.Id,
CreatedAt = currentDate,
});
}
}
if (records.Count == 0) return 0;
await _context.Set().AddRangeAsync(records);
await _context.SaveChangesAsync();
return records.Count;
}
}