using System.Globalization;
namespace Campaign_Tracker.Server.LegacyData.Schema;
///
/// Parses the Access schema text dump shipped at
/// Initial Documents/Access_Schema.txt into a strongly-typed
/// for compatibility checking.
///
/// File format (one table per block):
///
/// Table: Contacts
/// ---------------
/// Column: ID Type: 3 Size: Nullable: False
/// Column: EMAIL Type: 130 Size: 255 Nullable: True
///
///
public static class LegacySchemaBaselineParser
{
public static LegacySchemaBaseline ParseFile(string filePath, DateTimeOffset capturedAt)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException(
$"Legacy schema baseline file not found: {filePath}", filePath);
}
var text = File.ReadAllText(filePath);
return Parse(text, filePath, capturedAt);
}
public static LegacySchemaBaseline Parse(string text, string source, DateTimeOffset capturedAt)
{
var tables = new List();
string? currentTable = null;
var currentColumns = new List();
foreach (var rawLine in text.Split('\n'))
{
var line = rawLine.TrimEnd('\r').TrimEnd();
if (line.Length == 0) continue;
if (line.All(c => c == '-')) continue;
if (line.StartsWith("Table:", StringComparison.Ordinal))
{
FlushTable(tables, currentTable, currentColumns);
currentTable = line["Table:".Length..].Trim();
currentColumns = new List();
continue;
}
var trimmed = line.TrimStart();
if (!trimmed.StartsWith("Column:", StringComparison.Ordinal))
{
throw new FormatException($"Unrecognized legacy schema line: {line}");
}
currentColumns.Add(ParseColumn(trimmed));
}
FlushTable(tables, currentTable, currentColumns);
if (tables.Count == 0)
{
throw new FormatException("Legacy schema baseline did not contain any table definitions.");
}
return new LegacySchemaBaseline(tables, source, capturedAt);
}
private static void FlushTable(
List sink,
string? tableName,
List columns)
{
if (string.IsNullOrWhiteSpace(tableName) || columns.Count == 0) return;
sink.Add(new LegacyTableDefinition(tableName, columns.ToArray()));
}
private static LegacyColumnDefinition ParseColumn(string line)
{
// "Column: NAME Type: 130 Size: 255 Nullable: True"
var name = ReadField(line, "Column:", ["Type:"]);
var typeRaw = ReadField(line, "Type:", ["Size:", "Nullable:", "Constraints:"]);
var sizeRaw = ReadField(line, "Size:", ["Nullable:", "Constraints:"]);
var nullableRaw = ReadField(line, "Nullable:", ["Constraints:"]);
var constraintsRaw = ReadField(line, "Constraints:", []);
if (!int.TryParse(typeRaw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var typeCode))
{
throw new FormatException($"Invalid Type code in column line: {line}");
}
int? size = null;
if (!string.IsNullOrWhiteSpace(sizeRaw))
{
if (!int.TryParse(sizeRaw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedSize))
{
throw new FormatException($"Invalid Size value in column line: {line}");
}
size = parsedSize;
}
if (!bool.TryParse(nullableRaw, out var nullable))
{
throw new FormatException($"Invalid Nullable value in column line: {line}");
}
return new LegacyColumnDefinition(name, typeCode, size, nullable)
{
Constraints = ParseConstraints(constraintsRaw),
};
}
private static string ReadField(string line, string label, string[] terminators)
{
var start = line.IndexOf(label, StringComparison.Ordinal);
if (start < 0) return string.Empty;
start += label.Length;
var end = line.Length;
foreach (var terminator in terminators)
{
var t = line.IndexOf(terminator, start, StringComparison.Ordinal);
if (t >= 0 && t < end) end = t;
}
return line[start..end].Trim();
}
private static IReadOnlyList ParseConstraints(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return [];
}
return value
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.ToArray();
}
}