namespace ZB.MOM.WW.ScadaBridge.Commons.Types;

/// <summary>
/// One parsed per-instance native-alarm-source-override CSV row: the source
/// binding's canonical name plus the three optional retarget fields. A blank
/// (null) <see cref="Connection"/>/<see cref="SourceReference"/>/<see cref="Filter"/>
/// means "keep the inherited value" — the same blank-is-inherited semantics as the
/// CLI <c>instance native-alarm-source set</c> command. <see cref="LineNumber"/> is
/// the 1-based source line (the header is line 1) so downstream errors can point
/// back at the operator's file.
/// </summary>
public sealed record NativeAlarmSourceOverrideCsvRow(
    string SourceName, string? Connection, string? SourceReference, string? Filter, int LineNumber);

/// <summary>
/// Outcome of parsing a native-alarm-source-override CSV: the successfully-parsed
/// <see cref="Rows"/> plus per-line <see cref="Errors"/>. Parsing never throws —
/// malformed rows are reported and excluded, valid rows still flow through.
/// Downstream callers validate source names against the flattened instance; this
/// parser is purely syntactic.
/// </summary>
public sealed record NativeAlarmSourceOverrideCsvParseResult(
    IReadOnlyList<NativeAlarmSourceOverrideCsvRow> Rows, IReadOnlyList<string> Errors);

/// <summary>
/// Pure, dependency-free, quote-aware parser turning per-instance
/// native-alarm-source-override CSV text into structured rows plus per-line errors.
/// Callers supply the text (no file I/O). The header row is required and
/// case-insensitive (<c>SourceName,Connection,SourceReference,Filter</c>). Fields
/// follow the shared <see cref="CsvLineSplitter"/> RFC-4180 quoting rules — the same
/// splitter <see cref="OverrideCsvParser"/> uses. The parity analogue of
/// <see cref="OverrideCsvParser"/> for native alarm source retargets.
/// </summary>
public static class NativeAlarmSourceOverrideCsvParser
{
    private const string HeaderError =
        "Missing or invalid header row. Expected 'SourceName,Connection,SourceReference,Filter'.";

    private const int ExpectedColumns = 4;

    /// <summary>
    /// Parses native-alarm-source-override CSV <paramref name="csvText"/>. Returns
    /// parsed rows and any per-line errors; never throws. On a missing/unrecognized
    /// header returns zero rows and a single header error.
    /// </summary>
    /// <param name="csvText">The raw CSV text to parse.</param>
    /// <returns>The parsed rows and any per-line errors.</returns>
    public static NativeAlarmSourceOverrideCsvParseResult Parse(string csvText)
    {
        var rows = new List<NativeAlarmSourceOverrideCsvRow>();
        var errors = new List<string>();

        // Split into physical lines; \r\n and \r are normalized to \n boundaries.
        var lines = (csvText ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');

        var headerSeen = false;

        for (var i = 0; i < lines.Length; i++)
        {
            var lineNumber = i + 1;
            var rawLine = lines[i];

            // Skip fully-blank lines (whitespace-only included) without error.
            if (string.IsNullOrWhiteSpace(rawLine))
                continue;

            if (!CsvLineSplitter.TrySplit(rawLine, out var fields))
            {
                errors.Add($"Line {lineNumber}: Unterminated quoted field.");
                continue;
            }

            if (!headerSeen)
            {
                if (!TryMatchHeader(fields))
                {
                    errors.Add(HeaderError);
                    return new NativeAlarmSourceOverrideCsvParseResult(rows, errors);
                }

                headerSeen = true;
                continue;
            }

            if (fields.Count != ExpectedColumns)
            {
                errors.Add(
                    $"Line {lineNumber}: expected {ExpectedColumns} columns but found {fields.Count}.");
                continue;
            }

            var sourceName = fields[0];
            if (string.IsNullOrWhiteSpace(sourceName))
            {
                errors.Add($"Line {lineNumber}: SourceName must not be blank.");
                continue;
            }

            rows.Add(new NativeAlarmSourceOverrideCsvRow(
                sourceName,
                NullIfEmpty(fields[1]),
                NullIfEmpty(fields[2]),
                NullIfEmpty(fields[3]),
                lineNumber));
        }

        if (!headerSeen)
            errors.Add(HeaderError);

        return new NativeAlarmSourceOverrideCsvParseResult(rows, errors);
    }

    /// <summary>Matches the required 4-column header (case-insensitive).</summary>
    private static bool TryMatchHeader(IReadOnlyList<string> fields) =>
        fields.Count == ExpectedColumns &&
        HeaderEquals(fields[0], "SourceName") &&
        HeaderEquals(fields[1], "Connection") &&
        HeaderEquals(fields[2], "SourceReference") &&
        HeaderEquals(fields[3], "Filter");

    private static bool HeaderEquals(string field, string expected) =>
        string.Equals(field, expected, StringComparison.OrdinalIgnoreCase);

    private static string? NullIfEmpty(string field) => field.Length == 0 ? null : field;
}
