using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using OfficeOpenXml;
using rgbc_sds.dal.DB;
using rgbc_sds.services.Enums;
using rgbc_sds.services.Models;
namespace rgbc_sds.services.Services
{
///
/// Service for handling shipment import operations
///
public class ImportService : IImportService
{
private readonly rgbc_sdsContext _context;
private readonly ImportConfiguration _config;
public ImportService(rgbc_sdsContext context)
{
_context = context;
_config = new ImportConfiguration();
// Set EPPlus license context
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
}
///
/// Creates a new import session and validates the uploaded file
///
public async Task CreateImportSessionAsync(
Stream fileStream,
string fileName,
long fileSize,
string importMode,
string createdBy)
{
var stopwatch = Stopwatch.StartNew();
var session = new ImportSession
{
FileName = fileName,
FileSize = fileSize,
ImportMode = importMode,
CreatedBy = createdBy,
TotalRows = 0, // Force EF to include this in INSERT
ValidRows = 0, // Force EF to include this in INSERT
ErrorRows = 0 // Force EF to include this in INSERT
};
try
{
// Validate file
await ValidateFileAsync(fileName, fileSize);
// Process Excel file
var excelData = await ProcessExcelFileAsync(fileStream);
Console.WriteLine($"DEBUG: Excel file processed - Headers: {excelData.Headers.Count}, Rows: {excelData.Rows.Count}");
// Validate column headers
var headerValidation = ColumnMapping.ValidateHeaders(excelData.Headers);
if (!headerValidation.IsValid)
{
var error = ImportErrorLog.CreateFileError(
headerValidation.ErrorMessage,
$"Found headers: {string.Join(", ", headerValidation.FoundHeaders)}",
ErrorSeverity.Critical);
error.SetSession(session.Id);
session.ImportErrorLogs.Add(error);
session.Status = ImportStatus.Failed;
session.ErrorRows = 1;
session.TotalRows = 0;
return session;
}
// Validate row count
if (excelData.Rows.Count > ImportConfiguration.MaxRecordsPerImport)
{
var error = ImportErrorLog.CreateFileError(
$"File contains {excelData.Rows.Count} records, maximum allowed is {ImportConfiguration.MaxRecordsPerImport}",
null,
ErrorSeverity.Critical);
error.SetSession(session.Id);
session.ImportErrorLogs.Add(error);
session.Status = ImportStatus.Failed;
session.ErrorRows = 1;
session.TotalRows = excelData.Rows.Count;
return session;
}
// Create staging records
var stagingRecords = await CreateStagingRecordsAsync(excelData.Rows, session.Id);
Console.WriteLine($"DEBUG: Staging records created: {stagingRecords.Count}");
// Validate staging records
var validationResult = await ValidateStagingRecordsAsync(stagingRecords);
Console.WriteLine($"DEBUG: Validation completed - Valid: {validationResult.ValidCount}, Errors: {validationResult.ErrorCount}");
// Calculate row counts to ensure they satisfy the CHECK constraint
var totalRows = excelData.Rows.Count;
var validRows = validationResult.ValidCount;
var errorRows = validationResult.ErrorCount;
// Ensure the constraint: (ValidRows + ErrorRows) <= TotalRows
// If we have more processed rows than total rows, adjust totalRows
var processedRows = validRows + errorRows;
if (processedRows > totalRows)
{
totalRows = processedRows;
}
// Additional safety check: ensure all values are at least 0
totalRows = Math.Max(0, totalRows);
validRows = Math.Max(0, validRows);
errorRows = Math.Max(0, errorRows);
// Update session with validated results BEFORE saving to database
session.TotalRows = totalRows;
session.ValidRows = validRows;
session.ErrorRows = errorRows;
session.Status = validationResult.ErrorCount == 0 ? ImportStatus.Pending : ImportStatus.Failed;
// Force EF to track these properties as modified
_context.Entry(session).Property(s => s.TotalRows).IsModified = true;
_context.Entry(session).Property(s => s.ValidRows).IsModified = true;
_context.Entry(session).Property(s => s.ErrorRows).IsModified = true;
// Debug logging to see what values we're sending
Console.WriteLine($"DEBUG: Row Counts - Total: {totalRows}, Valid: {validRows}, Error: {errorRows}");
Console.WriteLine($"DEBUG: Constraint Check - (Valid + Error) = {validRows + errorRows} <= {totalRows} = {(validRows + errorRows) <= totalRows}");
// Add staging records and error logs
((List)session.StagingShipments).AddRange(stagingRecords);
((List)session.ImportErrorLogs).AddRange(validationResult.Errors);
Console.WriteLine($"DEBUG: About to save to database - Session ID: {session.Id}");
Console.WriteLine($"DEBUG: Staging records count: {session.StagingShipments.Count}");
Console.WriteLine($"DEBUG: Error logs count: {session.ImportErrorLogs.Count}");
// Save to database AFTER all properties are set and validated
_context.ImportSessions.Add(session);
Console.WriteLine($"DEBUG: Session added to context, calling SaveChanges...");
await _context.SaveChangesAsync();
Console.WriteLine($"DEBUG: SaveChanges completed successfully!");
stopwatch.Stop();
session.ProcessingTime = (int)stopwatch.ElapsedMilliseconds;
await _context.SaveChangesAsync();
return session;
}
catch (Exception ex)
{
// Log system error
var systemError = ImportErrorLog.CreateSystemError(
"Failed to create import session",
ex.ToString(),
ErrorSeverity.Critical);
systemError.SetSession(session.Id);
session.ImportErrorLogs.Add(systemError);
session.Status = ImportStatus.Failed;
session.ErrorRows = 1;
session.TotalRows = 0;
// Save error information
_context.ImportSessions.Add(session);
await _context.SaveChangesAsync();
throw;
}
}
///
/// Gets a summary of the import session
///
public async Task GetImportSessionAsync(Guid sessionId)
{
return await _context.ImportSessions
.Include(s => s.StagingShipments)
.Include(s => s.ImportErrorLogs)
.FirstOrDefaultAsync(s => s.Id == sessionId)
?? throw new ArgumentException($"Import session not found: {sessionId}");
}
///
/// Gets staging data for preview before import
///
public async Task<(List Data, int TotalCount)> GetStagingDataForPreviewAsync(
Guid sessionId,
int pageNumber = 1,
int pageSize = 50)
{
var query = _context.StagingShipments
.Where(s => s.SessionId == sessionId)
.OrderBy(s => s.RowNumber);
var totalCount = await query.CountAsync();
var data = await query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (data, totalCount);
}
///
/// Gets validation errors for the import session
///
public async Task> GetValidationErrorsAsync(Guid sessionId)
{
return await _context.ImportErrorLogs
.Where(e => e.SessionId == sessionId)
.OrderBy(e => e.RowNumber)
.ThenBy(e => e.FieldName)
.ToListAsync();
}
///
/// Executes the import process for validated data
///
public async Task ExecuteImportAsync(
Guid sessionId,
IProgress? progressCallback = null)
{
var stopwatch = Stopwatch.StartNew();
var result = new ImportResult();
var progress = new ImportProgress();
try
{
// Get session with staging data
var session = await GetImportSessionAsync(sessionId);
if (session.Status != ImportStatus.Pending)
{
throw new InvalidOperationException($"Cannot import session with status: {session.Status}");
}
// Start processing
session.StartProcessing();
await _context.SaveChangesAsync();
var validRecords = session.StagingShipments
.Where(s => s.Status == StagingStatus.Validated)
.ToList();
progress.TotalRecords = validRecords.Count;
progressCallback?.Report(progress);
foreach (var record in validRecords)
{
try
{
progress.CurrentRecord++;
progress.CurrentOperation = $"Processing record {record.RowNumber}";
progressCallback?.Report(progress);
// Check if record exists for update
var existingShipment = await GetExistingShipmentAsync(record.IndentNumber);
if (existingShipment != null)
{
// Update existing shipment
await UpdateExistingShipmentAsync(existingShipment, record);
record.SetUpdateFlag(true);
result.RecordsProcessed++;
}
else
{
// Create new shipment
await CreateNewShipmentAsync(record);
record.SetUpdateFlag(false);
result.RecordsProcessed++;
}
record.MarkAsImported();
}
catch (Exception ex)
{
var error = ImportErrorLog.CreateImportError(
$"Failed to import record {record.RowNumber}: {ex.Message}",
record.RowNumber,
ex.ToString());
error.SetSession(sessionId);
session.ImportErrorLogs.Add(error);
record.MarkAsError($"Import failed: {ex.Message}");
result.RecordsFailed++;
}
}
// Complete session
var success = result.RecordsFailed == 0;
session.Complete(success);
session.UpdateRowCounts(result.RecordsProcessed, result.RecordsFailed);
await _context.SaveChangesAsync();
stopwatch.Stop();
result.ProcessingTime = stopwatch.Elapsed;
result.IsSuccess = success;
result.Message = success
? $"Successfully imported {result.RecordsProcessed} records"
: $"Import completed with {result.RecordsFailed} errors";
return result;
}
catch (Exception ex)
{
// Log system error
var systemError = ImportErrorLog.CreateSystemError(
"Import execution failed",
ex.ToString(),
ErrorSeverity.Critical);
systemError.SetSession(sessionId);
_context.ImportErrorLogs.Add(systemError);
await _context.SaveChangesAsync();
result.IsSuccess = false;
result.Message = $"Import failed: {ex.Message}";
result.Errors.Add(ex.ToString());
throw;
}
}
///
/// Cancels an import session
///
public async Task CancelImportSessionAsync(Guid sessionId, string cancelledBy)
{
var session = await GetImportSessionAsync(sessionId);
if (session.Status != ImportStatus.Pending)
{
return false;
}
session.Status = ImportStatus.Cancelled;
session.Notes = $"Cancelled by {cancelledBy} at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC";
await _context.SaveChangesAsync();
return true;
}
///
/// Gets recent import sessions for the current user
///
public async Task> GetRecentImportSessionsAsync(string createdBy, int maxResults = 10)
{
return await _context.ImportSessions
.Where(s => s.CreatedBy == createdBy)
.OrderByDescending(s => s.CreatedAt)
.Take(maxResults)
.ToListAsync();
}
///
/// Cleans up old import sessions and staging data
///
public async Task CleanupOldImportSessionsAsync(int daysToKeep = 30)
{
var cutoffDate = DateTime.UtcNow.AddDays(-daysToKeep);
var oldSessions = await _context.ImportSessions
.Where(s => s.CreatedAt < cutoffDate)
.ToListAsync();
var count = oldSessions.Count;
foreach (var session in oldSessions)
{
_context.ImportSessions.Remove(session);
}
await _context.SaveChangesAsync();
return count;
}
#region Private Methods
///
/// Validates the uploaded file
///
private async Task ValidateFileAsync(string fileName, long fileSize)
{
// Check file extension
var extension = Path.GetExtension(fileName).ToLowerInvariant();
if (!ImportConfiguration.AllowedExtensions.Contains(extension))
{
throw new ArgumentException($"Invalid file type. Allowed types: {string.Join(", ", ImportConfiguration.AllowedExtensions)}");
}
// Check file size
if (fileSize > ImportConfiguration.MaxFileSizeBytes)
{
throw new ArgumentException($"File size {fileSize} bytes exceeds maximum allowed size of {ImportConfiguration.MaxFileSizeBytes} bytes");
}
}
///
/// Processes the Excel file and extracts data
///
private async Task ProcessExcelFileAsync(Stream fileStream)
{
var result = new ExcelDataResult();
using var package = new ExcelPackage(fileStream);
var worksheet = package.Workbook.Worksheets.FirstOrDefault();
if (worksheet == null)
{
throw new ArgumentException("Excel file contains no worksheets");
}
// Read headers from first row
var headerRow = worksheet.Cells[1, 1, 1, worksheet.Dimension.End.Column];
result.Headers = headerRow.Select(cell => cell.Text?.Trim() ?? string.Empty).ToList();
// Read data rows
var startRow = 2;
var endRow = worksheet.Dimension.End.Row;
for (int row = startRow; row <= endRow; row++)
{
var rowData = new ExcelRowData { RowNumber = row };
for (int col = 1; col <= result.Headers.Count; col++)
{
var header = result.Headers[col - 1];
var cellValue = worksheet.Cells[row, col].Text?.Trim() ?? string.Empty;
if (!string.IsNullOrEmpty(header))
{
rowData.ColumnValues[header] = cellValue;
}
}
if (rowData.HasData)
{
result.Rows.Add(rowData);
}
}
return result;
}
///
/// Creates staging records from Excel data
///
private async Task> CreateStagingRecordsAsync(List rows, Guid sessionId)
{
var stagingRecords = new List();
foreach (var row in rows)
{
var stagingRecord = new StagingShipment
{
SessionId = sessionId,
RowNumber = row.RowNumber,
SupplierName = row.GetValue("Supplier"),
IndentNumber = row.GetValue("Indent Number"),
ShipshapeNumber = row.GetValue("Shipshape Number"),
MotherVessel = row.GetValue("Mother Vessel"),
ETA = row.GetValue("E.T.A"),
BillOfEntryNumber = row.GetValue("Bill of Entry Number"),
ContainerNumber = row.GetValue("Container Number"),
OffloadingPort = row.GetValue("Offloading Port"),
ContainerReceived = row.GetValue("Container Received"),
NoOfFlatcases = row.GetValue("No of Flatcases"),
Voyage = row.GetValue("Voyage"),
MBLNumber = row.GetValue("M/BL Number"),
RedirectToSACD = row.GetValue("Redirect to SACD")
};
stagingRecords.Add(stagingRecord);
}
return stagingRecords;
}
///
/// Validates staging records
///
private async Task ValidateStagingRecordsAsync(List records)
{
Console.WriteLine($"DEBUG: Starting validation of {records.Count} staging records");
var result = new ValidationResult();
var errors = new List();
foreach (var record in records)
{
Console.WriteLine($"DEBUG: Validating record {record.RowNumber} - Supplier: '{record.SupplierName}', Indent: '{record.IndentNumber}'");
var recordErrors = new List();
// Required field validation
if (string.IsNullOrWhiteSpace(record.SupplierName))
{
recordErrors.Add("Supplier is required");
}
if (string.IsNullOrWhiteSpace(record.IndentNumber))
{
recordErrors.Add("Indent Number is required");
}
if (string.IsNullOrWhiteSpace(record.ShipshapeNumber))
{
recordErrors.Add("Shipshape Number is required");
}
if (string.IsNullOrWhiteSpace(record.MotherVessel))
{
recordErrors.Add("Mother Vessel is required");
}
if (string.IsNullOrWhiteSpace(record.ETA))
{
recordErrors.Add("E.T.A is required");
}
if (string.IsNullOrWhiteSpace(record.BillOfEntryNumber))
{
recordErrors.Add("Bill of Entry Number is required");
}
if (string.IsNullOrWhiteSpace(record.ContainerNumber))
{
recordErrors.Add("Container Number is required");
}
if (string.IsNullOrWhiteSpace(record.OffloadingPort))
{
recordErrors.Add("Offloading Port is required");
}
else
{
var validPorts = rgbc_sds.services.Enums.ValidPorts.AllPorts;
if (!validPorts.Contains(record.OffloadingPort))
{
recordErrors.Add($"Offloading Port must be one of: {string.Join(", ", validPorts)}");
}
else
{
// Additional validation: ensure port can be mapped to an ID
var portId = record.OffloadingPort switch
{
"Cape Town" => 1,
"Coega" => 2,
"Durban" => 3,
"P.E." => 4,
_ => 0
};
if (portId == 0)
{
recordErrors.Add($"Offloading Port '{record.OffloadingPort}' cannot be mapped to a valid port ID");
}
}
}
if (string.IsNullOrWhiteSpace(record.ContainerReceived))
{
recordErrors.Add("Container Received is required");
}
if (string.IsNullOrWhiteSpace(record.NoOfFlatcases))
{
recordErrors.Add("No of Flatcases is required");
}
if (string.IsNullOrWhiteSpace(record.Voyage))
{
recordErrors.Add("Voyage is required");
}
if (string.IsNullOrWhiteSpace(record.MBLNumber))
{
recordErrors.Add("M/BL Number is required");
}
if (string.IsNullOrWhiteSpace(record.RedirectToSACD))
{
recordErrors.Add("Redirect to SACD is required");
}
// Business rule validation
if (!string.IsNullOrWhiteSpace(record.IndentNumber))
{
// Check for duplicate indent numbers in the same session
var duplicateCount = records.Count(r =>
r.RowNumber != record.RowNumber &&
r.IndentNumber.ToLower() == record.IndentNumber.ToLower());
if (duplicateCount > 0)
{
recordErrors.Add($"Duplicate Indent Number found in rows: {string.Join(", ", records.Where(r => r.IndentNumber.ToLower() == record.IndentNumber.ToLower()).Select(r => r.RowNumber))}");
}
}
// Date format validation
if (!string.IsNullOrWhiteSpace(record.ETA) && !IsValidDate(record.ETA))
{
recordErrors.Add("Invalid ETA date format");
}
if (!string.IsNullOrWhiteSpace(record.ContainerReceived) && !IsValidDate(record.ContainerReceived))
{
recordErrors.Add("Invalid Container Received date format");
}
if (!string.IsNullOrWhiteSpace(record.RedirectToSACD) && !IsValidDate(record.RedirectToSACD))
{
recordErrors.Add("Invalid Redirect to SACD date format");
}
// Reference validation
if (!string.IsNullOrWhiteSpace(record.SupplierName))
{
try
{
Console.WriteLine($"DEBUG: Checking if supplier '{record.SupplierName}' exists in database...");
var supplierExists = await _context.sdsSupplier
.AnyAsync(s => s.name.ToLower() == record.SupplierName.ToLower());
Console.WriteLine($"DEBUG: Supplier '{record.SupplierName}' exists: {supplierExists}");
if (!supplierExists)
{
recordErrors.Add($"Supplier '{record.SupplierName}' not found in system");
}
}
catch (Exception ex)
{
Console.WriteLine($"DEBUG: Error checking supplier '{record.SupplierName}': {ex.Message}");
recordErrors.Add($"Error validating supplier '{record.SupplierName}': {ex.Message.Substring(0, Math.Min(ex.Message.Length, 200))}");
}
}
if (recordErrors.Count > 0)
{
Console.WriteLine($"DEBUG: Record {record.RowNumber} has {recordErrors.Count} errors: {string.Join(", ", recordErrors)}");
var error = ImportErrorLog.CreateValidationError(
string.Join("; ", recordErrors),
record.RowNumber,
"Multiple",
JsonSerializer.Serialize(recordErrors));
error.SetSession(record.SessionId);
errors.Add(error);
record.MarkAsError(string.Join("; ", recordErrors), JsonSerializer.Serialize(recordErrors));
result.ErrorCount++;
}
else
{
Console.WriteLine($"DEBUG: Record {record.RowNumber} is valid");
record.MarkAsValidated();
result.ValidCount++;
}
Console.WriteLine($"DEBUG: Record {record.RowNumber} validation completed - Errors: {recordErrors.Count}");
}
result.Errors = errors;
Console.WriteLine($"DEBUG: Validation completed - Total: {records.Count}, Valid: {result.ValidCount}, Errors: {result.ErrorCount}");
return result;
}
///
/// Gets existing shipment by indent number
///
private async Task GetExistingShipmentAsync(string indentNumber)
{
return await _context.sdsShipment
.FirstOrDefaultAsync(s => s.indent_number.ToLower() == indentNumber.ToLower());
}
///
/// Updates existing shipment with staging data
///
private async Task UpdateExistingShipmentAsync(sdsShipment shipment, StagingShipment staging)
{
// Update fields if they have values
if (!string.IsNullOrWhiteSpace(staging.ShipshapeNumber))
shipment.shipshape_number = staging.ShipshapeNumber;
if (!string.IsNullOrWhiteSpace(staging.MotherVessel))
shipment.mother_vessel = staging.MotherVessel;
if (!string.IsNullOrWhiteSpace(staging.ETA))
shipment.eta = ParseDate(staging.ETA);
if (!string.IsNullOrWhiteSpace(staging.BillOfEntryNumber))
shipment.bill_of_entry_number = staging.BillOfEntryNumber;
if (!string.IsNullOrWhiteSpace(staging.ContainerNumber))
shipment.container_number = staging.ContainerNumber;
if (!string.IsNullOrWhiteSpace(staging.OffloadingPort))
{
// Map port names to IDs based on hardcoded values from frontend
shipment.port_id = staging.OffloadingPort switch
{
"Cape Town" => 1,
"Coega" => 2,
"Durban" => 3,
"P.E." => 4,
_ => null // Unknown port name
};
}
if (!string.IsNullOrWhiteSpace(staging.ContainerReceived))
shipment.container_received = ParseDate(staging.ContainerReceived);
if (!string.IsNullOrWhiteSpace(staging.NoOfFlatcases))
shipment.no_of_flatcases = staging.NoOfFlatcases;
if (!string.IsNullOrWhiteSpace(staging.Voyage))
shipment.voyage = staging.Voyage;
if (!string.IsNullOrWhiteSpace(staging.MBLNumber))
shipment.mbl_number = staging.MBLNumber;
if (!string.IsNullOrWhiteSpace(staging.RedirectToSACD))
shipment.redirect_to_SACD = ParseDate(staging.RedirectToSACD);
shipment.updated_at = DateTime.Now;
shipment.updated_by = staging.ImportSession.CreatedBy;
}
///
/// Creates new shipment from staging data
///
private async Task CreateNewShipmentAsync(StagingShipment staging)
{
// Get supplier ID
var supplier = await _context.sdsSupplier
.FirstOrDefaultAsync(s => s.name.ToLower() == staging.SupplierName.ToLower());
if (supplier == null)
{
throw new InvalidOperationException($"Supplier '{staging.SupplierName}' not found");
}
var shipment = new sdsShipment
{
user_id = null, // Will be set when user context is available
indent_number = staging.IndentNumber,
shipshape_number = staging.ShipshapeNumber,
mother_vessel = staging.MotherVessel,
eta = ParseDate(staging.ETA),
bill_of_entry_number = staging.BillOfEntryNumber,
container_number = staging.ContainerNumber,
port_id = !string.IsNullOrWhiteSpace(staging.OffloadingPort) ? staging.OffloadingPort switch
{
"Cape Town" => 1,
"Coega" => 2,
"Durban" => 3,
"P.E." => 4,
_ => null // Unknown port name
} : null,
container_received = ParseDate(staging.ContainerReceived),
no_of_flatcases = staging.NoOfFlatcases,
voyage = staging.Voyage,
mbl_number = staging.MBLNumber,
redirect_to_SACD = ParseDate(staging.RedirectToSACD),
shipment_status = "SAVED",
supplier_id = supplier.id,
created_at = DateTime.Now,
created_by = staging.ImportSession.CreatedBy,
updated_at = DateTime.Now,
updated_by = staging.ImportSession.CreatedBy
};
_context.sdsShipment.Add(shipment);
}
///
/// Validates date string format
///
private bool IsValidDate(string dateString)
{
if (string.IsNullOrWhiteSpace(dateString))
return true;
return DateTime.TryParse(dateString, CultureInfo.InvariantCulture, DateTimeStyles.None, out _);
}
///
/// Parses date string to DateTime
///
private DateTime? ParseDate(string dateString)
{
if (string.IsNullOrWhiteSpace(dateString))
return null;
if (DateTime.TryParse(dateString, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
return date;
return null;
}
#endregion
}
#region Helper Classes
///
/// Result of Excel file processing
///
public class ExcelDataResult
{
public List Headers { get; set; } = new List();
public List Rows { get; set; } = new List();
}
///
/// Result of validation process
///
public class ValidationResult
{
public int ValidCount { get; set; }
public int ErrorCount { get; set; }
public List Errors { get; set; } = new List();
}
#endregion
}